ARTICLE DETAIL

资讯详情

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

Elasticsearch IndexShard 分片原理与调优实战:从分配策略到性能优化

Elasticsearch IndexShard 分片原理与调优实战:从分配策略到性能优化 上周我这边线上一个订单库的集群突发写入毛刺排查到最后发现是分片迁移和段合并撞在了一起。后来我们团队复盘做了两件事把索引的IndexShard分配策略重调了一遍顺手把一些大索引按天级别的滚动策略拆得更碎。问题解决以后数据节点的负载明显回归平稳查询的 p99 也从一千多毫秒降到三百毫秒以内。这类问题在 Elasticsearch 运维里很常见核心都绕不开一个底层概念IndexShard——也就是索引分片。《IndexShard》不是某个具体的开源工具而是“索引 分片”在整个分布式存储体系里的统称。你建了一个索引数据物理上存在多个分片里每个分片又是一个独立的 Lucene 实例。理解了这一层你不仅知道怎么调分片数还能想明白为什么调了之后集群的写入性能、查询性能、磁盘占用和故障恢复速度都会跟着变。这篇文章我会从概念原理、分片数量设计、内存规划、生命周期管理、真实问题排查几个维度来拆尽量用我实操中遇到的例子来讲解。适合正在接手 Elasticsearch 集群、遇到分配不均或写入变慢、或者想从“会调参数”进阶到“懂分片运行逻辑”的运维和开发同学。1. 分片到底在分布式系统里扮演什么角色1.1 从一份文档说起分片是数据分布的最小单位先做一个很基础的推演假设你想在一个集群里存 1 亿条订单数据单台机器磁盘空间足够但查询越来越慢原因无非是单机的 CPU、内存、IO 都被一条请求链路吃满了。这时候最自然的想法是“把数据摊到多台机器上”。分片干的就是这件事它把一个完整索引切成若干块每一块叫一个分片。每个分片本身就是一个完整的倒排索引拥有独立的 Lucene 文件、独立的写入队列和独立的 merge 线程。这里有一个非常关键、但很多人一开始会忽略的认知分片不是数据库里的“分区表”它更接近 MySQL 分库分表里的“分库”。你操作一个分片时是操作一个独立的 Lucene 引擎而不是单纯地“取一部分行数据”。这也解释了很多诡异现象比如某些时候你只对索引做几次文档更新磁盘上却冒出一大批新段文件那是分片内部的段合并行为和数据量大小没有必然的线性对应。实际运维中我的判断习惯是凡是单索引超过 20GB并且查询模式比较随机比如按用户 ID 或时间范围查询就会考虑拆多个分片。如果写入是主场景那分片路数更多是为了摊开索引压力而不是让某一次的查询变得更快。因为分片越多一次搜索请求广播到分片的数量也就越多协调节点要等所有分片都返回结果才能汇总所以分片数并非越多越好。1.2 主分片与副本分片一份数据两种角色分片又分成主分片和副本分片。主分片是每条文档真正写入的目标分片副本分片是主分片的完整拷贝。这个设计和我以前管理 MySQL 主从复制不同ES 的副本分片不仅承担读请求还承担故障转移。任何时间点集群内只要还有主分片的副本在哪怕主分片所在节点整个宕机了ES 也可以快速把某个副本提升为主分片继续对外提供服务。副本数量对读取性能的影响需要辩证去看。副本越多理论上读并发能力越强——因为你可以在多个分片拷贝上分摊查询压力。但副本越多磁盘空间翻倍增加写入路径上每个副本也都会有一份索引操作。相当于你每次写入都要同时写入主分片和所有副本分片的缓冲区当节点间网络抖动时副本写入跟不上还可能触发主分片上的写延迟。我见过最简单的容量估算误导很多人计算“单分片 30GB所以 1TB 数据需要 34 个主分片”然后顺手把副本设为 1也即最终占用约 2TB 磁盘。他们忘了算副本的时候把总磁盘空间扣掉了结果节点磁盘全部爆掉。副本承载的不仅是容灾还是查询并发能力。你在设计分片数和副本数时得把“数据量 x (1 副本数)”作为集群物理容量的下限而不是简单参考主分片数据总量。1.3 路由机制一条文档是怎么落到分片上的ES 写入一条文档时会先计算文档所属分片。默认路由规则是shard hash(routing) % number_of_primary_shards大多数情况下routing就是文档的_id。这个设计决定了主分片数一旦确定就无法轻易变更——因为文档已经按照这个模数分散到各个分片里了增大分片数会导致原有的哈希映射失效所有数据都必须重写这也是为什么 ES 不直接支持修改主分片数的原因。我经常在答疑群里看到新手问“我主分片数设小了现在索引大了直接在 settings 里改一下number_of_shards就行吗”如果真这么操作会遇到两个问题第一ES 静态设置不允许在线修改第二就算允许你改已有的数据也不会自动重新分布。可行方案是重建索引使用 Reindex API 把旧数据搬迁到新索引但这也需要老索引包含足够的分片路由信息。所以分片数量必须建索引之前就定好这是 ES 所有设计中比较反直觉但也极其重要的一条约束。2. 主分片数量到底怎么定一个可复用的决策流程2.1 别只看数据量还要看单分片容量与节点数量分片数量设计没有统一公式但有一个经验区间被广泛验证过单分片的数据量在 20GB 到 50GB 之间是相对安全的。低于 10GB 分片过碎查询时协调节点要聚合太多返回结果高于 50GB 分片又太重故障恢复和段合并都很慢。为了说明设计过程我用一个实际场景推演。假设某业务每天新增日志数据约 80GB保留 30 天。那么总数据体量是 2400GB主分片按单分片 40GB 估算需要 60 个主分片。但是别忘了集群节点规模如果数据节点有 6 台每台分片数我们是希望控制在“分片总数 / 节点数”左右也就是 10 个主分片加 10 个副本分片每节点 20 个分片。这个数量级对 Lucene 的线程和内存是合适的。节点少了分片数量和单节点压力就失衡你会看到某台节点 CPU 很高其他节点却很闲这就是典型的数据热点。真实环境中不能只按“数据总量”来算。ES 的搜索请求一般会广播到所有主分片如果每个分片上数据平均那么单次查询耗时取决于最慢的那个分片这也是分布式系统常见的“木桶效应”。所以单分片容量维持在 20-40GB不只是为了磁盘均衡更是为了让单次查询的最慢耗时保持在合理范围。2.2 从写入吞吐反推分片策略再来推演一个写多读少的场景。假设系统峰值的写入速度是每秒 2 万条文档每条文档大约 1KB也就是约 20MB/s。单节点单分片在普通 SSD 上能扛住的写入吞吐大约在 10MB/s 到 30MB/s 之间但还要叠加 refresh 间隔、translog flush 落盘、副本同步等开销。为了保险我会按每分片 15MB/s 来估算。这时需要的分片数就是20MB/s ÷ 15MB/s ≈ 2听起来很少对吧但实际业务里这种算法低估了峰值。我们再看一次突增情况周六晚上活动流量翻 3 倍写入峰值到了 60MB/s这时候 2 个分片肯定扛不住至少需要 4 个主分片才稳妥。所以采用一个更保守的公式预估峰值写入带宽 ÷ 单分片可承受带宽 × 冗余系数。冗余系数我一般给 1.5 到 2。也就是说最终主分片数不是 2而是 4 到 6 个。这里的原理是分配分片时ES 会尽量把分片分散到不同节点如果节点数和分片数不够会导致单节点上出现多个分片共享同一份 I/O 和 CPU 资源最终写入性能被单个节点的瓶颈锁死。2.3 一个覆盖常规场景的决策清单经过多次踩坑我总结了一个“分片数决策清单”基本覆盖 90% 场景因素判断方向影响数据总量单分片控制在 20-50GB分片太大恢复慢太小聚合慢节点数量分片总数约为节点数倍数避免单节点压力集中写入峰值单分片写入带宽 10-20MB/s写入突发时不会阻塞查询模式查询是否常按时间范围可规划滚动索引副本数量默认 1允许按读并发调大磁盘翻倍与读能力的权衡未来数据增长预留 1.5~2 倍空间避免三个月后重建索引每次设计索引之前把这张表过一遍基本上不会出大岔子。3. 从原理到实操IndexShard 生命周期与调优手法3.1 分片生命周期全流程从创建到合并再到归档分片的生命周期可以分成几个阶段创建、写入、可查询、段合并、关闭、删除。前两个阶段是很多参数调优的落点。首先是创建阶段。创建索引时ES 会为主分片和副本分片分配节点。分配过程会结合磁盘空间、节点负载和部分分片分配策略。如果一个节点已经有大量分片新的分片不太容易继续分配过去这涉及cluster.routing.allocation.disk.watermark等水位线设置。我用过最省心的配置是先把low和high分别设置为 80% 和 85%高水位设置成 90%。留足缓冲避免磁盘几乎写满时 ES 自动迁移分片导致节点间网络和磁盘同时被占满。写入阶段分片接收到文档后先写 translog再写内存 buffer等到 refresh 间隔或 buffer 满了以后才生成一个新的 Lucene 段文件。refresh 间隔我通常设置成 10-30 秒。很多新手为了“查询实时性”把 refresh 设为 1 秒这会让 ES 每秒都生成新的段文件段数量增长过快后台段合并线程被频繁唤醒CPU 无故被打满。段合并阶段几乎是分片最常见的性能杀手。随着写入不断进行大量小段文件堆积Lucene 不断把小段合并成更大的段。合并会占用磁盘 IO 和 CPU尤其当分片数量多且单分片数据量偏大时合并风暴会很严重。可以使用_forcemerge在索引变成只读时把段数量压缩到 1例如时序索引在关闭写入之前执行POST /my_index_2024.01/_forcemerge?max_num_segments1。这样极大了减少了老索引对查询的 IO 压力而且减少磁盘占用。3.2 内存到底分给谁分片级内存模型很多集群毛刺问题最后都能追溯到堆内存设置不合理上。ES 进程的 JVM 堆内存默认是机器内存的一半但上限官方建议不超过 32GB。如果你机器是 64GB 内存JVM 堆最多给 30-31GB剩下的内存多数分配给操作系统页缓存因为 Lucene 的索引文件读取非常依赖页缓存。分片内存主要消耗在倒排索引和文档值DocValues上。倒排索引用于快速定位哪些文档包含关键词DocValues 则用于排序和聚合两者加载到堆外内存时都占用页缓存。如果页缓存不够查询就要从磁盘重新加载表现为“第一次查询慢第二次查询快”或者清理缓存以后查询整体变慢。实操中我通常把indices.breaker.total.limit设置为 70% 左右防止聚合查询和 Fielddata 爆掉整个堆。同时把indices.fielddata.cache.size设置为堆内存的 20%-30%一旦缓存抖动至少要保证主流程不宕机。很多同学遇到CircuitBreakingException就只会加大堆内存其实更有效的手段是关掉高基数维度上的大量 terms 聚合或者用eager_fielddata预加载代替实时加载。3.3 一个具体的分片生命周期配置模板如果你正处于“看文档看懂上手不知道怎么写”的状态我这里给一套在 8.x 版本验证过的索引模板可以作为起点{ settings: { number_of_shards: 60, number_of_replicas: 1, refresh_interval: 30s, index.translog.durability: async, index.translog.flush_threshold_size: 1024mb, index.merge.scheduler.max_thread_count: 1, index.routing.allocation.total_shards_per_node: 20, index.codec: best_compression } }参数含义简单说refresh_interval30 秒是为了减少小段数量index.translog.durability: async意味着异步刷 translog能显著提升写入吞吐但极端断电场景可能丢失的部分数据如果不可接受就保持requestindex.merge.scheduler.max_thread_count: 1是为了避免段合并占满 CPUtotal_shards_per_node用于控制单节点上分片总量避免堆内存和线程数被分片数摊薄index.codec: best_compression会让 Lucene 采用压缩算法更激进的编码磁盘占用减少但 CPU 开销略有上升。这套配置适合日志、订单流水等写入大、查询次之的索引。如果你的场景是高并发查询压缩成本反而可能拖慢查询建议默认编码即可不做best_compression。3.4 动态扩容与收缩IndexShard最实用的两个技能前面提到主分片数不能直接修改但系统运行一段时间之后往往会遇到“初始分片不够用了”或者“老索引分片过多但没查询了”这两个尴尬场景。前者需要用 Reindex 重建索引POST /_reindex { source: { index: old_index }, dest: { index: new_index } }如果旧索引数据量非常大比如几个 TB直接 reindex 会在短时间内占用大量资源。更合理的做法是把源索引设置为只读然后按天或小时分批 reindex期间对目标索引临时关闭 refresh实时写入目标索引的批量速度比默认设置高出不少。我做过一个案例数据量 2.1TB按天拆分成 30 个批次做 reindex每批数据大概 70GB分片配置是目标索引 48 个主分片、1 个副本。跑完每批大约 15 到 20 分钟全程集群没有拒绝请求节点负载一直可控。如果索引已经不再写入但还需要查询就用 Shrink 来收缩分片数量。POST /old_index/_shrink/new_index需要先设置index.blocks.writetrue并且目标分片数必须能够整除源分片数。比如 60 个主分片可以收缩到 30、20、15、12、10、6、5、4、3、2、1。收缩完成以后记得去掉只读块。收缩本质上是在同一个节点上把分片文件硬链接或复制成更少、更大的分片旧分片不会自动消失还要手动删除。4. 实操过程一个新的日志索引是怎么从零配好的4.1 业务场景与技术选型背景我手上有一个日志平台项目每天新增各类应用日志约 120GB保留 10 天。原先架构是 5 台数据节点单节点磁盘 4TB内存 32GB磁盘空间和内存都够但问题是经常出现查询超时。看监控发现大部分超时都源于某几个大索引单索引数据近 1TB分片数却只有 5 个单分片 200GB明显不合理。我先和业务方确认了两件事查询是否按时间范围过滤业务对数据可见性的容忍度是多少。确认结果是“日志基本都是按天查”意味着我可以做成按天滚动索引每个索引只存放一天的数据数据量约 120GB。这就把“大索引单分片过大”问题转化为“中量级索引下合理分片设计”问题。4.2 索引模板设计与分片落位决定按天滚动后主分片数可以重新设计。单索引 120GB单分片控制在 30GB主分片数可以设为 4。有人会问“4 个主分片够吗每次查询同时打 4 个分片岂不是比原来的 5 个分片还少”这里要注意查询慢的根源不是分片数量少而是单分片数据量太大导致 Lucene 读取耗时长。4 个分片每个 30GB 和 5 个分片每个 200GB对于同样一次范围查询前者要扫的数据文件小得多IO 压力也轻很多。模板设置为{ index_patterns: [app-log-*], settings: { number_of_shards: 4, number_of_replicas: 1, refresh_interval: 20s, index.routing.allocation.total_shards_per_node: 3 }, mappings: { properties: { timestamp: { type: date }, service: { type: keyword }, message: { type: text, fields: { keyword: { type: keyword } } } } } }其中total_shards_per_node: 3是刻意的5 个数据节点每天索引 4 个主分片 4 个副本分片共 8 个分片。ES 分配时会尽量把主副本分散到不同节点。限制每节点最多 3 个分片可确保任意节点宕机后重新分配时剩余节点有足够余地接住这些分片而不是瞬间压垮邻近磁盘。4.3 实际验证与效果上线以后查询 p99 从 1200ms 降到 280ms写入侧偶尔还有短暂毛刺但不再持续报错。为了让旧索引查询更快跑了一遍后台强制合并# 先设置只读 PUT /app-log-2024.01.22/_settings { settings: { index.blocks.write: true } } # 强制合并到 1 个段 POST /app-log-2024.01.22/_forcemerge?max_num_segments1合并期间我盯着节点 IO峰值升到约 70%但没有影响写入和新索引查询。合并完成后删除旧索引的index.blocks.write不影响后续查询。这一套做完我们对运维侧的告警阈值也做了调整重点观察三个指标集群_cat/shards是否出现 unassigned 分片、节点磁盘水位是否超过 80%、索引 Segment Count 是否异常增长。5. 常见问题与排查技巧实录5.1 分片玩失踪unassigned 分片怎么查怎么办unassigned是分片最常见的异常状态之一。分片显示为黄色或者红色原因可能是节点宕机后新节点加入也可能因为磁盘水位满了导致 ES 无法把分片分配过去。首先看最直观的信息GET /_cat/shards?vhindex,shard,prirep,state,node,unassigned.reason如果unassigned.reason是NODE_LEFT一般等节点恢复后会自动再分配。如果长时间不分配多半是节点上的cluster.routing.allocation.enable被设成了none或primaries。我踩过一次蛮低级的水位坑磁盘使用率超过cluster.routing.allocation.disk.watermark.high默认 90%后ES 会把该节点上的分片迁到别处但如果所有节点的水位都超过阈值分片就滞留。这时候不是堆内存问题也不是节点宕机只需要清理一点旧索引释放空间分片就会自动重新分配到其它节点。如果想手动强行分配可以这样操作POST /_cluster/reroute?retry_failedtrue这条命令常用于分片分配失败后的重试。但要注意频繁手动 reroute 可能导致分片在不同节点间来回漂移造成无谓的网络传输和数据热点。我一般只会在节点恢复且确认节点可写后才执行。5.2 段数量异常增长一个小问题引发的连锁故障有一次线上某索引查询延迟直线上升检查发现该索引单个分片的 Segment 数竟然有 800 多个。原因是我把refresh_interval调成了 1 秒又开启了index.merge.scheduler.max_thread_count默认的高线程数。每秒刷新一次每 30 秒产生一个小段文件段合并线程来不及合并小段于是查询时 Lucene 需要打开大量文件句柄IO 和 CPU 同时飙升最终导致节点垃圾回收频繁。解决步骤# 查看单个索引段数 GET /my_index/_segments # 如果数量过大先关闭写入再强制合并 POST /my_index/_forcemerge?max_num_segments1事后我把refresh_interval从 1s 调整为 30s把max_thread_count调整为 1。强制合并后查询耗时从 900ms 降到 120ms 左右。这里要特别提醒对于还在持续写入的大索引不要频繁执行_forcemerge因为这会反复触发段合并形成“写一段合一段”的恶性循环。5.3 主分片计数不可改遇到容量瓶颈时的应对方案很多用户会遇到“索引越来越慢主分片又没法扩容”的尴尬。此时正确解法是重建索引POST /_reindex { source: { index: old_index }, dest: { index: new_index } }新旧索引的分片数可以不同reindex 会按新索引设置重新路由文档。迁移之前建议先把新索引的refresh_interval改为 -1不自动刷新副本数暂时设为 0。原因是 reindex 会产生大量写入如果一边实时刷新生成新段一边写入副本速度会大打折扣。等 reindex 完成后把刷新和副本数调整回来PUT /new_index/_settings { index: { refresh_interval: 30s, number_of_replicas: 1 } }这种方式我在迁移 200GB 索引时验证过在 3 个数据节点上全程跑了大概 6 分钟比默认配置快了一半以上。这样做还有一个额外好处新索引的段从一开始就是大段因为 reindex 期间没有频繁 refresh。5.4 磁盘水位导致的分片只读必须提前预防ES 在磁盘使用率超过cluster.routing.allocation.disk.watermark.flood_stage默认 95%以后会主动将索引置为只读也就是index.blocks.read_only_allow_delete此时业务写入会直接报错。这是 ES 自我保护的最后一道防线但很多人认为这是“误报”疯狂调大 watermark这样反而会增加数据丢失风险。我建议的设置组合水位类型默认值建议值low85%80%high90%85%flood_stage95%90%把水位整体调低 5%看起来浪费了一点磁盘但换来了更大的搬迁缓冲和主动介入时间。另外你需要监控索引的health状态当出现只读块时先清理旧索引或者调大磁盘然后执行PUT /my_index/_settings { index.blocks.read_only_allow_delete: null }只读块会在磁盘水位降下来之后自动解除但如果不清除这个块即使磁盘空出来索引状态依然不会自动恢复写入。6. 一些真实的调优体会IndexShard 这个词听上去很像某个底层模块的内部名称但它其实浓缩了 Elasticsearch 集群设计里最核心的取舍数据如何分布、冗余如何保证、资源如何被分片和段占用。你花时间理解分片回报是几十倍的排查效率提升。我的一个个人心得是不要一碰到集群慢就立刻加副本、加节点。先拿_cat/shards和_segment看一眼数据分布很多问题一眼就能看出来——某节点分片明显过多、某分片段的数异常大、某节点磁盘水位已经水流淹到脖子。把背后的机制搞清楚再加配置才有意义。另一个心得是分片数和节点数之间要留弹性。比如 5 个节点你硬把主分片设计成 60 个每个分片 5GB从数据量上看完全没问题但实际上这段时间日志增长很快半年后单分片涨到 30GB同时需要重算副本分配时节点的磁盘分布就会出现严重倾斜。最好是按照“未来 1.5 到 2 倍数据量”设计分片数而不是按当前体量精确计算。最后分享一个我自己的小习惯每次新建或者调整索引之前我都把分片设计方案写成一个简短文档包括数据量估算、节点数量、单分片容量、预计写入峰值、查询延迟目标和预留扩容空间。这样即使过了几个月以后再遇到同类问题也能快速回忆起当初的决策依据。后续如果有新的业务接入直接沿用这套模板踩坑概率大大降低。
返回列表