深入 Kibana 02 - Saved Object:一切状态的统一模型
上一篇确立了 Kibana 架构三层的必要性,Node.js server 的存在不是透明代理,而是承载凭据隔离、后台任务和持久化逻辑的应用层。这一篇进入状态层:Kibana 把所有用户配置统一表达为 Saved Object。Saved Object 容易被误解成"配置项的 JSON 存档"。更准确的说法是:它是一套带版本迁移框架、带引用图、带命名空间隔离的持久化协议,所有 Kibana 功能(Discover、Lens、Dashboard、Alerting)都在这套协议之上构建,而不是各自独立地往 ES 写数据。本文只抓一个问题:type、attributes、references 三段结构解决了什么,迁移框架和导入导出的本质是什么。
Saved Object 的结构
每个 Saved Object 在 .kibana 系列索引里是一条 ES 文档,其 _id 格式为 <type>:<uuid>,_source 里携带固定的元数据字段加上一个以 type 命名的嵌套对象存放实际载荷:
1 | |
type 字段决定这条文档用哪套 schema 解析 attributes,也决定 Saved Objects Service 在执行 CRUD 时用哪个迁移函数链。不同 type 的对象共存于同一个 .kibana 索引(自 8.0 起按命名空间路由到 .kibana_<space> 系列,但单个 Space 内仍混存)。
attributes 是对业务逻辑完全不透明的 payload,只有拥有该 type 的插件知道如何解读它。references 是从该对象到其他对象的有向边——Dashboard 引用多个 Lens 对象,Lens 引用 Data View,Data View 不引用其他 SO。这张引用图是导入导出的遍历依据。
type 注册与已知类型
每个 Kibana 插件在 setup 阶段通过 core.savedObjects.registerType(definition) 注册一个或多个 SO 类型。注册时声明:
1 | |
migrations 对象里的每个函数接收旧版本的 attributes,返回新版本的 attributes。升级 Kibana 时,core 会扫描 .kibana 索引里所有文档的 migrationVersion,对版本落后的对象依次应用缺少的迁移函数,写回更新后的文档。这个过程在 Kibana server 启动阶段完成,完成前 server 不接受用户请求。
已知的核心类型包括:index-pattern(自 8.0 起实质上被 data-view 取代,index-pattern 作为别名保留)、visualization、lens、dashboard、search(Saved Search)、alert、action、tag、space、config(每个 Space 的 Advanced Settings)、url(短链接)。
references 的作用:引用图
references 数组里每个元素是 { type, id, name },name 是在当前对象的 attributes 里引用这个外部对象的局部标识符(不是外部对象的 id)。
以 Dashboard 引用 Lens 为例:
1 | |
attributes.panels[0].panelRefName 是 "panel_0",references 里 name 为 "panel_0" 的条目指向真实的 lens 对象 id "lens-xyz"。这一间接层是为了让导入导出时 id 可以被重映射——同一个 Dashboard 导入到另一个 Kibana 实例时,lens 对象会分配新 id,references 里的 id 随之更新,而 attributes 里的 panelRefName 不变。
这种设计把 id 的稳定性要求从 attributes(业务载荷)里解耦出来,让 attributes 可以被直接 diff,而不用担心 id 引用在不同实例间漂移。
迁移框架:升级时发生了什么
Kibana 大版本升级的迁移过程分为以下阶段(自 7.11 起采用 WAIT_FOR_YELLOW_SOURCE → CLONE → MIGRATE → SWITCH 的四段流程,8.x 维持同一模型):
1 | |
迁移期间旧索引只读,不影响正在运行的旧版 Kibana(如果做滚动升级)。新索引就绪后,别名切换是单次原子操作。
自 8.0 起引入了"zdt(zero downtime)migration"实验特性,允许在不停机的条件下完成迁移,但截至 8.13 仍为 opt-in 模式。
实验:通过 Saved Objects API 观察结构
在 Dev Tools 里用 Saved Objects API 创建一个简单对象,观察它在 ES 里的实际存储:
1 | |
1 | |
1 | |
第 3 步的响应会展示 _id 的 type:uuid 格式,以及 _source.type、_source.tag(type 名作为嵌套对象键)等字段。对比 API 返回和 ES 原始文档,可以看到 Saved Objects Service 做了哪些字段映射和命名空间注入。
接着导出一个 Dashboard 观察引用图:
1 | |
响应是 NDJSON(Newline-Delimited JSON),每行一个 Saved Object,最后一行是导出元数据。includeReferencesDeep: true 会让导出器沿着引用图递归收集所有被引用的对象(Lens、Data View 等),保证导入时依赖完整。
将实验结果映射回内部对象
POST /api/saved_objects/<type> 最终调用的是 server 端 SavedObjectsRepository 的 create() 方法。这个 repository 把入参包装成 ES index 请求写入 .kibana 索引,_id 按 type:uuid 格式生成,_source 里注入 type、updated_at、migrationVersion 等元数据字段。
_export 端点的实现是 SavedObjectsExporter,它接受 type 列表或 object 列表作为起始节点,通过 SavedObjectsRepository.bulkGet() 批量拉取,再用 references 字段做图的 BFS(广度优先搜索),把所有可达节点收集后序列化为 NDJSON。
模式提炼
引用图替代外键约束。关系型数据库用外键保证引用完整性,Saved Object 在 ES 里没有约束能力,选择把引用声明在 references 数组里并交由应用层(SavedObjectsExporter/Importer)维护一致性。这让 ES 不需要做跨文档的事务操作,代价是删除被引用对象时不会自动级联,需要 Kibana 自己做孤儿清理。
迁移函数链可静态组合。每个版本号对应一个纯函数 (doc) => doc,升级时把函数链按版本排序后依次 reduce,中间结果可以被测试。这比"数据库迁移脚本按顺序执行"的模型更容易做单元测试和回归检查。
工程迁移表
| Kibana 概念 | CMS 对应 | 低代码平台对应 | Terraform 对应 |
|---|---|---|---|
| Saved Object type | CMS content type (e.g. Contentful) | 组件注册表 / widget registry | resource type |
| attributes | CMS 内容 payload | 组件 props schema | resource arguments |
| references | CMS 关联字段 (link/relation) | 组件依赖声明 | resource depends_on |
| migrations | CMS content migration script | 平台升级 schema migration | terraform state migration |
| namespaces/spaces | CMS multi-site / locale | 租户隔离 / workspace | workspace / module |
| export/import (NDJSON) | CMS 内容迁移包 | 低代码应用导出包 | terraform plan + state |
常见误解
不同 Kibana 功能的配置存在不同的"配置文件"里。所有用户配置(Dashboard、Alerting、Data View、Space 设置)都存为 Saved Object,进同一套 .kibana 索引,通过 type 字段区分。唯一例外是 kibana.yml,它存放的是 server 启动级配置(端口、ES 地址、TLS 证书),不是用户运行时配置。
Saved Object 的 id 是稳定的跨实例标识。id 只在单个 Kibana 实例内稳定。导入到另一个实例时,同一个对象会分配新 id(除非使用 overwrite: true 且手动对齐 id)。跨实例共享配置的正确方式是导出-导入流程,不是硬编码 id。
迁移会破坏旧版本 Kibana 的访问能力。迁移完成后旧别名 .kibana 切换到新索引,旧版本 Kibana 如果仍在运行会看到新格式的文档,通常会因为 migrationVersion 高于自身已知版本而拒绝处理,不会静默损坏数据。
练习
在 Dev Tools 里执行 GET .kibana/_mapping,观察 mapping 里有多少个嵌套对象(每个注册的 SO type 都有一个对应的 properties 块),理解为什么 .kibana 的 mapping 会随插件数量增长而变大。
创建一个 Dashboard,往里加两个不同的 Lens 可视化,然后用 POST kbn:/api/saved_objects/_export 导出,用文本工具数一数 NDJSON 里有多少行,对应哪些 type,理解引用图的深度。
在 Kibana 的 Stack Management → Saved Objects 页面,勾选一个 Dashboard 执行导出,再在另一个 Space 里执行导入,观察导入时 id 是否发生变化。
系列导航
| 篇 | 主题 | 核心问题 |
|---|---|---|
| 00 | 导读:状态存在 ES 里 | Kibana 是应用平台,不是 GUI |
| 01 | 架构:三层与 New Platform | 为什么需要 Node.js server |
| 02(本篇) | Saved Object:统一状态模型 | type / attributes / references |
| 03 | Data View:查询前的字段抽象 | 时间字段、字段格式化、运行时字段 |
