2. Elasticsearch数据管道与发布单元

在 Elasticsearch 的持续交付中,真正需要被“发布”的并不是某个索引里的文档,而是一组彼此依赖的元数据对象:索引、映射、别名、索引模板和 ILM 策略。它们共同构成一个可版本化的发布单元。理解这些对象之间的依赖方向,是设计发布流水线的前提。

先看依赖关系。ILM 策略是生命周期规则的载体,它本身不绑定具体索引,而是被索引模板引用;索引模板在索引创建时被匹配并应用,决定新索引的 settings、mappings 以及要挂载的 ILM 策略和别名;别名则指向一个或多个具体索引,是应用读写流量的入口。因此依赖方向是:ILM 策略 → 索引模板 → 索引 → 别名。发布时必须按这个顺序推进,回滚时则反向操作。若先删 ILM 策略再删模板,模板会因引用不存在的策略而报错;若先删别名再删索引,流量入口会提前中断。

发布单元对象依赖与版本化建模 ILM 策略 logs-policy-v3 hot→warm→delete 索引模板 logs-app-template-v5 settings+mappings+alias 索引 logs-app-2025.06.01 物理存储单元 别名 logs-app-write logs-app-read 引用 匹配创建 指向 版本化建模要点 • 每个对象带版本号,如 logs-policy-v3 • 模板通过 priority 控制匹配优先级 • 别名用 is_write_index 指定唯一写索引 • 发布顺序:策略→模板→索引→别名 • 回滚顺序:别名→索引→模板→策略

把上述对象纳入版本控制时,推荐采用“声明式定义 + 版本号后缀”的方式。每个对象在 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.json1
templates/索引模板logs-app-template-v5.json2
indices/初始索引logs-app-2025.06.013
aliases/别名绑定logs-app-write/read4

发布流水线在执行时,先读取 ilm/ 下所有策略并调用 PUT /_ilm/policy/<name>,再读取 templates/ 并调用 PUT /_index_template/<name>,然后检查目标索引是否存在,不存在则按模板创建,最后执行别名绑定。每一步都应记录返回的 acknowledged 字段,只有为 true 才进入下一步。若某一步失败,流水线应停止并输出当前已发布对象的版本快照,便于人工判断是回滚还是修复后重试。

版本号建议与 Git 提交或发布标签关联,例如 logs-policy-v3 对应 tag release-2025.06.01。这样在集群中看到某个策略版本时,能直接定位到对应的代码提交,缩短排障路径。

最后需要强调一个常见误区:映射变更不是“改一下 JSON 再 PUT”那么简单。Elasticsearch 不允许直接修改已有字段的类型,例如把 keyword 改成 text 会直接报错。因此映射的版本化必须配合索引重建或 reindex 流程。发布单元中应包含一个“映射兼容性检查”步骤,在应用新模板前,用 GET /<index>/_mapping 拉取现有映射,与目标映射逐字段比对,识别出类型冲突、字段删除等破坏性变更,并阻止流水线继续。只有确认兼容或已规划好 reindex 路径后,才允许发布新版本模板。

综上,索引、映射、别名、模板和 ILM 策略不是孤立配置,而是一个有明确依赖方向、有版本边界、有发布顺序的发布单元。把它们统一建模、统一版本化、统一走流水线,才能让 Elasticsearch 的数据管道具备可重复、可回滚、可审计的持续交付能力。