ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Elasticsearch Reindex完全指南:原理、实操与性能调优

Elasticsearch Reindex完全指南:原理、实操与性能调优 Elasticsearch用久了总会碰上一个绕不开的问题mapping建好了不能直接改但业务字段说变就变。比如我第一次做电商订单索引时图省事把客户IP字段存成了text类型后面要做精确聚合统计发现IP被分词器拆得七零八落查出来的数据对不上。这种场景下唯一靠谱的办法就是用Reindex把数据搬到一份新索引里。Reindex这个词直译过来就是“重建索引”核心作用是把你指定的源索引完整拷贝到另一份目标索引拷贝过程中可以顺带改字段类型、换分词器、调整分片数甚至跨集群搬运数据。这不只是运维侧的救命工具开发侧改数据结构、业务侧做数据归档也都离不开它。这篇文章会从原理、实操、调优到常见坑位把Elasticsearch Reindex完整讲透适合正在用ES做业务检索、数据存储的开发者、运维同学和半路接手ES的人。1. 什么时候需要重建索引触发场景和底层原因1.1 mapping不可变到底是怎么回事很多人第一次接触Reindex是去网上搜“ES怎么改字段类型”结果搜出来的答案都是“不能直接改只能重建索引”。为什么ES这么“死板”根源在底层存储结构上。Elasticsearch底层依赖Lucene每个字段写入的时候是以倒排索引、doc_values、store这三种形态落盘的。text字段会被分词器拆成一个个词项再构建倒排链keyword字段则作为一个完整词项存储long、integer这类数值字段又会使用不同编码方式做doc_values压缩。这些信息在mapping建立时就固化在Lucene的数据文件里了Lucene没有提供“原地改字段类型”的能力。好比一本书已经印刷装订完了你想把这个字改成那个字只能重新排版印刷不可能用橡皮擦在原书上改得不留痕迹。还有一点很容易被忽略即使目标索引还空着只要mapping已经同步到集群元数据字段类型就定了。新建空索引后立刻修改某些字段类型一样会报“Cant update a mapping for a field”之类的错误。所以别试图在已有索引上直接改踏踏实实走重建路线。1.2 真正需要reindex的常见场景结合我经手的项目和平时在技术群里看到的问题需要动用Reindex的场景基本可以归成这几类场景具体表现用Reindex解决什么修改字段类型原本用text存IP、状态码后面要做精确匹配或聚合新索引用keyword或integer重新映射修改分词器从standard切到ik_smart或调整自定义词典新索引绑定新分词器重新清洗一遍数据调整分片数初期评估不准分片过多/过少导致性能瓶颈新索引用更合理的分片规划拷贝数据过去调整副本数读写压力变化需要动态扩缩副本新索引配置合适的副本数据搬过去字段加工新增子字段、切出多余字段、删字段Reindex时用script或ingest pipeline处理迁移集群换机器、升降版本、机房调整远程源索引Reindex到目标集群索引瘦身大量删除字段或压缩冷数据降低存储占用新索引只保留核心字段修复脏数据历史写入的数据类型错乱、格式不统一清洗过程中按新mapping重新落地可以看到Reindex的定位不只是“改mapping”本质上是一套数据重放机制。你在新索引上把mapping、分词、字段规则全部调整好数据照样能按新规则流进去。2. Reindex原理与主流迁移方案怎么选2.1 Reindex API的后台运行机制_Reindex API写起来并不复杂例如POST _reindex { source: { index: old_index }, dest: { index: new_index } }但你发完这个请求之后ES内部做了一系列事情协调节点先拿到源索引的分片信息按scroll方式一批一批读取数据读出来之后在内存里组装成bulk请求再批量写到目标索引。整个过程对客户端来说几乎是无感的你不需要在本地写循环去scrollbulk人力和出错率都大幅降低。这里的关键点在于Reindex是服务端行为数据不经过你的应用服务器。如果数据量有几千万上亿在客户端写脚本循环读取客户端内存大概率先爆掉而Reindex模式是协调节点分页拉取、批量写入配合wait_for_completionfalse可以丢到后台异步跑就算你的电脑断网了任务也不会中断。版本冲突处理也很重要。Reindex在写入目标索引时如果目标索引已经有相同_id的文档默认情况下会直接覆盖。你可以在请求里指定version_typeinternal或op_typecreate来改变覆盖策略如果只是备份或迁移建议加上op_typecreate这样目标索引里已存在的文档不会被误覆盖遇到冲突也能用conflicts继续跳过去。2.2 迁移方案横向对比Reindex当然不是唯一的数据搬迁方式很多人也会用Logstash或者自己写脚本导数据。为了让你选型更清楚我把常用几种方案放一起对比过方案传输方式性能跨集群字段加工上手成本_reindex APIES内部scrollbulk高可slices并行支持remote支持script/pipeline低Logstash独立数据管道中高需要配置ES input/output强大中elasticdump命令行工具中低支持字段调整较弱低自写脚本scrollbulk看代码质量自己实现灵活高我的建议是纯ES到ES的同构迁移无脑用_reindex如果还要从MySQL、Kafka等其他数据源同步或者链路里要做复杂清洗Logstash更合适临时手动倒一点数据拿elasticdump也行但我不建议在生产环境用它处理大数据量一是不支持断点续传二是本身是Node.js单线程吞吐上不去。还要注意版本匹配。ES的Reindex支持跨大版本比如从6.x迁到7.x、7.x迁到8.x都能跑但源索引如果是老版本生成的可能会有索引只读blockindex.blocks.read_only_allow_deleteReindex前需要处理掉。另外ES 8.x以后默认开启了安全认证远程Reindex时必须在请求里带认证信息否则直接401。2.3 什么时候不要用ReindexReindex也不是万能的。如果业务对连续性要求极高比如源索引还在高频写入同时你又希望迁完数据后旧的索引能立即停用那Reindex期间的新写入数据就需要你用业务双写或者利用别名切换来解决。Reindex只负责存量数据的复制不保证增量实时同步。对于“存量增量”无缝迁移一般得先做一轮全量Reindex然后短暂停写或双写补增量再切换别名。这个流程在本地的测试环境多演练两次真正上生产才不会手忙脚乱。3. Reindex完整实操与性能调优3.1 环境准备与版本匹配实操之前先把环境准备好。自己电脑上做实验最简单的方式是直接跑ES单节点Windows环境直接进bin目录执行elasticsearch.batLinux和macOS执行elasticsearch脚本。ES对JDK版本有要求如果你用的是ES 7.17以下版本需要自己配置JDK 11ES 7.17及以上版本内置了JDK不用额外装这点对新手很友好。建议至少使用7.x以上版本做实验Reindex相关的slices、task API和指标都比较成熟。版本不要太老比如ES 5.x、6.x那批索引默认mapping还带_typesReindex时还要处理type映射操作复杂度会高不少。现在还在维护老项目的同学如果碰到带_types的索引迁移前先在目标索引的mapping里把_doc去掉避免数据写到一半发现目标索引结构对不上。3.2 一次完整重建索引的五个步骤我用一个实际案例演示完整的重建过程。假设我有一份商品订单索引order_v1里面有字段order_id字符串需要改成keywordamount当初误存成text需要改成doublecreate_timedate类型保持不变remarktext类型需要用ik分词器重新分词接下来就是标准的五个步骤。第一步查看旧索引mapping和setting搞清楚现状GET order_v1/_mapping GET order_v1/_settings拿到结果后确认要调整的字段名和类型同时看一眼旧索引的分片数和副本数新索引可以沿用也可以重新规划。第二步创建新索引提前定义好mappingPUT order_v2 { settings: { number_of_shards: 3, number_of_replicas: 1, analysis: { analyzer: { ik_analyzer: { type: custom, tokenizer: ik_smart } } } }, mappings: { properties: { order_id: { type: keyword }, amount: { type: double }, create_time: { type: date }, remark: { type: text, analyzer: ik_analyzer } } } }这一步尤其重要目标索引的mapping必须和源数据对得上。不要在目标索引留空让ES自动生成mapping否则默认会把order_id识别成text又会重蹈覆辙。第三步启动Reindex任务用异步方式跑POST _reindex?wait_for_completionfalse { source: { index: order_v1, size: 5000 }, dest: { index: order_v2, op_type: create } }请求返回会有一个taskId例如oTUltX4IQMOUUVeiohTt8A:124。这里设置size5000表示每scroll 5000条文档批量写一次不要和批量写入接口的batch_size混为一谈。第四步通过task接口跟踪进度GET _tasks/oTUltX4IQMOUUVeiohTt8A:124重点关注返回里的status.created、status.total、status.deleted、status.noops。当total和created接近并且响应里出现completed: true说明任务已经跑完了。第五步验证数据并切换别名。先用count接口对比新旧索引文档数GET order_v1/_count GET order_v2/_count数量一致之后给新索引挂别名再从别名读取验证POST _aliases { actions: [ { add: { index: order_v2, alias: order } }, { remove: { index: order_v1, alias: order } } ] }如果业务方之前全是按别名访问的这里切换别名之后线上查询自动落到新索引整个过程对应用透明。确认稳定之后再把旧索引删掉完成整个循环。3.3 核心参数逐个拆解Reindex请求里有几个参数直接影响数据正确性和执行效率我一个个讲清楚。op_type和conflicts算是一对好搭档。op_typecreate表示写入目标索引时如果_id已存在则冲突报错不加conflicts参数任务会直接中止加上conflictsproceed冲突文档会跳过任务继续跑最终返回的version_conflicts计数就是被跳过的文档数。如果是为旧索引做备份我强烈建议op_typecreate加conflictsproceed防止目标索引已有数据被覆盖。query参数可以只迁一部分数据比如只迁移最近90天的订单POST _reindex { source: { index: order_v1, query: { range: { create_time: { gte: now-90d } } } }, dest: { index: order_v2 } }这样适合做冷热分离、定期归档。源索引很大时只迁需要的数据能明显节省IO和时间。slices参数值得多说两句。这个参数是把Reindex任务拆成多个子任务并行执行比如slices5指定5个分片并行拷贝数据。设成autoES会自动按源索引的分片数并行通常是最省心的选择。我曾经在一个32分片、单分片几千万文档的索引上做过测试不加slices时迁移效率极低加auto后整体时长下降了60%以上。还有pipeline参数可以关联ingest pipeline在写入前做字段加工。比如新增一个keyword类型的tag字段、移除掉某些无用字段都可以用pipeline搞定不用在script里写复杂的代码。host、remote这些参数用于remote Reindex也就是跨集群复制后面统一讲。3.4 大索引调优三板斧处理上亿文档的大索引直接甩一个裸Reindex请求过去往往压着好几个小时跑不完。这里有三板斧可以显著提速。第一板斧目标索引临时关掉刷新和副本。刷新频率高每次写入都要生成segment并刷盘副本同步又会多复制一份数据。迁移期间可以把这两个东西临时调整到最低PUT order_v2/_settings { index: { refresh_interval: -1, number_of_replicas: 0 } }Reindex结束后改回来PUT order_v2/_settings { index: { refresh_interval: 1s, number_of_replicas: 1 } }第二板斧调大scroll的批量大小和协调节点的批量上限。批量大小影响每次读多少数据协调节点一次写多少数据则受接收bulk时的上限控制。可以把批量上限从默认的100MB调整到250MB再配合source里的size字段控制每轮读取条数一般size设置在5000到10000之间、每条文档在1KB上下时吞吐比较合适。第三板斧开启slices并行。前面提过这是最直接的效果放大器。对于大索引直接写slicesauto让ES按分片自动并发POST _reindex?wait_for_completionfalse { source: { index: order_v1, size: 5000, slices: auto }, dest: { index: order_v2, op_type: create } }注意这里slices要放在source节点里是旧版本参数位置容易写错的地方。官方文档显示slices放在request body顶层也可以但放在source里语义更明确很多线上排查案例都是因为slices写错位置没生效。还有一点关于副本和刷新的调整顺序。一定要先改目标索引的settings再启动Reindex而不是任务启动后再去调否则前半段数据已经走了高成本写路径起不到加速作用。真实实测下来关刷新、关副本、开slices三件事同时做了之后和默认配置相比提升非常明显单位时间吞吐量翻三倍是常规操作。4. 常见问题与避坑实录4.1 任务进度怎么跟踪、怎么取消异步提交Reindex后最常遇到的就是“这任务到底跑到哪了”的问题。如果用的是KibanaDevTools里可以直接看Stack Monitoring也可以执行task查询。原生API方式更通用查看所有Reindex任务GET _tasks?actions*reindexdetailed查看单个任务GET _tasks/taskId取消任务POST _tasks/taskId/_cancel如果任务卡住不动先把pending_tasks和hot_threads拉出来看一眼是不是积压了大量写请求或者某个分片排队。取消任务也要小心取消后已经写入新索引的文档不会回滚重新跑Reindex时如果目标索引没有做幂等控制就可能导致重复数据这也是为什么前面我推荐op_typecreate配合conflictsproceed。4.2 我踩过的几个坑说实话Reindex看着就一个请求的事真正跑起来坑全在细节里。拿我这些年遇到的典型问题整理成清单第一个坑是目标索引没提前建mapping直接Reindex让ES自动建mapping结果把数字字段识别成text日期字段识别成带毫秒格式的date后面查出来全是脏数据。所以目标索引mapping一定要手动定义至少要对齐核心字段类型。第二个坑是版本冲突设置缺失。默认情况下Reindex写文档是覆盖写如果你业务里依赖源索引_id目标索引里又已经有同一_id的数据不加op_typecreate就不会报错直接默默覆盖数据最终不一致却没人察觉。加op_typecreate后至少能知道有多少冲突。第三个坑是大批量Reindex把网卡打满。源集群和目标集群如果是同一批机器并发太高会影响在线业务。这种情况建议把slices设小一点比如和源分片数保持一致就行同时在业务低峰期执行不追求极限速度。第四个坑是旧索引突然变成只读。ES 7.x开始如果磁盘使用率超过高水位线索引会被自动设为只读。Reindex过程中如果磁盘快满了会报错或任务失败这时候需要先清理磁盘空间或者调整cluster.routing.allocation.disk.watermark相关配置否则任务反复失败状态还看不出来是磁盘问题。第五个坑是统计字段误导。task接口里的created只是“本次创建”的文档数不包含修改和跳过的。如果目标索引存在同_id数据total不等于created很正常判断任务是否完成要对照total和processed而不是盲目看created大小。4.3 进阶跨集群迁移与数据加工最后聊两个实用的进阶场景。跨集群Reindex也就是把A集群的数据搬到B集群。这种情况下请求发往目标集群source里加remote节点信息POST _reindex?wait_for_completionfalse { source: { remote: { host: http://source-cluster:9200, username: user, password: pass }, index: order_v1, query: { range: { create_time: { gte: now-30d } } } }, dest: { index: order_v2 } }跨集群执行时源集群的数据会先被scroll到目标集群的协调节点再写入对应分片所以目标集群的节点需要能够访问源集群地址。网络打通这一段是很多问题的根源建议先在两台集群节点上用curl测一下9200端口互通再跑任务。看数据量大小一次搬几十GB以上的话最好放在内网专线环境公网传输很容易因为网络抖动导致scroll游标失效。数据加工方面Reindex支持source里的script对文档做处理也支持dest里的pipeline。举个例子给新索引额外增加一个order_id_keyword字段要求保留原字段同时新增字段做聚合POST _reindex { source: { index: order_v1, script: { source: ctx._source.order_id_keyword ctx._source.order_id;, lang: painless } }, dest: { index: order_v2 } }这样一份数据同时保留原来的text分词能力又增加了keyword精确匹配能力在不影响检索习惯的前提下解决了聚合问题。工程上常见的“一个字段既要分词检索又要精确聚合”的诉求用这个方式解决最省事。4.4 关于Reindex的时候版本和插件的一点经验Reindex过程中如果涉及自定义分词插件比如中文IK要确保目标集群同样安装了对应版本插件否则Reindex写入时解析分词会报错。还有一种情况是源索引用了一个很冷门的分词器新集群没集成数据迁过去检索结果会跟旧集群不一致这种问题要到查询阶段才能暴露排查起来很头疼。版本升级前我习惯先在测试环境建两个索引用Reindex过一遍数据然后对比新旧索引的查询结果和聚合结果确认一致后再上生产。如果你在带认证的集群上用程序客户端触发Reindex还需要在HTTP请求头里带上认证信息内容。不同客户端写法不太一样但思路都一样在调用API之前先初始化好认证上下文基础认证或者Token认证都行不要为迁移去临时关闭安全认证那样反而会把集群暴露在风险里。最后说点亲手实践下来的体会Reindex这套机制设计得相当务实它没追求“一键自动搞定”而是把数据读取、批量写入、冲突处理、并行策略这些细节全部开放出来让使用者自己掌控。我经历过的几次大规模索引重建没有一次是严格按照默认参数直接跑到结束的但无一例外是先用小数据验证mapping调整好参数再开异步任务边跑边看task状态。心里有底了自然不用整夜守着日志。所以我的建议也很简单Reindex前先把目标索引的mapping定义得明明白白任务启动后多盯一下task接口的前几轮进度确认速度正常后再放手让它跑。这个小习惯帮我少熬了好几个夜。
返回列表