2212 字
约 7 分钟
20
S13-A19 · 索引别名与零停机重建:为什么不能直接改索引

一次 mapping 变更(把一个字段从 text 改成 keyword)直接改了索引,结果旧数据里该字段的历史值与新写入的格式不一致,查询在这个字段上同时出现"能搜到"和"搜不到"两种结果,持续了三个月才通过全量重建修好。

关系库改表结构有 DDL,搜索索引没有"就地改"—mapping 中的字段类型一旦有数据写入就不可变。所以变更的唯一正确路径是:建新索引、写两处、切别名、删旧索引。


1. 别名是这套流程的枢纽

对象 作用
物理索引(orders_v1) 真正存数据、带 mapping
别名(orders) 指向物理索引,应用只认别名
写入别名 只能指向一个索引
读别名 可指向多个索引

别名还能承担"按时间切分"的职责:logs-2026.11 这样的物理索引按天/月创建,logs 别名指向当前可写的那个,查询别名指向最近 N 个。这类"滚动索引"把"重建"变成了"新建一个",成本从数小时降到分钟级。

关键规则:应用永远只使用别名。一旦应用里写死了物理索引名,"零停机重建"就做不到了——因为切换必须改代码。

2. 零停机重建的五步

步骤 操作 风险
1 建新索引 用新 mapping 建 orders_v2 无
2 双写 同时写 orders_v1 与 orders_v2 一致性处理
3 回填 把旧数据从源库或 v1 重新写入 v2 耗时长、影响集群
4 切读别名 把 orders 指向 v2 需验证
5 清理 停写 v1,确认无引用后删除 需观察期

回填期间还有一个隐患:双写与回填会竞争同一批文档。同一文档先被回填写入旧版本、再被双写写入新版本,最终值取决于顺序。可靠做法是让回填只写"双写开始之前的历史数据",时间边界写进任务参数。

第三步是最耗时的:回填速度受集群写入能力限制,全量重灌一个亿级索引通常要数小时到一天。这期间双写仍在进行,所以回填必须能处理"同一文档被写两次"(按 ID upsert,天然幂等)。

// 用别名切换:原子操作,无需停写
curl -XPOST "localhost:9200/_aliases" -H 'Content-Type: application/json' -d '{
  "actions": [
    { "remove": { "index": "orders_v1", "alias": "orders" } },
    { "add":    { "index": "orders_v2", "alias": "orders" } }
  ]
}'
// 两个动作在同一个请求里,是原子的:不会出现别名短暂不存在的窗口

3. 回填的三种方式

方式 速度 影响
从源库读再写 中 需要源库支持批量读
从旧索引读再写(_reindex) 快 占用集群 IO 与 CPU
快照恢复后重建 最快 需要快照与新版 mapping 兼容

限速不只是"给个速率上限",还要能观察副作用:

// 回填期间持续观察集群压力,而不是等它变红
watch -n 30 'curl -s "localhost:9200/_cluster/health?pretty" | grep -E "status|unassigned"'
curl -s "localhost:9200/_nodes/stats/thread_pool?pretty" | grep -E "write|search" -A3
// write 队列持续非零 → 回填压过了写入能力,应降速
// unassigned 分片出现 → 集群不稳,应暂停回填

第二种最省事,代价是它同时读写的都是同一个集群,回填期间集群负载会明显上升。所以要在低峰做,并限速。

// 限速 reindex:把对集群的影响压在可控范围
curl -XPOST "localhost:9200/_reindex?wait_for_completion=false" -d '{
  "source": { "index": "orders_v1", "size": 1000 },
  "dest":   { "index": "orders_v2", "op_type": "index" },
  "conflicts": "proceed"
}'
// 用 _tasks 观察进度;必要时 POST /_tasks/<id>/_cancel 中止

4. 双写的一致性怎么处理

策略 一致性 复杂度
应用层双写(同步) 强 写入延迟上升
应用层双写(异步) 最终 需处理顺序
变更数据捕获(CDC) 最终 需管道
只回填不双写 有窗口 窗口内新数据只在一侧

第四种(只回填不双写)在低频更新的数据上可行,前提是能明确"回填时刻"这个边界:边界之后的新数据只在一侧,需要在切换前补一轮增量。这类补写常常被遗漏,症状是"切换后最近一小时的数据搜不到"。

第三种是大型系统的主流做法:业务只写数据库,由 CDC 同步到索引。这样"索引重建"变成"换一个消费者",而不是改业务代码。

// 三种策略的取舍
同步双写:实现简单,写延迟翻倍,失败要处理"一边成功一边失败"
异步双写:写延迟不变,需保证顺序(按文档 ID 分区)
CDC    :业务无感,需维护同步管道与位点管理

5. 切换前的验证清单

检查 目的
文档数量对齐 回填完整性
抽样比对若干文档 字段是否正确映射
关键查询结果对比 排序与过滤是否等价
别名指向确认 避免切错
回滚方案可用 切回 v1

验证还可以做得更具体:挑十条已知"应该排在前面"的查询,看新索引的结果是否仍然合理。这是业务视角的验证,比随机抽样更接近真实。

第三项最容易被跳过:mapping 改了,查询的相关性排序可能整体改变。同一条查询在 v2 上返回的顺序不同是正常的,但必须确认"这个变化是可接受的",而不是上线后才发现。

// 切前对比:同一查询在新旧索引上的结果差异
diff <(curl -s "localhost:9200/orders_v1/_search?q=keyword:foo&size=20" | jq -r '.hits.hits[]._id') \
     <(curl -s "localhost:9200/orders_v2/_search?q=keyword:foo&size=20" | jq -r '.hits.hits[]._id')
// 差异应当能被解释(如新增了排序字段),无法解释的差异意味着 mapping 或分析器有问题

6. 排错:五种"重建后不对"

现象 成因 处理方向
部分文档搜不到 回填未完成就切了 对齐文档数后再切
中文搜不到 新索引未配置分析器 检查 analyzer 映射
排序全变了 字段类型从 text 改 keyword 预期内,需业务确认
写入报错 写别名指向了两个索引 写入别名只能一个
磁盘暴涨 新旧索引并存 按计划删旧索引

第四项是别名使用的硬规则:读别名可以指多个索引,写别名只能指一个。把两者弄混,写入会直接失败。

7. 边界:什么情况可以简单改

场景 为什么 做法
索引为空(无数据) 无不可变约束 直接删了重建
只加字段(新增属性) mapping 允许新增字段 直接改,不用重建
开发环境的临时索引 无数据价值 重建即可
已有快照恢复流程 重建成本低 用快照 + 新 mapping

还有一条与团队协作有关的边界:别名切换应当是一次变更,而不是一次操作。它有明确的前置检查、执行人、观察窗口与回滚动作;把它当成"执行一条命令",最容易在切错别名时无人发现。

判断口径一句话:这次变更改了已有字段的类型吗。没改(只新增字段),通常可以就地更新;改了,就必须走重建流程。


8. 自测

  1. 为什么 mapping 里的字段类型不能就地改?这与关系库的 DDL 差在哪?
  2. 别名在这套流程里承担什么职责?为什么应用里不能写死物理索引名?
  3. 零停机重建的五步中,哪一步最耗时?它为什么必须幂等?
  4. 三种回填方式各自的代价是什么?_reindex 为什么要限速?
  5. 切读别名为什么必须与移除旧别名放在同一个请求里?
  6. "只新增字段"与"改字段类型"分别对应什么做法?判断依据是什么?
S13-A19 · 索引别名与零停机重建:为什么不能直接改索引
https://www.clxhxhhr.top/posts/6421/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。