1. 为什么需要 reindex五个让我踩过坑的典型场景先给没接触过的朋友一个基本认知reindex 不是某个数据库独享的功能主流存储引擎基本都有类似的能力。我最早接触是在 Elasticsearch 上后面在消息队列、关系型数据库分库分表扩容时也遇到过同种思路的操作。说白了reindex 的核心就一句话把数据从一个索引空间完整复制到另一个索引空间过程中允许你变更结构、清洗字段、甚至跨集群搬迁。你可能会问好好的数据在那放着为什么非要搬一次这就要说到我实际遇到过的问题了。之前帮某团队维护一套日志检索系统当时创建的索引分片数是 3结果数据涨到一定量级后单个分片撑到了接近 40GB查询响应从几十毫秒退化到秒级。想改分片数原生 ES 不支持直接修改唯一的正经路子就是 reindex 到新索引。类似的情况还有一开始 mapping 里某个字段类型定错了比如把时间字段存成了 text后续要做 range 查询才发现聚合结果不对再比如索引里有大量已过期数据想清理掉重新整理一个压缩版本。这些情况本质都是同一个答案——用 reindex 重建一份数据。还有一个高频场景是集群迁移。之前我们做过一次机房整体搬迁业务要求停机时间控制在 20 分钟以内数据量大概 3TB。用 snapshot 恢复固然可行但快照恢复需要先挂存储、再等分片初始化整个过程偏重而且对版本敏感。后来我们是直接在新集群上把 mapping 和 setting 建好然后用 reindex 从旧集群拉数据做到双写切换干净利落。所以每次有人问我 reindex 到底有什么用我的回答就一句它是你给数据换房子时的搬家货车车斗里还能顺手帮你扔点垃圾、重新摆摆家具。哪些人需要重点看这篇文章如果你是系统运维、后端开发或者正在负责某个数据平台的日常维护这篇文章可以直接作为操作手册来用。里面提到的参数调优、冲突处理、性能瓶颈排查很多是我在客户现场和线上事故里真金白银换来的经验常规文档里不会写这么细。2. reindex 的核心机制与参数选择逻辑2.1 从 source 到 destreindex 的底层工作方式要真正用好 reindex还是得稍微理解一下它的底层逻辑。它本质上是一个分布式数据复制任务不是在一台机器上把数据读出来再塞回去而是由协调节点发起把任务拆分成多个并行的子任务分发到不同的分片上执行。每个子任务做的事情很简单从 source 索引按游标滚动读取数据然后并行写入 dest 索引。这就引出了理解 reindex 的第一个关键点目标索引不是必须预先存在。如果你没指定 dest 的索引名reindex 会自动按 source 的 mapping 创建一份一模一样的索引。听起来很方便但我要提醒一句我强烈建议你先手动创建 dest 索引原因后面在 mapping 与分词一节细说。reindex 在 ES 里的实现是基于scroll接口的它会维护一个上下文快照。这个机制决定了 reindex 运行时源索引里的新增数据不会被搬过去除非你额外配置了增量区间。所以如果你是想做实时双写迁移光靠 reindex 是不够的还需要配合 binlog 回放或者双写策略。另外一个容易忽略的点是版本控制。reindex 时如果 dest 索引已经存在且文档 ID 与 source 相同那么默认情况下它会覆盖同 ID 的文档。这是符合预期的因为 reindex 默认采用_version外部版本控制也就是说它不会像普通索引一样用内部自增版本号而是直接使用源文档的版本号。好处是你可以反复执行 reindex 而不用担心版本冲突坏处是——如果你在迁移过程中源索引和目标索引同时被业务写入且写入了不同的内容到同一个 ID数据会互相覆盖产生逻辑错误。实操建议是在迁移窗口内暂停对源索引的写操作或者使用别名切换的方式让业务先写别名的临时索引切完之后再换回来。2.2 关键参数详解conflicts、size、slices以及它们背后的坑reindex 有一个让人爱恨交加的指令级操作方式那就是你可以在_reindexbody 里写各种处理逻辑。先把最常用的几个参数列一下参数作用我的默认建议conflicts遇到版本冲突怎么办设成proceed避免任务被中断size每批次滚动读取的文档数看分片大小默认 1000 太保守slices并行分片数设为源索引分片数或手动指定 4~8timeout单批次写入超时默认 1 分钟大文档场景要调大refresh迁移完成后是否刷新建议设为false完成后手动 refreshquery只迁移符合条件的子集支持范围查询清理过期数据很好用script迁移时执行脚本转换字段常用于字段合并、类型转换、脱敏conflicts这个参数值得展开说说。默认情况下如果 reindex 过程中遇到版本冲突比如你手动往 dest 写了一条同 ID 文档版本号比源还新整个任务会直接失败回滚。可如果是线上大索引迁移一次任务跑半个小时因为一条脏数据失败那是很崩溃的。加上conflicts: proceed后任务会跳过冲突文档并继续执行结束后报告冲突数量。很多场景是你并不关心个别文档的冲突先保证大部分数据迁移过去再说。size参数则是很多人优化性能第一个想到的。实际上它控制的是 scroll 的 batch size不是总迁移量。ES 默认一次 scroll 拿 1000 条如果你单条文档比较大比如 10KB 以上1000 条就意味着每次要传输 10MB 数据网络和内存都吃紧。我一般会调到 3000~5000但也不要无脑调大因为协调节点需要维护 scroll 上下文太大会导致堆内存暴涨。这里有一个常见的衡量方式每个批次的数据量控制在 5~15MB 之间比较合适你可以用文档平均大小 × batch size粗算一下。script是 reindex 里功能最灵活也最危险的东西。它允许你在复制过程中对字段做处理比如把两个字段拼接成一个新字段或者把嵌套对象打平。我在一次日志脱敏迁移中就用它替换了用户手机号中间四位。但注意script 会显著增加 CPU 开销如果数据量在亿级以上建议先用小样本验证脚本逻辑再全量跑否则等你跑到 80% 才发现脚本有 bug返工代价是很高的。3. 实操过程全记录从构建目标索引到切流完成3.1 第一步目标索引的 mapping 与 setting 设计我先强调一遍不要让 reindex 自动创建目标索引手动建。原因有三第一自动创建的索引分片数、副本数、刷新间隔全都沿用源索引但你在迁移时往往就是想改这些第二自动创建的 mapping 会把动态字段推断一次可能会给后续不需要的字段也建了倒排索引浪费存储第三如果源索引里某些字段是text同时又有keyword子字段在处理时你可能想只保留 keyword 子字段自动创建无法做到这种灵活取舍。手动创建目标索引用例大概是这样的写法PUT /dest_index { settings: { number_of_shards: 10, number_of_replicas: 0, refresh_interval: -1 }, mappings: { properties: { timestamp: { type: date }, user_id: { type: keyword }, message: { type: text, fields: { keyword: { type: keyword, ignore_above: 256 } } } } } }这里有两个刻意操作number_of_replicas设为 0等到数据迁移完再调成 1否则每次写入都要复制副本速度至少慢一半甚至更多refresh_interval设为-1意思是迁移期间不主动刷新这样 Lucene 不会频繁产生 segment写入吞吐会显著提高。这两招是我做大数据量迁移时的标配动作等数据搬完了再恢复。3.2 第二步增量数据同步与业务停写判断很多人在迁移时会忽略一个问题reindex 执行期间源索引不是静止的业务可能还在写入。如果你做的是跨集群整体迁移最安全的做法是让业务停写或者切到只读模式。但对于大型业务系统停写窗口很难申请到这时就要采用增量同步或双写方案。一个比较实用的策略是先做一次全量 reindex任务结束后记录源索引的当前时间点或序列号然后在下一次同步时用 query 加上 range 条件只把新增的时间段数据再 reindex 过去。ES reindex 支持带上游标时间POST /_reindex { source: { index: src_index, query: { range: { timestamp: { gte: 2025-01-01T00:00:00 } } } }, dest: { index: dest_index } }这个模式适合分钟级延迟的场景。如果业务要求秒级双写简单的 reindex 就不够了需要在应用层做双写reindex 只负责历史数据兜底。这是我遇到过很多次的情况——团队以为一个 reindex 就能搞定无缝迁移结果数据还是缺了一段。建议事前做充分的业务写入评估。另外还有一个细节如果你要迁移的数据已经做了生命周期管理例如索引按天拆分然后做了冷热分层存储reindex 时要注意热索引和冷索引的存储路径可能不同。旧集群冷节点上的索引字段可能被 force merge 过源数据是只读的。reindex 本身没问题但如果你的目标集群也分层就要先确认目标索引的routing和allocation配置是否正确。3.3 第三步执行 reindex 并监控进度执行 reindex 的完整示例带脚本转换的版本POST /_reindex?wait_for_completionfalse { source: { index: src_index, size: 3000 }, dest: { index: dest_index, version_type: external }, script: { source: if (ctx._source.timestamp ! null) { ctx._source.date_str ctx._source.timestamp; }, lang: painless }, conflicts: proceed }注意?wait_for_completionfalse这个参数很多人会忽略。如果不加这个参数请求会一直挂在那里直到任务跑完一旦数据量大请求很容易超时客户端会以为失败了其实后台任务还在跑后续就很尴尬。加了之后ES 会返回一个 task id你可以用这个接口查询进度GET /_tasks/task_id?detailed返回结果里有total、updated、created、failed等计数指标。我一般会写一个小脚本每 30 秒轮询一次记录进度并估算剩余耗时心里有个底也方便和领导汇报进度。如果是超大索引比如几千亿条用我一个人在终端前盯进度肯定不现实。此时我建议用任务管理能力更强的特性按 URL 编码后的子任务进度或者接入监控面板展示迁移速率。另外如果中途出错了可以用POST /_tasks/task_id/_cancel随时取消任务已经写入的数据不会被回滚后续重新执行 reindex 会自动跳过已存在且版本号一致的文档这个幂等性设计是 reindex 做得比较好的地方。3.4 第四步迁移后数据校验与别名切换迁移完成后第一件事不是切换业务而是做数据校验。我的固定动作用三个文档总数比对源索引和目标索引执行_count对比数字是否一致。注意如果有 conflict 跳过目标数会少一点这个要心里有数。抽样比字段随机挑若干 ID对比源和目标文档的_source内容是否一致特别是脚本转换过的字段要仔细看。查询性能验证在目标索引上跑几个业务核心查询看响应耗时是否符合预期同时观察集群是否有 reject 或熔断。校验通过之后就到了切换环节。这里有一个推荐做法用 alias 别名来切换而不是让业务直接改写索引名。比如线上业务一直访问log_index这个别名它原本指向旧索引迁移完成后你只需要先让业务停写然后原子地把别名指向新索引并移除旧索引关联。这样业务代码一行都不用改回滚也方便——直接把别名指回去就行。POST /_aliases { actions: [ { remove: { index: src_index, alias: log_index } }, { add: { index: dest_index, alias: log_index } } ] }提醒一下如果数据量大、校验耗时长整个切换窗口要提前申请。个人经验是全量迁移 1 小时校验加切换至少再留 30 分钟宁可多留buffer不要卡线切换。4. 性能调优从半小时到三分钟的实战优化过程4.1 批量大小与 slice 并行度的平衡有一次我帮某团队迁移一个 100GB 的索引一开始直接跑默认参数预计要 35 分钟。这个结果不能说不能用但他们的停机窗口只有 15 分钟超时就影响业务恢复。我当时做了三件事最终把时间压到了 3 分钟出头。第一步是调size。默认 1000 条一批对于单文档 3KB 的日志数据批量数据量只有 3MB网络包都利用不满。改成 5000 后单批 15MB整体吞吐直接翻了 1.5 倍。第二步是开 slices。没有指定 slices 时reindex 只有 1 个并行任务相当于一辆货车在搬货。我把slices设置为源索引分片数 8等于同时来了 8 辆货车时间一下就下来了。设置为自动模式也很有趣slices: auto会让 ES 根据源索引的分片数以及每个分片的大小自动估算但实测下来自动值通常取的是源索引分片总数有时候会偏大或者偏小。比如源索引有 30 个分片但其中大量小分片数据量很少自动切成 30 个 slice 反而会造成调度压力。我的经验是直接手动指定优先等于源分片数如果节点数少则约为节点数的 2~3 倍。4.2 写入侧优化副本与 refresh 的设置技巧前面提到我在构建目标索引时就把副本设成 0、refresh 设成 -1。这是写入优化里最关键的两板斧我把它们单独拎出来强调。索引写入时每个分片上的主分片完成索引后还要等到副本分片写入确认才算完成。如果副本数设为 1那么主分片和副本分片都要执行同样的写盘任务写入耗时接近翻倍。在 reindex 期间把副本设 0是牺牲容错换性能完全值得但要注意此时集群发生节点故障数据会有丢的风险。所以建议在 reindex 期间尽量保证集群健康不要同时做节点替换或版本升级。refresh 的影响则更隐蔽。ES 默认每 1 秒刷新一次每次刷新都会形成一个新的 segment 文件刷新频率越高 segment 文件越多reindex 过程就会频繁触发 merge大量消耗磁盘 IO。我把 refresh 设为 -1 后数据只进 lucene 内存不落盘形成独立 segment最终统一 force merge效果立竿见影。有次全量迁移 40GB 数据只这两项改动就省下了 40% 的时间。但这项优化有个代价如果任务中途崩溃未刷新的数据会丢失需要重跑。而且 reindex 完成后一定要记得把 refresh_interval 改回去不然目标索引的查询延迟会很高。4.3 资源瓶颈识别是网络、CPU 还是磁盘做性能优化不能只盯着参数瞎猜要学会看指标。reindex 本质是读写双密集任务瓶颈通常出现在三个地方网络带宽跨集群 reindex 时尤其明显。可以看源集群和目标集群的网卡流量如果打满说明网络是瓶颈此时调大 size 和 slices 均无效只能压缩数据或者换时间窗口。CPU脚本转换、分词处理都会吃 CPU。如果节点 CPU 持续在 90% 以上同时任务速度上不去多半是 CPU 卡住了。这时要检查是不是有复杂的 script或者源分片在节点上分布不均导致个别节点过热。磁盘 IO写入侧磁盘压力大常见于机械硬盘环境如果iowait很高说明磁盘顶不住了。此时再增加 slices 只会加重磁盘负担还会导致任务失败重试。解决思路是降低 refresh 和副本、升级存储或者将目标索引放到冷数据盘上。我见过不少团队在 reindex 时报错 timeout 或者 rejected execution第一反应就是加大 slices结果越加大越慢。那多半是因为集群写入线程池已经被打满HTTP 请求返回 429。正确的做法是先看_cat/thread_pool和_nodes/stats确认瓶颈在哪个环节再做针对性调整。5. 高频问题速查与排查经验总结5.1 reindex 版本冲突怎么处理版本冲突是最常见的报错之一。我在某个新搭建的集群上执行 reindex 时一上来就报version_conflict_engine_exception。排查下来发现是因为之前做过一次失败的迁移目标索引里已经有一批旧数据版本号比源数据略低再次执行覆盖时就会触发冲突。应对方法有两个方向一是加conflicts: proceed让任务跳过冲突文档继续跑适合大部分场景二是如果你确定要以源索引为准可以用version_type: internal强制覆盖目标版本让源索引数据直接覆盖同 ID 文档。我一般优先用第一个方便事后排查数量差异。注意如果你在脚本里手动组装了文档 ID并且没有用_id字段覆盖也可能出现同一个 ID 多条数据轮番写入导致冲突持续增多这种场景需要先修业务逻辑。5.2 reindex 执行超时或者中断怎么办reindex 因为数据量大经常会被反向代理或者前端的 HTTP 连接超时打断。很多人一看请求超时就以为迁移失败了急着重新跑结果每次都是从头跑起浪费大量时间。我的建议是永远用 task 方式提交 reindex不要用同步方式等待。执行后即使 HTTP 请求断了后台任务依然在执行。通过 task id 查询进度即可。如果真的中断了reindex 具备断点续跑能力吗严格说没有但因为它按文档 ID 覆盖且使用外部版本控制重跑时会跳过那些版本号一致的文档。所以重跑不会重复劳动太多已经迁移过的文档会快速跳过。5.3 迁移后查询性能反而下降的排查思路有个案例很典型某团队把一个索引从 3 分片迁移到 10 分片后发现指定的聚合查询反而比以前慢了。排查下来发现目标索引的 mapping 里多了很多字段比如某个字段在源索引是keyword但自动 mapping 推断成了text导致聚合时需要对大量数据做分词。另外源索引曾经做过多轮 force mergesegment 数量很少而目标索引没有做 merge段文件很多影响了查询性能。解决方案也很简单迁移后执行一次force merge把 segment 数量压缩下来同时检查 mapping 是否符合预期不需要为查询服务的字段不建索引。这些操作应该在切换业务前完成否则用户感知到慢查询就会直接影响印象。5.4 reindex 执行中源索引写入怎么办如果你的 reindex 是在生产环境在线执行的源索引还在不断写入那默认的 reindex 会有明显的数据一致性问题。它基于 scroll 快照读取下单时创建的快照只含当下数据之后新增的文档不会被搬迁。针对这个场景你需要做增量迁移全量 reindex 完成后记录当时的系统时间或业务侧自增 ID。新增一个增量 reindexquery 条件为timestamp 上次迁移位置。反复执行增量 reindex直到新旧索引数据差小于业务容忍范围。最终在停写窗口内做最后一次增量同步然后切换别名。这个方案我用过很多次在日志系统和订单系统迁移中都很稳。注意增量 reindex 执行时如果目标索引已有相同 ID 的文档且版本号比源新默认情况下会跳过所以重复执行是安全的。6. 更进一步的扩展操作跨集群迁移与数据清洗6.1 远程集群数据迁移的认证与防火墙配置跨集群 reindex 的场景越来越多但实操里有很多网络层面的坑。ES 跨集群迁移支持在 source 里指定 remote 集群信息需要在elasticsearch.yml里配置reindex.remote.whitelist否则会报错connect transport exception。POST /_reindex { source: { remote: { host: http://old-cluster:9200, username: migration_user, password: migration_pass }, index: src_index }, dest: { index: dest_index } }安全方面需要特别注意远程 reindex 是在目标集群上发起请求读取远端数据所以远端集群要允许目标集群的 IP 访问。如果网络隔离严格走的是 HTTPS 并且有自签名证书那你还要在目标集群配置证书信任否则会报 SSL 错误。我建议在生产环境做跨集群迁移前先同网段拉一条测试数据跑通全流程避免迁移当天被网络策略卡住。6.2 迁移过程中做数据脱敏与类型转换reindex 最迷人的地方在于它给了你一个近乎无损的数据管道。除了基本的字段复制你几乎可以在搬家的同时完成所有清洗操作。我之前帮某平台把用户表从 MySQL 分库中导成 ES 索引时就用 script 把手机号、身份证等敏感字段做了脱敏处理只保留格式不保留真实值。script: { source: if (ctx._source.mobile ! null) { ctx._source.mobile ctx._source.mobile.replaceAll((\\d{3})\\d{4}(\\d{4}), $1****$2); }, lang: painless }这种操作给业务带来的好处很大迁移和脱敏一步到位省掉了后续单独跑数据清洗任务的人力。但要注意执行效率问题脚本涉及正则时 CPU 消耗会明显上升建议先在 100 万条子集上试跑估算整体耗时再决定是采用脚本处理还是全量迁移后在目标集群上执行 update_by_query。6.3 从其他存储迁移数据超越传统 reindex 的边界最后提一个我近期在给团队培训时反复强调的认知reindex 的思想不限于 Elasticsearch 内部。比如你想把 MySQL 的一张表同步到 ES 用于全文检索很多人会专门写一套同步程序其实思路完全可以复用。你可以在 MySQL 端定时查询增量数据通过 ES 的 bulk API 写入目标索引在目标索引结构变更时跑一次全量重建。这本质上也是 reindex——把 A 存储的数据搬到 B 存储并带上你设计的结构变更。从这个角度看理解 reindex 不只是会敲一个 API更重要的是理解它帮你抽象出的三个核心环节读数据、写数据、改结构。只要把这三个环节的机制想清楚你再遇到任何数据搬家任务都不会慌。我后来做分库分表扩容时把旧的 16 个表合并成 48 个表也是用的同一种思维模式先把结构定义好再分批搬运最后校验切流整个流程和 reindex 如出一辙。最后说点实际的reindex 这个操作表面上看就是一行 API但真正决定成败的往往是你对目标索引的设计、对参数的理解、对业务写入窗口的判断。我在多次迁移项目中攒下一条最重要的体会永远不要把 reindex 当成一次性脚本要把它当成一个分阶段、可验证、可回滚的工程流程。先设计目标架构再小样验证再全量执行最后校验切流每一步都有明确的状态和责任人。还有一个私藏小技巧送给你在 reindex 前先跑一次小数据量演练记录从建索引到校验完成每个环节的耗时然后按数据量线性放大估算这样你在向团队申请停机窗口时给出的时间预估会相当靠谱也能避免因为预估不足被反复打回审批。如果你正在准备一次数据迁移希望这篇内容能帮你少走一些弯路。迁移过程遇到具体报错时别急着搜答案先看任务日志和集群监控指标多数问题的线索就藏在里面。