上一篇确立了"Kibana 的状态存在 Elasticsearch 里"这条主线。这一篇进入架构层:浏览器、Node.js server 与 Elasticsearch 三层之间发生了什么。三层架构容易被误解成"Kibana server 只是一个反向代理"。更准确的说法是:Node.js 层承载了凭据隔离、后台任务、报表生成、插件生命周期等纯粹属于应用平台的职责,不是透明转发。本文只抓一个问题:为什么 Kibana 需要自己的 Node.js 服务,这个服务在请求链路里做了什么。

三层结构

请求从浏览器出发,经过 Node.js server,最终到达 Elasticsearch。每一层的职责边界如下:

1
2
3
4
5
6
7
8
Browser                  Node.js Server              Elasticsearch
─────────────────── ────────────────────────── ───────────────────────
React UI (plugins) ──► HTTP routes / auth proxy ──► .kibana system indices
KQL / Lens editor Saved Objects service business indices
Dashboard renderer Task Manager _cluster / _nodes / _cat
Dev Tools terminal Reporting engine
Alerting executor
Plugin setup/start lifecycle

浏览器端是一批 React 应用,以插件 bundle 的形式加载。每个 Kibana 功能区(Discover、Lens、Dashboard、Alerting)都是独立插件,在浏览器里注册到 core 提供的客户端框架。

Node.js server 拦截所有浏览器到 ES 的请求,在这里验证凭据、注入 Kibana 自己的认证头,然后以 Kibana 服务账号的身份向 ES 发出实际请求。ES 看到的始终是 Kibana server 的凭据,浏览器端的用户凭据不直接到达 ES。

Elasticsearch 同时扮演两个角色:业务数据的存储和查询后端,以及 Kibana 自身状态的持久化存储(.kibana* 系列索引)。

为什么 Kibana 必须有自己的 server

纯浏览器直连 ES 的架构(Kibana 3 的方式)在以下场景下失效:

凭据安全问题。浏览器直连要求把 ES 的地址和认证信息暴露给前端。任何打开页面的用户都能拿到这些凭据直接访问 ES。加一层 server 后,凭据留在 server 端,浏览器只持有对 Kibana HTTP 端点的会话 token。

后台任务调度。告警规则要定时执行查询——用户不开着浏览器这个任务也得跑。Kibana 的 Task Manager 是一个跑在 server 端的调度器,把任务定义存在 .kibana_task_manager 索引,server 启动后持续轮询待执行任务。这类长周期后台逻辑在浏览器里没有稳定的执行环境。

报表生成。Reporting 功能用 headless Chromium 渲染 Kibana 页面截图,或把 Discover 结果导出为 CSV。headless 浏览器和文件系统 IO 都只能在 server 端完成。

机器学习与数据摄入辅助。自 7.x 起 Kibana 内置了多个数据向导(Fleet、Integrations),这些功能需要代理 ES 的 Index Template API 调用,并在 server 端做配置校验。

New Platform 插件模型

Kibana 7.x 完成了从 Legacy Platform 到 New Platform 的迁移,插件生命周期从简单的"注册一堆全局对象"变成了显式的 setup/start/stop 三段式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Plugin Lifecycle (server side)
──────────────────────────────────────────────────────
Kibana core bootstrap

├─ plugin.setup(core, plugins)
│ 注册路由、注册 Saved Object 类型、声明依赖
│ 此时其他插件还未 start,不能调用跨插件 API

├─ plugin.start(core, plugins)
│ 所有插件已初始化完毕,可以调用跨插件服务
│ Task Manager 在此阶段注册任务类型
│ Alerting 在此阶段注册规则类型

└─ plugin.stop()
清理定时器、关闭连接

同样的三段式在浏览器端也有对应,browser-side 插件的 setup 阶段注册导航菜单项和路由,start 阶段初始化与 server 的连接。

New Platform 的核心约束是:插件间依赖必须在 setup 阶段显式声明,不允许全局单例引用。这一设计让 Kibana 可以在不重启整个 server 的条件下热更新单个插件(自 8.x 起对部分插件生效),也让插件间的依赖关系可以在启动时做图遍历检查,循环依赖会直接报错。

请求链路追踪

一次从 Discover 发出的查询,在三层里的路径如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 浏览器 Discover 插件
构造 KQL → 转换为 ES Query DSL
POST /internal/search/es (Kibana internal API)

2. Node.js server
验证用户会话 (cookie / API key)
调用 data.search 服务 (Search Service)
Search Service 注入 kibana.version
向 ES 发 POST /<target_indices>/_search

3. Elasticsearch
执行查询,返回 hits + aggregations

4. Node.js server
把 ES 响应按 Kibana 的 response schema 封装
返回给浏览器

5. 浏览器
Discover 插件渲染 hits 表格和时间轴

浏览器看到的是 Kibana 的 internal API(路径通常是 /internal//api/),从不直接调 ES。这一层隔离让 Kibana 可以在 server 端做限流、做结果缓存、做字段格式化,而浏览器插件不需要知道 ES 的实际地址。

实验:通过 Status API 观察 server 信息

Kibana 暴露了一个公开的 status 端点,可以不登录查看 server 侧基本信息:

1
curl -s http://localhost:5601/api/status | python3 -m json.tool | head -60

响应里的关键字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "kibana",
"uuid": "...",
"version": {
"number": "8.13.0",
"build_hash": "...",
"build_date": "..."
},
"status": {
"overall": { "level": "available" },
"core": {
"elasticsearch": { "level": "available", "summary": "..." },
"savedObjects": { "level": "available", "summary": "..." }
}
},
"metrics": {
"process": { "memory": { "heap": { "used_in_bytes": ... } } },
"os": { "load": [...] }
}
}

status.core.elasticsearch 反映 Kibana server 与 ES 的连通状态,status.core.savedObjects 反映 .kibana 系列索引的迁移状态。server 启动时会等这两项都变为 available 才开始接受用户请求。

用 ES 的 _nodes/stats 也可以观察到 Kibana 对 ES 的 HTTP 连接:

1
GET _nodes/stats/http

在返回的 nodes.<node_id>.http.total_opened 里,持续打开的 HTTP 连接中有一部分来自 Kibana server 的连接池(自 8.x 起默认使用长连接池,减少 TLS 握手开销)。

将实验结果映射回内部对象

Status API 的 status.core.savedObjects 对应 server 端的 SavedObjectsService——这个服务在 setup 阶段接收各插件注册的类型定义,在 start 阶段完成 .kibana 索引的 mapping 迁移(如有)后才标记为 available。

status.core.elasticsearch 对应 ElasticsearchService,管理 Kibana server 到 ES 的 HTTP 客户端连接池。这个客户端有两个实例:internalClient(用 kibana system 账号,用于操作 .kibana* 系列索引)和 scopedClient(用请求用户的凭据,用于代理用户的业务查询)。

模式提炼

三层架构里值得提炼的两个设计决策:

凭据作用域分离。server 用 internalClient 操作系统索引,用 scopedClient 代理用户请求。两个客户端的凭据完全独立,保证了 Kibana 系统索引的操作权限不会因为一个低权限用户的请求而受影响。

插件生命周期显式化。setup/start/stop 强制插件在正确的时间点注册服务和路由,不允许"在某个函数里偷偷初始化全局对象"。代价是插件开发者要理解两阶段启动的约束,收益是整个系统的依赖关系可静态分析。

工程迁移表

Kibana 概念 Grafana 对应 BI 工具对应 Next.js / Nuxt 模式
Node.js server 凭据代理 Grafana backend plugin data proxy BI server-side JDBC/ODBC driver API routes with server-side fetch
New Platform setup/start Grafana plugin activate() 无显式生命周期 nuxt.config module setup hooks
Task Manager Grafana internal scheduler (alerting) BI scheduled reports engine Vercel Cron Jobs / BullMQ
Reporting (headless Chrome) Grafana image renderer plugin BI PDF export service Puppeteer in serverless function
Plugin bundle splitting Grafana lazy panel loading 无直接对应 Next.js dynamic imports

常见误解

Kibana server 只是 ES 的反向代理。实际上 server 端有 Task Manager、Reporting、Alerting executor、Saved Objects migration 等完全不是代理的逻辑。纯粹的代理不需要自己的数据存储,而 Kibana 依赖 .kibana_task_manager 索引来协调多实例下的任务分配。

多个 Kibana 实例会造成配置冲突。多实例模式下(水平扩展),每个实例都连同一个 ES 集群,.kibana 索引是共享存储。Task Manager 用 optimistic locking(版本号 + seq_no)来协调任务认领,保证同一任务只被一个实例执行。

升级 Kibana 时 ES 的业务数据会受影响。Kibana 的迁移只写 .kibana* 系列系统索引,不触碰业务索引。.kibana_8.x.0_001 这类带版本号的索引是每次大版本升级后新建的,旧索引保留一段时间后由迁移脚本归档或删除。

浏览器插件可以直接调 ES API。自 New Platform 起 ES 的地址和凭据对浏览器端插件完全不可见,插件只能通过 data.searchhttp.fetch 这类 core 提供的 client 间接访问 ES。

练习

在 Dev Tools 里执行 GET _cat/indices/.kibana*?v,观察有多少个 .kibana 系列索引,它们的文档数量分别是多少。对比 Kibana 大版本号和索引名中的版本号。

在 Kibana 的 Stack Management → Task Manager Health 页面(路径 /app/management/insightsAndAlerting/taskManager)观察当前实例的任务调度状态,记录 claim_conflicts 指标,理解多实例下的任务竞争模型。

curl -s http://localhost:5601/api/status 在 Kibana 启动后的不同阶段多次调用,观察 status.core.savedObjectsinitializing 变为 available 的过程,理解迁移锁的存在。

系列导航

主题 核心问题
00 导读:状态存在 ES 里 Kibana 是应用平台,不是 GUI
01(本篇) 架构:三层与 New Platform 为什么需要 Node.js server
02 Saved Object:统一状态模型 type / attributes / references
03 Data View:查询前的字段抽象 时间字段、字段格式化、运行时字段

参考资料