分布式搜索扩容后QPS不涨:分片、段合并与查询链路排查

发布时间:2026/11/6 15:02:01

分布式搜索扩容后QPS不涨:分片、段合并与查询链路排查 集群从三个节点扩到六个机器加了一倍QPS 却没跟着涨p99 反而在凌晨两点抖到两秒多——这是我在一个日志检索场景里真实遇到过的情况。当时第一反应是扩容没生效查完_cat/nodes才发现节点都在副本也分下去了问题根本不在扩容本身而在于分片数、段合并和查询链路这几个藏在背后的东西平时不显眼一上量就全冒出来了。分布式搜索这条线把单机跑起来、把集群拉成三节点只能算热身。真正拉开差距的是接下来这层一条文档到底落到哪个分片、写进去为什么一秒后才搜得到、from size翻到第 500 页为什么突然变慢、后台那个叫 merge 的任务凭什么能把磁盘 I/O 吃满。这些机制串起来才叫分布式搜索。这篇适合已经能跑通集群、但一遇到延迟抖动和写入拒绝就抓瞎的同学也适合正在做容量规划、要给集群定分片数的人。下面的内容我尽量按先讲清为什么再给能抄的配置这个顺序走中间夹一些踩过的坑。1. 一个文档到底进哪个分片路由规则决定了分片数不能乱改很多人对分片的理解停留在数据切开放到不同机器上这没错但漏掉了最关键的一点切分的依据是路由值而路由算法直接锁死了主分片数量。这一层想不明白后面扩容、建索引、reindex 全是被动应付。1.1 哈希取模这一步为什么让主分片数变成一次性决策写入请求到了协调节点之后第一件事不是找机器而是算一个值shard_num hash(_routing) % number_of_primary_shards_routing默认就是文档的_id。也就是说用户传进来的 ID 经过哈希对主分片数取余得到的结果就是这条文档该去哪个分片。整个过程不查任何元数据表纯计算所以路由是 O(1) 的这也是 ES 能在毫秒级定位数据的基础。代价是什么主分片数一旦定下来改不了。假设原来 6 个主分片某条文档 ID 哈希后对 6 取余是 4落在 P4。你现在想把主分片改成 8同一个 ID 对 8 取余可能变成 1落在 P1——但那条数据还躺在 P4 里。系统不会自动搬家它只会找不到。这不是 bug是设计上的必然取舍用分片数不可变换路由零查表。所以有一条铁律需要刻在脑子里主分片数在索引创建那一刻就要定好后期只能靠 reindex 或 split 重建。副本数随便改主分片数不行。我在早期项目里吃过这个亏当时想着先建 5 个分片不够再加结果数据涨到 800GB5 个分片每个 160GB单个分片太大导致恢复慢、merge 压力集中最后花了整整一个周末做 reindex 加别名切换。补充一点自定义_routing是允许的比如按用户 ID 路由让同一个用户的数据落到同一分片查询时能直接命中单分片省掉广播。但这么做的副作用是数据倾斜——大客户的数据全堆在一个分片上热点问题比性能收益更明显。除非业务上确实需要按租户隔离查询否则别轻易自定义。1.2 分片数怎么算从单分片容量和堆内存两头倒推定分片数这件事网上的建议五花八门有说 1GB 一个的有说 50GB 一个的。这些数字不是拍脑袋来的背后有两个硬约束。第一个约束是单分片数据量。分片是 ES 调度的最小单位一个分片只能待在一个节点上。分片越大恢复、迁移、merge 的时间越长。行业里比较通行的经验区间是10GB 到 50GB超过 50GB 之后单分片恢复耗时会明显拉长节点故障后的数据补齐窗口变得不可控。低于 10GB 则分片数量过多每个分片都有独立的 Lucene 索引、独立的段文件、独立的元数据开销协调节点在查询时要等所有分片返回分片太多反而拖慢。第二个约束是堆内存。每个分片都要在堆里维护一份段级别的元信息经验值大约是每分片 1MB 到 2MB 的常驻开销段数量多的时候更高。一台 64GB 内存的机器堆一般给到 31GB超过 31GB 会失去指针压缩反而更浪费能撑住的活跃分片数量大概在 600 到 1000 之间。这里说的是活跃分片包括主分片和副本。我一般的算法是这样走一遍步骤计算方式举例1. 估算数据总量日均增量 × 保留天数 × 副本数50GB/天 × 30天 × 2 3TB2. 单分片目标容量取 30GB 左右保守30GB3. 主分片数3TB ÷ 2副本 ÷ 30GB ≈ 50取 484. 对齐节点数主分片数取节点数的整数倍6 节点 × 8 485. 校验堆占用48 × 2含副本× 1.5MB ≈ 144MB可接受第 4 步容易被忽略但很重要主分片数最好能被数据节点数整除。否则每个节点分到的分片数量不均导致某几台机器磁盘和 CPU utilisation 明显偏高扩容时也容易出现加了机器但分片搬不过去的尴尬。48 个主分片放在 6 个节点上正好每个节点 8 个主分片换成 50 个就有节点要扛 9 个长期看是隐患。还有一个容易踩的点分片数是给未来定的不是给现在定的。刚上线数据量 50GB建了 48 个主分片每个分片才 1GB看着挺浪费——但只要你能确认三个月后数据量会到 1TB 以上这个数量就是对的。反过来如果这是个长年不涨的小索引3 到 5 个分片就够了别照搬大索引的配置。1.3 分片定错了怎么救reindex 别名切换的完整流程发现分片数不合理能走的路径只有两条reindex 重建或者split / shrink。前者通用后者有前提条件split 要求索引是只读的并且源主分片数能被目标整除。我推荐的做法是从一开始就用别名让上层应用只认别名不认物理索引。这样重建时切换是无感的# 第一步创建新索引分片数按新的规划来 PUT /orders_v2 { settings: { number_of_shards: 24, number_of_replicas: 1, refresh_interval: 5s }, aliases: { orders_read: {} } } # 第二步后台跑 reindex注意限速别把线上写入挤死 POST /_reindex?wait_for_completionfalse { source: { index: orders_v1, size: 2000 }, dest: { index: orders_v2 }, conflicts: proceed }wait_for_completionfalse会返回一个 task id用GET _tasks/task_id轮询进度。这一步很关键大索引 reindex 动辄几小时同步等待会超时。size参数控制每批拉的文档数默认是 1000对于宽表字段多、单文档大的场景要往小调否则容易触发批量请求体过大的限制。conflicts: proceed也要谨慎用——它会让版本冲突的文档被跳过而不是中断整个任务适合数据允许最终一致的场景。如果是账务类数据建议先用conflicts: abort跑一遍确认没有冲突再决定。数据同步完之后切换别名是一步原子操作POST /_aliases { actions: [ { remove: { index: orders_v1, alias: orders_write } }, { add: { index: orders_v2, alias: orders_write } }, { add: { index: orders_v2, alias: orders_read } }, { remove: { index: orders_v1, alias: orders_read } } ] }把读写别名分开是有讲究的。写别名只指向新索引读别名在同一批次里同时包含新旧两个索引一段时间等确认数据完整了再摘掉旧的。这样即使 reindex 期间有少量数据没同步过来读请求也能兜住。我一般会保留旧索引至少一周再删尤其是那种数据回补脚本跑得不及时的团队。提示reindex 期间源索引的写入不会被自动捕获。要么停写要么在 reindex 结束时补一次增量同步按时间字段过滤这一步千万别漏。2. 写入链路上的三跳转发为什么文档写完要等一秒才搜得到写入这块最常见的困惑就一句话我明明写成功了为什么立刻查查不到这个问题背后是 refresh、translog、flush 三个机制在分工搞清楚它们各自管什么很多数据丢失的误判就不会再发生。2.1 协调节点、主分片、副本一次写入到底转了几手一条写入请求进来先落到协调节点任何一个节点都能当协调节点。协调节点算完路由知道目标主分片在哪个节点上把请求转发过去。这一步是网络跳转第一跳。主分片节点收到请求后做三件事写内存缓冲区、写 translog、然后把请求并行转发给所有副本分片。注意是并行不是串行——这是 ES 写入吞吐能做到不错水平的原因之一。副本数量增加会放大网络和磁盘压力但不会线性拖慢主分片的响应因为主分片只等最快的那批或者全部返回取决于一致性配置。副本全部写入成功后主分片向协调节点返回成功协调节点再把结果返回给客户端。整个链路是三次网络往返客户端 → 协调节点 → 主分片 → 副本 → 主分片 → 协调节点 → 客户端。所以在大集群里协调节点的选择是有讲究的让客户端轮询所有节点而不是全打到一个节点上避免单点成为转发瓶颈。这里有个容易忽略的细节协调节点本身也参与分片分配它可能既是协调者又是主分片的宿主。如果一台机器同时承担大量协调转发和分片读写CPU 会被两头挤。规模大了之后一般会把协调角色独立出来配几台低配高网络的机器专门做转发数据节点只管存储和计算。可以用_nodes/stats观察每个节点的转发压力GET _nodes/stats/indices,transport?filter_pathnodes.*.indices.indexing,nodes.*.transport看indexing.index_total和transport.rx_count如果某台机器的这两个值明显高于其他节点说明流量分发不均需要检查客户端侧的节点列表配置。2.2 refresh 与 translog 的分工近实时的边界在哪写入成功后文档在内存缓冲区里还没有进入可搜索的段文件。这时候你去查查不到——这就是近实时的含义不是实时。refresh干的事是把内存缓冲区的文档生成一个新的 segment写进文件系统缓存page cache。注意是文件系统缓存不是磁盘。这一步之后文档就能被搜到了。默认refresh_interval是 1 秒所以最坏情况下你写入后要等 1 秒才能搜到。调这个参数的收益和代价都很直接。把refresh_interval从 1s 调到 30s写入吞吐能提升好几倍因为每次 refresh 都要创建新 segmentsegment 多了之后 merge 压力陡增。批量导入场景下我一般直接把 refresh 关掉PUT /logs_bulk { settings: { refresh_interval: -1, number_of_replicas: 0 } }导入完成后再改回来同时把副本数加上去PUT /logs_bulk/_settings { refresh_interval: 30s, number_of_replicas: 1 }导入期间副本设 0 是个常用技巧——写入不需要等副本确认吞吐翻倍等数据灌完了再补副本代价是一次全量分片复制但比全程带着副本写要快得多。那 translog 是干嘛的它是预写日志每次写操作在返回客户端之前都要写进 translog。因为 refresh 之后的 segment 还在文件系统缓存里机器断电就没了translog 是唯一的持久化保障。默认配置下index.translog.durability是request意思是每个写入请求都fsync一次 translog 才返回成功。改成async能把写入性能再提一截但代价是index.translog.sync_interval默认 5 秒内的数据在断电时会丢。这个取舍要看业务日志类数据丢几秒无所谓订单类数据绝对不行。我见过为了压测指标好看把 durability 改成 async上线后忘了改回来结果一次机房断电丢了半分钟订单——这种坑不值得踩。flush则是更重的操作它把内存里的 segment 强制fsync落盘然后清空 translog。flush 的触发条件有三个translog 大小超过阈值默认 512MB、距离上次 flush 时间超过 30 分钟、显式调用。日常不用管它但如果你看到磁盘 I/O 周期性飙高可以看看是不是 flush 太频繁适当调大index.translog.flush_threshold_size。2.3 wait_for_active_shards 与写入拒绝那些写不进去的真实原因写入被拒绝最常见的报错是两类es_rejected_execution_exception和unavailable_shards_exception。前者是线程池满了后者是分片不可用处理思路完全不同。线程池满的判断方法GET _cat/thread_pool/write,search?vhnode_name,name,active,queue,rejectedqueue列持续接近上限write 默认 10000search 默认 1000rejected持续增长就是过载了。write 线程池的线程数默认等于 CPU 核数队列 10000这个配置在写入突增时表现为延迟上升但不断流一旦队列也满了就开始拒绝。要不要调大队列我的经验是不要轻易调。队列本质上是个缓冲区队列越长请求在内存里排队的时间越久最终要么超时要么 OOM掩盖了真实的容量不足。正确的做法是先看是不是 refresh 太频繁、是不是 merge 抢了太多资源、是不是副本数设得过高把根因找出来。队列调大只是把爆炸推迟几分钟。wait_for_active_shards这个参数值得单独说。默认值是 1意思是只要主分片活着就返回成功副本还没同步完也照常返回。可以设成all或者具体数字比如 2 表示至少 1 个主分片加 1 个副本确认。设成all的代价是写入变慢而且任何一个副本出问题都会导致写入失败——生产环境我基本不这么配除非是极高一致性要求的场景。真正需要关注的是分片不可用的情况。_cluster/allocation/explain这个接口能告诉你某个分片为什么没分配GET _cluster/allocation/explain { index: orders_v1, shard: 3, primary: false }返回里会有allocate_explanation和node_allocation_decisions常见的拒因包括磁盘水位超限low disk watermark [85%] exceeded、节点属性不匹配、分片数超过限制。磁盘水位这个特别常见——单节点磁盘用到 85% 之后ES 就不再往这台机器分配新分片了很多人扩容后看到黄色状态查半天才发现是磁盘快满了。3. 查询为什么跑两趟query then fetch 的代价与深分页陷阱查询这块的性能问题九成以上都跟分片数 × 单页大小这个乘积有关。理解了 query then fetch 的两个阶段你就能自己算出为什么翻到第 100 页会突然变慢。3.1 协调节点的归并成本from size 是怎么被放大的一次搜索请求打到协调节点它要做的是第一阶段query phase把请求广播给索引的所有分片主分片或副本之一轮询选。每个分片在本地执行查询、打分、排序然后只返回from size条文档的 ID 和排序值给协调节点。第二阶段fetch phase协调节点把所有分片返回的结果做全局归并取出真正需要的size条再按文档 ID 去对应分片拉取_source。关键点在第一阶段的from size。假设你有 5 个分片要取第 100 页每页 20 条那么from 1980size 20。每个分片都要返回2000条文档的 ID 给协调节点5 个分片就是 10000 条。协调节点要在内存里对这 10000 条做堆排序才能确定全局前 2000 名。分页越深这个数字涨得越快。翻到第 500 页from 9980每个分片返回 10000 条5 个分片就是 50000 条。协调节点要处理 5 万条记录的归并内存和 CPU 都扛不住。这就是为什么 ES 用index.max_result_window默认 10000硬性限制from size的上限。这个限制不是随便设的它是保护协调节点不被打挂的底线。有人图省事直接改成 100000短期内不报错了但在数据量大的索引上一次深分页查询就能把协调节点的堆打满进而触发 GC 停顿影响整个集群。我见过一次事故就是这么来的某个报表页面为了能翻到最后一页把窗口开到 5 万结果每天早会时间点都会出现集群级别的延迟毛刺。3.2 scroll、search_after 与导出场景的取舍深分页有三条路scroll、search_after、以及调大max_result_window不推荐。前两个各有明确的适用边界。scroll的思路是给这次查询拍个快照然后一批一批往下拉。它会在服务端维持一个搜索上下文每次请求带上scroll_id返回下一批结果。快照意味着整个遍历期间数据是固定的中途新写入的文档不会出现。这个特性对数据导出非常合适但对用户翻页就不合适了——用户看到的应该是实时数据不是几分钟前的快照而且 scroll 上下文会占用堆内存忘了清理就是内存泄漏。POST /orders/_search?scroll2m { size: 1000, query: { range: { created_at: { gte: 2024-01-01 } } }, sort: [{ created_at: asc }] }scroll2m是上下文保活时间每次请求会刷新这个时间。用完必须显式清理DELETE /_search/scroll { scroll_id: [上一步返回的 scroll_id] }search_after是更现代的方案思路是用上一页最后一条记录的排序值作为游标。它不需要服务端维持上下文内存开销小而且能看到实时数据。代价是不能跳页——只能一页一页往后翻做不了直接跳到第 800 页。GET /orders/_search { size: 20, query: { term: { status: paid } }, sort: [ { created_at: desc }, { order_id: asc } ], search_after: [1690000000000, ORD-88213] }这里有个细节要注意排序字段组合必须是全局唯一的否则会出现重复或漏数据。像created_at这种时间戳同一毫秒可能有多条记录只按它排序会导致游标定位不准。解决办法是加一个唯一的 tiebreaker比如订单 ID 或_id。用_id做 tiebreaker 性能一般因为它没有 doc_values每次都要从存储里读如果业务表本身有唯一且有序的字段优先用它。ES 7.10 之后还有个point in timePIT配合search_after能同时拿到快照一致性和低内存开销是目前做深分页遍历最推荐的组合。PIT 的用法是先开一个时间点然后在查询里带上pit.id用完关掉。相比 scroll它不会因为遍历过程中段合并而导致结果不一致。方案数据一致性能否跳页内存开销适用场景from size实时能随深度线性增长前 10 页的常规翻页scroll快照不能中需手动清理离线导出、全量遍历search_after实时不能低用户端无限滚动search_after PIT快照不能低大数据量导出且要求一致3.3 分片数对查询延迟的二次放大除了深分页分片数量本身也在放大查询开销。一个查询要广播到所有分片协调节点要等最慢的那个分片返回长尾效应所以分片越多撞上慢分片的概率越高。假设单个分片的查询延迟分布是 p50 5ms、p99 40ms5 个分片时整体延迟大概在 p99 ≈ 45ms 左右20 个分片时就要接近 80ms 了。这不是线性关系而是最大值分布的统计效应。这也解释了为什么很多团队在扩容时发现QPS 没涨、延迟反而升了——加了节点但索引没重建分片数没变每个分片反而要处理更多数据或者重建了索引把分片数翻倍查询要等的分片多了尾部延迟被放大。对抗这种放大有几个实用手段。一是减少不必要的分片查询如果查询条件里带了路由键比如租户 ID用_routing参数直接定位到单分片能省掉全部广播。二是给慢查询加超时和降级用timeout参数控制单个查询的最长等待超时返回部分结果避免一个慢查询拖垮整个线程池。三是缓存热点查询request_cache对聚合类查询有效重复的聚合请求可以直接命中缓存。GET /orders/_search?request_cachetrue { size: 0, timeout: 800ms, aggs: { by_status: { terms: { field: status, size: 10 } } } }size: 0表示只要聚合结果不要文档能省掉整个 fetch 阶段。这个技巧在报表类查询里非常有效但很多人不知道加白白让 fetch 拖慢了响应。4. 段合并那个不显眼却能把整台机器吃干的后台任务写完、查完还有一件事在后台持续发生段合并。它不出现在任何首页监控里但只要你的写入量上去了就一定会在某个时刻跟它正面相遇——通常表现为磁盘 I/O 突然打满读写延迟一起飙升。4.1 段为什么不可变删除又是怎么实现的Lucene 的核心设计是段不可变。一个新 segment 一旦写到磁盘就再也不会被修改。这个设计带来的好处是巨大的不用加锁、可以缓存、可以并发读、崩溃恢复简单。代价是更新和删除都变成追加 标记。删除一条文档实际上是把它的 doc id 记在一个.del文件里查询时跳过。更新一条文档是先标记旧版本删除再写入一条新的。所以频繁更新的索引磁盘占用会膨胀——旧文档还在段里躺着只是被标记为已删。这部分空间什么时候回收就是靠 merge。merge 会把若干个小段读出来去掉被标记删除的文档写成一个更大的新段然后删掉旧段。所以 merge 有两个作用降低段数量提升查询效率因为每个段都要单独查和回收已删除文档占用的空间。段数量对查询的影响很容易被低估。一次查询要在每个段上执行段越多跳表、评分、归并的开销都越大。ES 默认的index.merge.policy是分层策略会尽量把段数量维持在一个合理区间。但如果你在短时间内大量写入段数量会迅速上涨查询延迟随之变差。看段数量GET _cat/segments/orders_v1?vhshard,segment,size,docs.count,docs.deleteddocs.deleted这一列如果有大量非零值说明有很多被标记但没回收的文档可以考虑在低峰期做一次 forcemerge。4.2 forcemerge 什么时候该用什么时候是灾难forcemerge的作用是强制把多个段合并成指定数量。最常见的用法是对只读索引做max_num_segments1POST /orders_2024_01/_forcemerge?max_num_segments1但这里有个前提——索引必须是只读的。对一个还在持续写入的索引做 forcemerge合并出来的大段很快又会被新写入的小段淹没而 merge 本身消耗的 I/O 是实打实的。更糟的是forcemerge 期间会产生巨大的磁盘临时占用需要同时容纳旧段和新段如果磁盘水位本来就在 80% 以上很可能直接触发水位告警分片开始往其他节点搬。我的做法是按时间滚动的索引在封存之后做一次 forcemerge。比如按月建索引1 月的索引在 2 月初不再写入这时候挑个低峰期跑 forcemerge把段数压到 1查询性能能明显改善磁盘空间也能回收一批因为删掉的文档被真正清除了。forcemerge的 API 支持异步执行加wait_for_completionfalse避免请求超时。同时要注意它只有单线程多个分片的合并是串行的所以一个大索引可能跑几个小时。用_cat/thread_pool/force_merge能看到排队情况。还有一个更轻量的替代方案_forcemerge只合并删除比例高的段。ES 没有直接提供这个粒度但可以通过only_expunge_deletestrue参数实现只回收被删除的文档不改变段的结构。这个操作便宜得多适合日常清理POST /orders_2024_01/_forcemerge?only_expunge_deletestrue4.3 写入放大与 I/O 的实测观察merge 的 I/O 放大倍数是可以估算的。假设每秒写入 100MB 原始数据refresh 每秒生成 1 个段merge 会不断把这些段合并成更大的段。一次完整的层级合并总写入量大约是原始数据的2 到 3 倍因为每一层都要重写一遍。也就是说 100MB/s 的写入会带来 200 到 300MB/s 的磁盘写压力。这个数字如果超过磁盘的写带宽就会表现为写入队列堆积、refresh 变慢、查询延迟上升。我遇到过一次磁盘 util 长期 95% 以上的情况查下来是索引的refresh_interval设成了 1s、每天写入 20 亿条日志、分片数又只有 6 个——每个分片每秒都要生成新段merge 完全跟不上写入速度。当时的处理是三条一起上措施具体操作效果调大 refresh_interval1s → 30s段生成速度降为 1/30merge 压力骤减增加主分片数6 → 24重建索引merge 压力分散到更多节点限制 merge 线程数index.merge.scheduler.max_thread_count: 1避免 merge 抢占过多 I/O第三条值得展开说。index.merge.scheduler.max_thread_count默认在机械盘上是 1在 SSD 上是min(4, CPU/2)。如果你的存储是普通云盘IOPS 有限把它设成 1 能显著降低 merge 对读写请求的干扰代价是 merge 完成得更慢、段数量维持得更高。这是个用空间换稳定的取舍在写入压力大而查询不敏感的场景里值得。注意max_thread_count只能通过索引 settings 修改且对已有索引生效需要关闭再打开close/open不能热更新。改之前先评估影响。5. 集群状态与选主绿色、黄色和红色到底在说什么集群健康状态是运维最常看的指标但很多人只会看颜色不知道黄色和红色分别意味着什么、该怎么处理。这一节把三个颜色、选主机制和分片分配串起来讲。5.1 三种颜色的准确含义以及黄色为什么常被误判绿色所有主分片和副本分片都已分配。黄色所有主分片已分配但至少有一个副本分片未分配。数据可读可写但没有冗余节点挂掉就可能丢数据。红色至少有一个主分片未分配。这部分数据不可用查询会返回部分结果或者直接报错。黄色是最容易被忽视的状态。很多单节点集群天生就是黄色不可能把副本分配到和主分片同一节点上大家看习惯了就当成正常。但在多节点集群里出现黄色必须查清楚原因因为这意味着某份数据只有一份。排查未分配分片的标准流程# 1. 看哪些分片没分配 GET _cat/shards?vhindex,shard,prirep,state,unassigned.reason # 2. 让 ES 自己解释原因 GET _cluster/allocation/explain常见原因和对应处理未分配原因典型场景处理方式NODE_LEFT节点下线等delayed_timeout默认 1 分钟后自动分配DISK_THRESHOLD_EXCEEDED磁盘水位超 85%清理数据或扩容或临时调高水位ALLOCATION_FAILED分配重试次数耗尽用retry_failedtrue重试NO_VALID_SHARD_COPY主分片数据丢失需要从快照恢复INDEX_CREATED刚建索引还没分配等待或检查节点属性NODE_LEFT对应的index.unassigned.node_left.delayed_timeout这个参数很实用。默认 1 分钟意味着节点短暂重启时ES 会等 1 分钟再开始搬分片。如果没有这个延迟每次滚动重启都会触发大规模分片搬迁等节点回来了又要搬回去白白消耗大量 I/O。生产环境我一般调到 5 到 10 分钟。磁盘水位也值得说清楚。ES 有三档水位low85%不再分配新分片、high90%开始尝试把分片迁走、flood_stage95%索引被设为只读。第三档最麻烦——索引变成read_only_allow_delete写入全部失败需要手动解除PUT /_all/_settings { index.blocks.read_only_allow_delete: null }这个设计初衷是防止磁盘写满导致节点崩溃但触发时如果没人盯着业务会直接中断。所以监控里必须给磁盘水位单独设告警阈值要比 85% 更低比如 75% 就报警留出处理时间。5.2 选主机制与最小投票数为什么不能随便凑节点ES 的集群有一个 master 节点负责管理集群状态索引元数据、分片分配决策、节点上下线。master 不参与数据读写但它的稳定性决定了整个集群的稳定性。选主用的是类 Raft 的投票机制。节点启动时通过discovery.seed_hosts找到其他节点通过cluster.initial_master_nodes完成首次引导。一旦集群成型投票配置由集群自己维护不再依赖配置文件。老版本7.0 之前有个discovery.zen.minimum_master_nodes参数要求手动设成(master 候选节点数 / 2) 1防止脑裂。7.0 之后这个参数被移除了由系统自动维护投票配置配置负担小了很多。但原理还是要懂任何决策都需要超过半数的投票节点同意所以候选节点数最好是奇数3、5、7偶数节点并不能提升容错能力反而增加通信开销。专用 master 节点的配置大概长这样node.roles: [ master ] discovery.seed_hosts: [10.0.0.1:9300, 10.0.0.2:9300, 10.0.0.3:9300] cluster.initial_master_nodes: [master-1, master-2, master-3]三个专用 master 是性价比最高的配置能容忍一台挂掉选主仍然可以完成。五个能容忍两台但大多数场景三个就够。不要让数据节点兼任 master尤其是负载高的大集群——数据节点 GC 停顿会导致心跳超时进而触发不必要的选主集群状态反复震荡。还有一个容易忽略的点cluster.initial_master_nodes只在集群首次启动时使用。集群成型后这个配置要删掉否则某次全量重启时可能造成节点各自为政。这个坑我踩过滚动重启时忘了清理配置结果重启后有节点试图重新引导集群虽然最终没出事但集群状态抖了十几分钟。5.3 扩容时的分片搬迁限流参数比机器数量更重要加节点之后ES 会自动把部分分片迁移到新节点上这个过程叫 rebalance。默认配置下每个节点同时进行的恢复任务数是 2cluster.routing.allocation.node_concurrent_recoveries每节点恢复带宽限制是 40MB/sindices.recovery.max_bytes_per_sec。这两个默认值在中小集群里够用但在大集群扩容时往往会成为瓶颈。比如一次加 6 台机器每台要接收几十个分片按 40MB/s 的速率一个 30GB 的分片要搬 12 分钟串行下来要几个小时。期间集群处于部分分片在迁移的状态查询可能变慢。调大限流能加速但要小心别把线上读写挤死PUT /_cluster/settings { persistent: { indices.recovery.max_bytes_per_sec: 200mb, cluster.routing.allocation.node_concurrent_recoveries: 4 } }我一般会在业务低峰期临时调高扩容完成后调回默认值。判断标准是看_cat/recovery的进度和节点 I/OGET _cat/recovery?vactive_onlytruehindex,shard,source_node,target_node,bytes_percent,time如果bytes_percent长时间不动可能是目标节点磁盘写不过来这时候调高并发反而更糟。扩容不是加机器这么简单搬迁速率、磁盘带宽、查询影响三者要一起权衡。6. 一次真实的扩容翻车从现象到根因的完整排查链路前面讲的都是机制这一节还原一次完整的排查过程。写这部分是因为很多同学遇到问题时不知道从哪下手缺的不是知识点而是排查顺序。6.1 现象扩完节点延迟反而涨了三倍场景是这样一个日志检索服务原有 3 个数据节点每天写入约 800GB索引每天一个每个索引 6 个主分片 1 个副本。随着数据量上涨磁盘快满了于是加了 3 个节点变成 6 节点。扩容操作本身很顺利节点都起来了_cat/nodes显示 6 个节点全部在线。但接下来的两个小时里查询 p99 从 200ms 涨到了 700ms写入开始出现零星拒绝。第一反应是扩容过程中搬迁影响了性能这确实是常见原因但等了三个小时搬迁完成后延迟没有回落。这时候才开始正式排查。第一步看集群状态GET _cluster/health GET _cat/indices?vhindex,health,pri,rep,docs.count,store.size状态是绿色没有未分配分片说明不是分配问题。第二步看节点负载GET _cat/nodes?vhname,heap.percent,cpu,load_1m,disk.used_percent发现三台老节点的 heap 都到了 75% 以上三台新节点只有 40% 左右。CPU 也是老节点明显更高。这个分布非常可疑——分片应该均匀分配才对。第三步看分片分布GET _cat/shards?vhindex,shard,prirep,node,store问题浮出水面每天的索引都是 6 个主分片分到 6 个节点上理论上应该一节点一个。但因为索引是按天创建的创建时的节点数和分配策略决定了分片落在哪加上部分索引只有 6 个分片而节点有 6 个导致有些老索引的分片全落在老节点上新节点根本没分到老索引的数据。新节点只承载了新增的索引,老索引的查询压力全部压在 3 台老机器上。6.2 逐层排除线程池、merge、还是热点分片确认了分布不均还要排除其他叠加因素因为在真实场景里问题很少是单一原因造成的。查线程池GET _cat/thread_pool/write,search?vhnode_name,name,active,queue,rejected老节点的search.queue有持续堆积rejected有小幅增长新节点则很空闲。这印证了查询压力集中在老节点的判断。查热点分片GET _cat/segments?vhindex,shard,segment,size,docs.count发现前一天的索引里有几个分片的段数量特别多超过 50 个其他分片只有十几个。段多的分片查询自然慢这也是分布式查询里长尾的来源之一——协调节点要等最慢的分片一个热点分片就能拖慢整个查询。查 merge 状态GET _cat/thread_pool/force_merge?v没在跑。但看节点 I/O 的时候发现老节点在扩容期间经历了大量分片搬迁搬出去了搬迁完成后又在做 merge因为搬迁过来的分片要重新整理段。两个 I/O 密集型任务叠在一起把老节点的磁盘带宽吃掉了。到这里根因链条清楚了分片按索引创建时分配老索引的分片集中在老节点新节点参与不进来。老节点承载了大部分历史数据查询同时还在做分片迁出。迁出完成后接收方节点做 merge又反过来影响整体 I/O。查询长尾叠加p99 被放大三倍。6.3 修复方案与验证三步落地修复不是再调几个参数能解决的得从分配层面动手。第一步让老索引的分片真正均匀分布。用的是分片分配过滤临时把某个老索引的分片强制迁到新节点上PUT /logs_2024_01/_settings { index.routing.allocation.include._name: new-node-1,new-node-2,new-node-3, index.routing.allocation.total_shards_per_node: 2 }这一步是定向搬迁比等自动 rebalance 快得多。搬完之后把设置清掉PUT /logs_2024_01/_settings { index.routing.allocation.include._name: null, index.routing.allocation.total_shards_per_node: null }total_shards_per_node这个参数值得记住它能硬性限制单个节点上某个索引的分片数防止分片挤在一台机器上。日常建索引时就该加上比如每个索引 6 个分片、6 个节点设成 2 就有余量。第二步治理长尾分片。对已经封存的只读索引做 forcemerge把段数压到 1消掉段数量不均带来的长尾POST /logs_2024_01/_forcemerge?max_num_segments1wait_for_completionfalse这一批操作要串行做一个索引一个索引来避免同时触发大量 I/O。第三步调整写入侧参数降低整体的 merge 压力。把当天的索引refresh_interval从 1s 改成 10s写入吞吐没有明显下降因为日志场景对实时性要求不高但段生成速度降了一个数量级。三步做完观察了 24 小时p99 回到 220ms 左右写入拒绝归零。但这个结果不是调参调出来的而是先把分片分布这个结构性问题解决了参数优化才有意义。如果顺序反过来先调refresh_interval可能短期指标好看一点但老节点的压力依然存在过几天又会复发。后来这个集群做了两个长期改进一是索引模板里固定加上total_shards_per_node和routing.allocation.awareness让分片在机架级别打散二是把索引从每天一个改成按数据量滚动比如单个索引超过 50GB 就滚动避免数据量波动导致分片大小差异过大。我个人在实际操作中的体会是分布式搜索的大部分性能问题最后都能归结到数据分布和资源竞争这两件事上。参数能调但调参数之前先看一眼_cat/shards和_cat/segments往往比翻十页文档更管用。另外再分享一个小技巧给所有关键查询加上profile: true跑一次看query和fetch两个阶段各花了多少时间如果 fetch 占比很高说明是取_source拖慢的可以考虑用_source过滤只取必要字段这一步在大宽表场景里经常能省掉一半响应时间。
延伸阅读

更多相关文章

2026/11/6 14:51:20

帝国CMS接入微信JS-SDK实现分享缩略图与简介

简介:这份源码面向使用帝国CMS 7.5 UTF8版搭建内容站点的站长与开发者,用于解决微信内分享文章时朋友圈和转发好友不显示缩略图、简介缺失的问题。压缩包共6个文件,以php脚本、txt安装说明、js前端脚本和jpg示意图为主,整体约31KB…

2026/11/6 15:03:51

Elasticsearch分布式搜索:分片、写入、查询与生产实践

上一篇把 ES 的基本盘铺完了:倒排索引怎么建、文档怎么落盘、节点有哪几种角色。但真上手做项目就会发现,那些知识只够让一个单机实例跑起来。数据量一过千 GB、节点数超过三台,问题立刻换了一副面孔:明明返回result: created却查…

2026/11/6 15:14:32

C#交互式绘图:图元、坐标变换与鼠标状态机实战

简介:这份C#交互式绘图代码资源面向具备一定C#基础、希望掌握GDI图形编程的开发者,通过完整项目实例解决窗体绘图与用户交互结合的实际问题。压缩包共383个文件,约1.17MB,以84个cs源码文件为核心,配合22个resx与24个re…

2026/11/7 0:28:09

龙游县电工证怎么考网上查询

龙游县电工证怎么考网上查询 你是不是学历不高,担心自己考不了龙游县电工证?别急,这期内容就给你说清楚怎么搞定这个证,还有网上查询怎么操作,别让学历成为你上手电工行业的绊脚石。 一、电工证在龙游县的报考条件有哪些? 先说重点: 学历不是门槛…

2026/11/7 0:28:09

电力电缆证在哪复审多少钱?实操考试心里没底怕挂科

电力电缆证在哪复审多少钱?实操考试心里没底怕挂科 实操考试心里没底怕挂科,你不是一个人。很多人第一次报考电力电缆证时,都担心自己操作不熟练,一不小心就挂科。尤其是在河南,像商丘这样的城市,电力电缆作业是市政工程里最常见、最基础的岗位之一,但…

2026/11/7 0:28:09

隆回电工证培训机构排名曝光:真实经历告诉你怎么避坑

隆回电工证培训机构排名曝光:真实经历告诉你怎么避坑 怕考不过白交培训费?我身边就有人因为选错培训机构,花了几千块最后连考试都没通过。别急,这篇讲的是隆回电工证培训机构排名的干货,全是真实经历,帮你少走弯路。…

2026/11/7 0:28:09

无操作证从事特种作业多少钱?工地太忙根本没时间复习考试

无操作证从事特种作业多少钱?工地太忙根本没时间复习考试 工地太忙根本没时间复习考试,你是不是也经常在工地上头昏脑涨,连手机都顾不上看?更别提抽时间去复习特种作业操作证的考试内容了。很多人觉得, 无操作证从事特种作业…

2026/11/7 0:28:09

大专如何报考电工证多久拿证 证书过期了不知道怎么复审延期

大专如何报考电工证多久拿证 证书过期了不知道怎么复审延期 证书过期了不知道怎么复审延期,这个问题在洛阳涧西区不少转岗从业者身上都出现过。尤其是持有电工证的朋友,很多人在换工作、换行业后,忽视了证书的有效期和复审时间,导致证书失效,影响了后续…

2026/11/7 0:23:09

自贡电工证报考多久拿证?学历不高怕报不上名怎么解决

自贡电工证报考多久拿证?学历不高怕报不上名怎么解决 你是不是也这样想:“我学历不高,连初中都没读完,还能不能报考自贡电工证?”这个问题,我在安阳的一个劳务班组里遇到过,他们班组长老张,就是典型的“半文盲”,但照样考到了低压电工证,现在成了项…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/6 10:49:01

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/7 0:03:08

郑州报考避坑指南:电工证和钳工证怎么选?实操考试心里没底怕挂科

郑州报考避坑指南:电工证和钳工证怎么选?实操考试心里没底怕挂科 实操考试心里没底怕挂科?你不是一个人。特别是在郑州这样的城市,很多施工企业负责人和一线作业人员在报考电工证和钳工证时都面临着同样的困惑。到底该考哪个证?哪个更适合自己?哪个证书…

2026/11/7 0:03:08

广西电工证办理流程全解析:外省证怎么转?在哪考最靠谱?

广西电工证办理流程全解析:外省证怎么转?在哪考最靠谱? 之前考的证在外省能不能转过来?这个问题我遇到太多了,今天就拿广西电工证办理流程来具体说说,别再走弯路了。广西电工证的报考、复审、继续教育这些环节,如果你不了解,真的会吃大亏,尤其对于从…

2026/11/7 0:03:08

电气电工证都有哪些保姆级教程

电气电工证都有哪些保姆级教程 你是不是正愁不知道去哪报名,怕被中介坑?别急,这篇保姆级教程专为河南初次报考人员准备,帮你搞清楚 电气电工证都有哪些 ,以及如何避开那些坑。 电气电工证都有哪些?别被五花八门的证书搞懵了…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/6 10:49:01

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑