深入 Kibana 02 - Saved Object:一切状态的统一模型
Dashboard、Data View、Lens 配置和告警规则看起来分属不同功能,落到持久化层却共享同一套 Saved Object 协议。它用 type、attributes、references 和版本迁移机制管理跨插件状态,避免每个功能各自设计一套存储格式。导入、导出与升级迁移也由这层协议统一承接。
Saved Object 的结构
每个 Saved Object 在 .kibana 系列索引里是一条 ES 文档,其 _id 格式为 <type>:<uuid>,_source 里携带固定的元数据字段加上一个以 type 命名的嵌套对象存放实际载荷:
1 | |
type 字段决定这条文档用哪套 schema 解析 attributes,也决定 Saved Objects Service 在执行 CRUD 时用哪个升级模型。不同 type 的对象共存于同一个 .kibana 系统索引;Space 只是通过 namespace 字段做逻辑隔离,不是把索引拆成 .kibana_<space> 系列。真正会变的是升级时的版本化索引别名,例如 .kibana_9.4.0_001 这种按版本滚动的新索引。
attributes 是对业务逻辑完全不透明的 payload,只有拥有该 type 的插件知道如何解读它。references 是从该对象到其他对象的有向边——Dashboard 引用多个 Lens 对象,Lens 引用 Data View,Data View 不引用其他 SO。这张引用图是导入导出的遍历依据。
type 注册与已知类型
每个 Kibana 插件在 setup 阶段通过 core.savedObjects.registerType(definition) 注册一个或多个 SO 类型。注册时声明:
1 | |
modelVersions 里的每个版本描述一组 schema / mapping 变化。升级 Kibana 时,core 会根据类型注册表把文档转换到目标模型版本;在传统 stack 升级里这是带停机的 v2 迁移,在 Serverless 里则走零停机(ZDT)路径,依赖 forwardCompatibility 让新旧版本短暂共存。migrations 是旧式写法,当前新类型应优先使用 modelVersions。
已知的核心类型包括:index-pattern(历史 type 名;UI 现在叫 Data View)、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(如果做滚动升级)。新索引就绪后,别名切换是单次原子操作。
当前官方文档分别描述 classic stack 的 v2 迁移流程和 Serverless 使用的 zero-downtime 迁移算法。两者的触发条件与运维边界不同,不能把 Serverless 的 ZDT 行为直接套到所有自管理版本上。
实验:通过 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 |
| modelVersions / 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 的访问能力。在传统 v2 升级里,迁移完成后旧别名 .kibana 切换到新索引,旧版本 Kibana 如果仍在运行会看到新格式的文档,通常会因为模型版本高于自身已知版本而拒绝处理,不会静默损坏数据。ZDT 路径则要求类型的 forwardCompatibility 能把新字段收敛回旧版本可理解的形状。
练习
在 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 是否发生变化。
