上一篇展示了 New Platform 的插件体系如何通过 setup/start 生命周期和 contract 模式组织代码。这一篇进入性能模型,核心问题是:首屏加载的 bundle 体积如何控制、插件如何按需加载,以及 Search Session 如何让长查询可以后台运行并复用结果。

前端 bundle 体系

Kibana 的前端代码量巨大。Discover、Lens、Dashboard、Maps、Security、Observability 加在一起,编译后的 JavaScript 体积以百 MB 计。如果把所有代码打成一个 bundle 让用户一次性下载,首屏时间会无法接受。

Kibana 的解法是三层 bundle 结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
浏览器请求 Kibana 页面

├── bootstrap.js 核心 shell,极小,总是先加载
│ └── 初始化运行时,注册模块加载器,决定加载哪些插件

├── core bundle 平台公共服务
│ └── http client · savedObjects client · uiSettings · notifications

└── app plugin bundle 按需加载
├── discover.chunk.js ← 用户导航到 Discover 时才加载
├── lens.chunk.js ← 用户导航到 Lens 时才加载
├── dashboard.chunk.js
└── ...(几十个插件各自的 bundle)

bootstrap.js 是 Kibana server 动态生成的,它携带当前用户能访问的插件列表(受 Spaces 和许可证过滤)以及每个插件 bundle 的哈希路径。这意味着两个用户登录同一个 Kibana,拿到的 bootstrap.js 内容可能不同。

core bundle 包含所有插件都会用到的平台服务,在 bootstrap.js 之后立即加载。从 Kibana 8 起,core bundle 经过积极的 tree-shaking,把不被任何插件用到的服务剔出去。

app plugin bundle 使用 Webpack code splitting 生成。当用户在 SPA 里导航到某个应用(比如从 Home 点进 Discover),Kibana 的 ApplicationService 才触发对应 plugin bundle 的动态 import。在此之前,这个插件的代码不在内存里。

bootstrap 阶段的详细序列

用浏览器 DevTools 的 Network 面板看一次完整的 Kibana 首次加载,可以观察到以下顺序。

1
2
3
4
5
6
7
8
1. GET /                         HTML shell(极小,仅包含 <script> 标签)
2. GET /bootstrap.js 动态生成,含插件列表和 bundle URL
3. GET /bundles/core/core.chunk.js
4. GET /bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.js
React、RxJS、lodash 等共享依赖
5. GET /api/core/capabilities 当前用户的功能权限集
6. (用户进入某个 App 时)
GET /bundles/plugin/discover/discover.chunk.js

步骤 5 的 capabilities 请求很关键:Kibana 在渲染导航菜单之前需要知道当前用户在当前 Space 里能访问哪些应用,这个请求的耗时直接影响用户看到主导航的时间。

共享依赖(React、ReactDOM、RxJS、lodash、EUI)被提取到 kbn-ui-shared-deps bundle 里,所有插件共享同一份,不重复打包。这是控制总体积的关键措施:否则每个插件各自打包一份 React,体积会翻数倍。

Node.js server 的内存模型

Kibana server 是一个 Node.js 进程,默认不做多进程/多线程(Worker Threads 在部分场景下使用,如 reporting)。

1
2
3
4
5
6
7
Kibana Node.js process

├── HTTP server(hapi.js) 处理来自浏览器的请求
├── ES client pool 复用到 ES 集群的 HTTP 连接
├── Saved Object service 读写 .kibana 系统索引
├── Task Manager 后台任务调度(告警、报表、Search Session 清理)
└── Plugin runtime 各插件的 start() 注册的 service

单进程模型意味着 CPU 密集型任务(如大报表生成)会阻塞事件循环。Kibana 为此把 Reporting 和部分 ML 任务移到了独立的后台处理路径。

关键配置项:

  • server.maxPayload:默认 1048576 bytes(1 MB),控制请求体上限。Dashboard 导入大型 ndjson 文件时经常需要调大。
  • elasticsearch.requestTimeout:默认 30000 ms。超过这个时间 Kibana server 会主动放弃等待 ES 响应。
  • node.options:可以通过 --max-old-space-size 调整 V8 heap 上限,默认约 1.4 GB。

Search Session:长查询的后台运行

Kibana 的 Dashboard 经常包含几十个 panel,每个 panel 是一条独立的 ES 查询。在数据量大或集群负载高时,完整刷新一个 Dashboard 可能需要数分钟。如果用户在查询完成前关掉了浏览器标签,所有进行中的查询就浪费了。

Search Session 是解这个问题的机制(在 Kibana 7.12 引入,7.x 期间标记为 Technical Preview,8.x 趋于稳定)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Search Session 工作流

├── 1. 用户点击"在后台运行"
│ Kibana 为当前查询集合创建一个 Search Session

├── 2. 每条查询通过 ES Async Search API 提交
│ POST /_async_search
│ → 返回 asyncId(不等结果完成就返回)

├── 3. Kibana 把 sessionId + asyncId 列表写入 Saved Object
│ type: search-session
│ { urls: [...], asyncSearchIds: [...], status: running }

├── 4. 用户关闭页面,查询在 ES 侧继续执行

├── 5. 后台 Task Manager 轮询 asyncId,更新 session 状态

└── 6. 用户回来,从 Sessions 管理界面恢复
Kibana 检查 asyncId 是否还有效
有效 → 直接用缓存结果渲染 Dashboard,不重新查询
过期 → 标记 session expired,提示重新运行

Search Session 的结果复用有两个维度。第一是跨时间:同一用户把查询发到后台,一小时后回来还能看到结果,不需要重新等待。第二是跨用户:Kibana 8.6 之后,相同参数的查询在 ES 侧命中同一个 async search 缓存,不同用户打开同一个 Dashboard 可能复用同一批结果(前提是 ES 的 async search 结果还在缓存里)。

Search Session 的 Saved Object 本身有过期时间,由 xpack.data_enhanced.search.sessions.defaultExpiration 配置(默认 7 天)。Task Manager 有一个定时清理任务,把过期的 session 删掉。

实验:观察 bundle 加载序列和 Search Session

第一个实验:用 Chrome DevTools 观察 bundle 加载。

打开一个空的 Kibana 实例(清空浏览器缓存),打开 DevTools Network 面板,勾选 Disable cache,然后访问 Kibana 主页。

过滤 .js 请求,观察:

  • bootstrap.js 是不是最先出现
  • core.chunk.jskbn-ui-shared-deps-npm.js 的体积
  • 点击进入 Discover 时,是否出现一个新的 discover.chunk.js 请求

用 DevTools 的 Coverage 工具(More tools → Coverage)可以看到各 bundle 文件的实际执行覆盖率,未使用代码比例通常在 60-80%,说明 tree-shaking 还有很大空间。

第二个实验:创建和恢复 Search Session。

在 Kibana(需要 Platinum 许可证或试用模式)打开一个含多个 panel 的 Dashboard,点击时间选择器旁边的 “Run in background” 按钮(或在 Stack Management → Search Sessions 里找到入口),把当前查询推入后台。

关掉标签,过一分钟后在 Stack Management → Search Sessions 里找到刚才的会话,点击 View,观察 Dashboard 是否无需等待就渲染完成。

用 Kibana 的 Dev Tools 直接检查 session 状态:

1
2
3
4
5
GET .kibana/_search
{
"query": { "term": { "type": "search-session" } },
"_source": ["search-session.name", "search-session.status"]
}

模式提炼

三层 bundle(bootstrap + core + plugin)是大型 SPA 的标准分层:最小 shell 先到,平台依赖次之,业务功能按需到。这个模式在 React 生态里对应 React.lazy + Suspense 的拆分策略。

共享依赖提取(kbn-ui-shared-deps)是多插件架构下控制体积的必要措施。没有这一层,每个插件各自打包 React 和 EUI,总体积会是现在的数倍。

Search Session 的设计把"查询结果"建模成 Saved Object,使结果具备了持久化、跨用户共享、有过期时间的属性。这比在内存里缓存查询结果更可靠,也更容易在多实例 Kibana 集群里共享。

工程迁移表

问题 Kibana 解法 迁移到其他系统的等价模式
大型 SPA 首屏慢 三层 bundle + lazy loading Route-based code splitting + shared vendor chunk
各插件重复打包依赖 kbn-ui-shared-deps 共享层 Module Federation 或 externals 提取
长查询用户等不住 Async Search + Search Session 提交任务 → 返回 job ID → 轮询 → 结果持久化
查询结果无法跨会话复用 Session Saved Object + Task Manager 结果持久化到 DB,带 TTL,后台任务清理
内存堆上限不够 node.options max-old-space-size JVM -Xmx / Go GOMEMLIMIT 等效调整

常见误解

增加 Kibana 实例数就能线性提升查询性能。不能。Kibana 实例是无状态的请求代理,查询性能的瓶颈在 Elasticsearch 集群,多开 Kibana 实例不会提升 ES 的处理能力。

Search Session 只对慢查询有用。不对。Search Session 对任何需要"稍后回来看结果"或"跨用户共享相同查询结果"的场景都有价值,即使单条查询只要几秒。

elasticsearch.requestTimeout 控制 ES 集群内部查询的超时。不对。这个参数控制的是 Kibana server 等待 ES HTTP 响应的时间,不是 ES 内部执行查询的时间。ES 内部的查询超时由 ES 侧的 search.default_search_timeoutcancel_after_time_interval 控制。

练习

  1. 在本地 Kibana 开发环境里运行 yarn build --no-optimizeryarn build,对比有无 optimizer 时的产物目录结构和各 bundle 文件体积。

  2. GET /api/ui_counters 查看 Kibana 内置的前端性能指标上报,理解 Kibana 如何追踪插件加载耗时。

  3. 在 Dev Tools 里用 POST /api/data_enhanced/search 直接提交一条慢查询(给一个大索引做 expensive aggregation),记录返回的 id,然后用 GET /api/data_enhanced/search/{id} 轮询结果,观察 isRunning 字段从 true 变为 false 的过程。

系列导航

  • 上一篇:深入 Kibana 14 - 插件体系:New Platform 的 setup/start 生命周期
  • 下一篇:深入 Kibana 16 - Kibana vs Grafana:两种可视化平台的设计取舍

参考资料