2. Elasticsearch数据管道与发布单元
在 Elasticsearch 的持续交付中,真正需要被“发布”的并不是某个索引里的文档,而是一组彼此依赖的元数据对象:索引、映射、别名、索引模板和 ILM 策略。它们共同构成一个可版本化的发布单元。理解这些对象之间的依赖方向,是设计发布流水线的前提。
先看依赖关系。ILM 策略是生命周期规则的载体,它本身不绑定具体索引,而是被索引模板引用;索引模板在索引创建时被匹配并应用,决定新索引的 settings、mappings 以及要挂载的 ILM 策略和别名;别名则指向一个或多个具体索引,是应用读写流量的入口。因此依赖方向是:ILM 策略 → 索引模板 → 索引 → 别名。发布时必须按这个顺序推进,回滚时则反向操作。若先删 ILM 策略再删模板,模板会因引用不存在的策略而报错;若先删别名再删索引,流量入口会提前中断。
把上述对象纳入版本控制时,推荐采用“声明式定义 + 版本号后缀”的方式。每个对象在 Git 仓库中对应一个 JSON 文件,文件名包含语义化版本,例如 ilm/logs-policy-v3.json、templates/logs-app-template-v5.json。版本号不是装饰,而是发布流水线判断“是否需要变更”的依据:流水线对比目标集群中已存在的对象版本与仓库中的版本,只有版本不同才执行创建或更新。
下面给出一个可直接参考的索引模板示例,它同时引用了 ILM 策略并声明了别名:
{
"index_patterns": ["logs-app-*"],
"priority": 200,
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"lifecycle.name": "logs-policy-v3",
"lifecycle.rollover_alias": "logs-app-write"
},
"mappings": {
"dynamic": "strict",
"properties": {
"@timestamp": { "type": "date" },
"service": { "type": "keyword" },
"level": { "type": "keyword" },
"message": { "type": "text" },
"trace_id": { "type": "keyword" }
}
},
"aliases": {
"logs-app-read": {}
}
}
}
这个模板中有几个关键参数值得说明。priority 设为 200,表示当多个模板同时匹配 logs-app-* 时,优先级高的模板覆盖低优先级模板的同名配置,避免模板冲突导致映射被意外覆盖。dynamic: strict 表示拒绝写入未在 mappings 中定义的字段,这在持续交付中非常重要:它让映射变更必须显式经过发布流程,而不是被一条意外字段悄悄改变。lifecycle.rollover_alias 指定滚动别名,配合 ILM 的 rollover 动作,当索引达到 max_primary_shard_size 或 max_age 时自动创建新索引并切换写别名。
ILM 策略本身也需要版本化。一个典型的日志策略包含 hot、warm、delete 三个阶段:
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_primary_shard_size": "50gb",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"forcemerge": { "max_num_segments": 1 },
"shrink": { "number_of_shards": 1 }
}
},
"delete": {
"min_age": "30d",
"actions": { "delete": {} }
}
}
}
}
注意 min_age 是相对于索引创建时间计算的,不是相对于上一阶段完成时间。这一点在排查“索引为什么没有按时进入 warm”时经常被忽略。发布新版本策略时,Elasticsearch 不会自动把已有索引迁移到新策略,需要显式调用 POST /<index>/_lifecycle/explain 确认当前索引实际生效的策略版本,必要时用 _lifecycle API 重新挂载。
别名是发布单元中最容易被低估的对象。它承担两个职责:读入口和写入口。写别名必须唯一指向一个索引,通过 is_write_index: true 标记;读别名可以指向多个索引,用于跨索引查询。在滚动发布场景中,写别名的切换是原子操作:
POST /_aliases
{
"actions": [
{ "remove": { "index": "logs-app-2025.06.01", "alias": "logs-app-write" } },
{ "add": { "index": "logs-app-2025.06.02", "alias": "logs-app-write", "is_write_index": true } }
]
}
这条请求在同一个 _aliases 调用中完成移除和添加,保证切换过程中写别名始终有且仅有一个指向,不会出现写入丢失或双写。发布流水线应把别名切换作为独立步骤,并在切换后验证 GET /_alias/logs-app-write 返回的索引与预期一致。
把上述对象组织成发布单元时,建议在仓库中采用如下目录结构,使版本、依赖和发布顺序一目了然:
| 目录 | 对象 | 版本示例 | 发布顺序 |
|---|---|---|---|
| ilm/ | ILM 策略 | logs-policy-v3.json | 1 |
| templates/ | 索引模板 | logs-app-template-v5.json | 2 |
| indices/ | 初始索引 | logs-app-2025.06.01 | 3 |
| aliases/ | 别名绑定 | logs-app-write/read | 4 |
发布流水线在执行时,先读取 ilm/ 下所有策略并调用 PUT /_ilm/policy/<name>,再读取 templates/ 并调用 PUT /_index_template/<name>,然后检查目标索引是否存在,不存在则按模板创建,最后执行别名绑定。每一步都应记录返回的 acknowledged 字段,只有为 true 才进入下一步。若某一步失败,流水线应停止并输出当前已发布对象的版本快照,便于人工判断是回滚还是修复后重试。
logs-policy-v3 对应 tag release-2025.06.01。这样在集群中看到某个策略版本时,能直接定位到对应的代码提交,缩短排障路径。最后需要强调一个常见误区:映射变更不是“改一下 JSON 再 PUT”那么简单。Elasticsearch 不允许直接修改已有字段的类型,例如把 keyword 改成 text 会直接报错。因此映射的版本化必须配合索引重建或 reindex 流程。发布单元中应包含一个“映射兼容性检查”步骤,在应用新模板前,用 GET /<index>/_mapping 拉取现有映射,与目标映射逐字段比对,识别出类型冲突、字段删除等破坏性变更,并阻止流水线继续。只有确认兼容或已规划好 reindex 路径后,才允许发布新版本模板。
综上,索引、映射、别名、模板和 ILM 策略不是孤立配置,而是一个有明确依赖方向、有版本边界、有发布顺序的发布单元。把它们统一建模、统一版本化、统一走流水线,才能让 Elasticsearch 的数据管道具备可重复、可回滚、可审计的持续交付能力。