
前几天处理了一个 Elasticsearch 集群变红的问题光是健康状态从 red 恢复到 99% 这段前后折腾了四个多小时。中间最诡异的点是明明恢复进度眼看着要走到 100%节点却自动重启然后一堆分片重新变成 unassigned状态又被打回原形。这种问题在实际运维里非常典型尤其分片数量多、节点内存又不算宽裕的集群几乎每隔一阵就会冒出来一次。我先把结论放在前面这次问题的直接原因是节点在分片恢复的最后阶段内存达到峰值被操作系统 OOM 杀掉而“自动重启”只是守护进程的常规拉起动作。但真正让集群长期无法变绿的原因却藏在分配器的判定逻辑和磁盘水位的连锁反应里。所以这篇文章不打算只讲一个案例而是把排查思路、判断依据、修复顺序和几个容易踩的坑一起写清楚给你一份可以直接照着用的排障手册。无论你是刚接手 ES 集群的运维还是已经在处理 unassigned shard 的问题这篇内容应该都能帮你少走弯路。我会尽量按实际操作的顺序来讲命令和参数以 7.x 版本为例8.x 上基本通用差异点会单独说明。1. 先做判读集群红色到底在告诉你什么1.1 红、黄、绿的本质很多刚接触 Elasticsearch 的朋友会把集群颜色当成一个简单的“健康灯”其实它背后定义非常具体绿色表示所有主分片和副本分片都分配成功黄色表示主分片都分配了但至少有一个副本分片没有分配红色表示至少有一个主分片没有分配。也就是说只要出现红色就意味着部分索引已经不可读写这不是“有点问题”而是“出了问题”。这次我遇到的集群当时有三台数据节点某个核心业务索引的主分片状态是 UNASSIGNED所以整个集群状态一直停留在红色。当时第一反应是看看分片是不是在节点迁移或者重启过程中被“丢”了但实际查下来发现情况比想的复杂分片问题只是一个表象真正的原因在节点反复重启。这里有个经验看到红色以后不要急着 reroute先搞清楚 unassigned 的 reason 和节点本身的实时状态。否则你强制分配一个不稳定的副本过去勉强把颜色变绿等到节点再挂一次问题会以更难看的方式反弹回来。1.2 为什么 99% 的进度是烟雾弹这次最让人迷惑的一点就是健康检查里看着分片恢复进度已经到 99%结果节点一重启全部归零。我盯着那串进度数字看了十几分钟一度以为是资源不够导致恢复变慢后来才意识到方向错了。ES 显示出来的“恢复进度”默认看的是一个分片从源节点拷贝到目标节点的文件传输进度。当它显示 99% 时说明绝大多数文件块已经拷完剩下的工作包括校验、写 translog、刷新、维护 Lucene 底层数据结构。这个阶段恰恰是内存和 IO 压力快速上升的窗口期。所以“到 99% 就自动重启”通常不是恢复逻辑本身出了问题而是节点在这个峰值点撑不住了。如果你只盯着分片进度去调并发不去查节点内存和系统日志大概率会像我第一次排查那样一无所获。1.3 第一轮信息采集把状态固化下来我处理这类问题的习惯是先别急着改配置先把现场信息完整抓一遍。以下这组命令是我这次实际执行的也是后面所有判断的基础# 集群整体状态重点看 active_shards、relocating、initializing、unassigned GET _cluster/health?pretty # 所有分片分布带 unassigned.reason 字段最直观 GET _cat/shards?vsindexhindex,shard,prirep,state,node,unassigned.reason # 每个节点上的分片数量方便判断是否严重倾斜 GET _cat/allocation?v # 恢复进度active_onlytrue 时看正在传的分片 GET _cat/recovery?vactive_onlytrue # 节点级别统计OS、进程、JVM、磁盘全靠它 GET _nodes/stats/os,process,jvm,fs # 分配器对未分配分片的解释这是最重要的命令没有之一 GET _cluster/allocation/explain?pretty在终端里执行完后我把输出全部存到了本地文件里。后面每做一步操作都会再对比一次这些基线数据。如果没有这个习惯你很容易在连续重启之后忘记某个分片一开始是在哪台节点上的那判断就会越来越乱。2. 最容易背锅的磁盘高水位和只读索引2.1 磁盘水位的三档机制ES 的磁盘分配策略是默认开启的它根据节点磁盘使用率来做三档限制低水位默认 85%高水位默认 90%洪水水位默认 95%。超过低水位时ES 就不再往该节点分配新分片超过高水位时会尝试把该节点的分片迁走超过洪水水位时所有索引会被强制设为只读防止磁盘被彻底写爆。很多人对这套机制的印象是“磁盘快满了所以不分片”实际上它惩罚的范围远比想象中大。当某个节点磁盘超过高水位它上面已有的分片不会被立刻删除但它们会被标记为不可作为分配目标。如果此时另一个节点发生了主分片丢失分配器在找新位置时可能把所有节点都判定为“不适合落盘”最终结果就是分片一直悬在 UNASSIGNED。2.2 我这次查磁盘的过程排查开始时我先看了_cat/allocation发现有一台节点的磁盘使用率已经到 91%另一台在 78%第三台在 63%。第一反应是“破案了高水位导致分片分配不了”。于是赶紧去清理磁盘删了一批归档索引又把磁盘使用率压回 80% 以下。清完磁盘后集群确实消停了几分钟但紧接着又出现新的异常那个 91% 的节点上ES 进程直接退出了然后又自动重启。这时候我就知道问题没这么简单。磁盘高水位可能是压垮进程的间接原因但不会直接把 java 进程杀掉。如果是磁盘写满ES 会抛 IOException而不是 OOM 或进程消失。用df -h确认之后发现清理完的磁盘还剩下 15% 左右并没有再次冲到洪水水位。于是我暂时把“纯磁盘问题”这个结论挂起转向系统日志和 JVM 日志。2.3 高位磁盘引发的传导效应这里要提醒一句磁盘高水位在分片恢复场景里有个容易被忽略的副作用。当某个节点磁盘超过 90%它上面大量的副本分片会进入“不可分配”状态RAM 中的数据又不能释放ES 会不断重试向其它节点分配这些分片造成一堆无意义的网络传输和临时文件写入。在我这次的问题里那台 91% 的节点实际上是从节点它上面堆积了大量恢复中间文件而主分片又需要在另一个节点上重建。磁盘高水位导致分配器把大量分片压到了剩余两台节点上那两台节点的内存和 IO 同时升高最终成为 OOM 的导火索。所以磁盘问题不是“清掉空间就完事”还要观察清理后分片数是否自动均衡、恢复流量是否给其它节点带来过载。如果只看一次df -h就收工后面极容易被“恢复到一半节点重启”打脸。3. 真凶慢慢浮出节点反复重启的内存问题3.1 从系统日志里发现 OOM 记录当节点自动重启时第一件事不是打开 ES 日志看报错而是去翻系统日志和 dmesg确认进程是不是被内核杀的。我这边的排查步骤如下# 查看系统消息重点关注与 java 或 elasticsearch 相关的日志 journalctl -u elasticsearch --since 2 hours ago | tail -200 # 查看内核 OOM 记录 dmesg -T | grep -i oom | tail -50在 dmesg 里我看到了一行非常刺眼的记录某个时刻内存不足内核选择了一个 java 进程杀掉PID 对应的正是那台节点上的 ES。得知这个信息后我立刻意识到之前看着“恢复 99%”再重启不是偶发现象而是内存压力下必然的结果。为什么内核选中的是 ES因为 Linux 的 OOM Killer 会综合进程的内存占用、运行时长等因素打分ES 作为这个节点上最吃内存的常驻进程几乎每次都在击杀名单前列。你没办法责怪操作系统只能从内存规划上找问题。3.2 为什么偏偏在恢复 99% 时被 OOM我在前面说过恢复进度到 99% 时分片正在做最后的校验和 translog 回放。这时候 JVM 堆内用于索引写入的 buffer、Lucene 底层用于排序和过滤的数据结构、以及文件系统缓存都会在一个很短的窗口里迅速膨胀。如果平时节点内存余量比较多这个峰值不会造成问题。但在分片数量大、节点上同时有多个副本在恢复的情况下峰值可能比正常运行高出 30% 甚至更多。再加上“恢复进度已经显示 99%”让你下意识觉得压力不大结果就是最容易放松警惕的时候出事。我这次的问题里三台节点总共要恢复一百多个分片其中约三分之二都堆在两台目标节点上。恢复并发没有调低磁盘 IO 又跟不上导致写入速度变慢JVM 里的数据积压越攒越多。最后系统内存耗尽内核直接把 ES kill 掉整个恢复功亏一篑。3.3 JVM 堆、堆外内存和 Linux 内存的三角关系很多刚接触 ES 的人会以为只要把Xms和Xmx设置成 32G节点内存就不够就“加堆”就行。但实际上 JVM 堆只是 Elasticsearch 内存使用的一部分还有大量内存被 Lucene 的段文件、页缓存、translog 等占用这部分叫做堆外内存。以一台物理内存 64G 的节点为例如果你把 JVM 堆设置为 31G那么留给操作系统和文件系统缓存的就只有 33G其中还要算上ES 自己映射的 Lucene 文件。分片恢复时源数据要读进页缓存目标数据也要写入页缓存这个过程中文件缓存占用会快速上升。堆和堆外同时竞争才容易出现整机内存耗尽。我这次集群的节点JVM 堆设置的是 30G物理内存总共 48Gsystemd 启动脚本里又没有做bootstrap.memory_lock之类的限制。正常运行或许没问题但恢复峰值时堆外内存一膨胀整机直接 OOM。后来我做了调整把堆降到 26G并开启了内存锁定节点的稳定性明显提升。3.4 节点被 kill 后的连锁反应一个节点被 OOM Killer 杀掉后集群层面会发生一系列连锁反应master 节点标记该节点失联它上面承载的主分片变成 UNASSIGNED副本分片等待重新分配。然后 systemd 或者其它守护进程会把 ES 重新拉起节点重新加入集群。这个“重新加入”的过程非常关键。节点启动时会去扫描本地磁盘上的分片目录尝试恢复它之前持有的分片。如果恢复过程中再次 OOM就会再次重启。在集群看来这个节点就像是一个“来了又走、走了又来”的幽灵节点分配器为了稳定往往会推迟给它分配新的分片甚至因为重试次数过多把一个分片标记为“分配失败”必须手动 retry 才能再次尝试。所以我后来越发确认先解决进程稳定性再谈分片分配顺序上不能反过来。4. 未分配分片的真实裁判分配器怎么说4.1 先别 reroute先让分配器解释遇到 unassigned 分片新手最容易做的一件事就是手动 reroute把分片强制挪到某个节点。这是最危险的姿势之一。你不清楚分配器拒绝分配的原因就强行覆盖判定轻则分片反复迁移重则损坏副本数据。ES 提供了一个专门用来“解释”的命令GET _cluster/allocation/explain。它可以告诉你某个分片为什么未分配、分配器做了哪些决策、被哪个 decider 拒绝。这次问题排查里这个命令直接帮我锁定了两个原因一个是节点重启后本地分片状态不干净另一个是同一节点上已经存在该分片的副本分配器要求“不能把主分片和副本放到同一个节点”。GET _cluster/allocation/explain?pretty { index: your-index-name, shard: 1, primary: true }请求返回的信息很丰富重点看unassigned_info.reason、last_allocation_status、decide_explanation这几个字段。它会把每个候选节点的判断都说出来比如磁盘水位、分片总数限制、awareness 属性不匹配、same_shard 冲突等。4.2 常见未分配原因和对应处理思路根据我见过的大量案例ES 的 unassigned.reason 最常见的有这么几类我整理成了一个小表unassigned.reason含义优先处理方向NODE_LEFT持有分片的节点离开集群先等节点回来确认本地分片状态INITIALIZING分配后初始化失败用 retry_failed 重试必要时清理损坏分片PRIMARY_FAILED主分片所在节点故障优先恢复节点或重建主分片REPLICA_ADDED新副本刚被创建一般等待自动分配即可CLUSTER_RECOVERED全集群重启后的恢复关注磁盘水位和恢复并发ALLOCATION_FAILED多次分配失败查 explain通常需要 retry 或清缓存DANGLING_INDEX_IMPORTED导入孤儿索引主动确认索引归属后再处理注意reason 只是告诉你“为什么会进入 unassigned”真正决定能不能分配的是各种 decider。比如same_shard会说“同一节点上已存在该分片的副本”disk_usage会说“节点磁盘超过水位”throttle会说“节点正在恢复太多分片”。这些信息在 explain 里都会出现一定要逐条看。4.3 我这次实际看到的决策过程回到我的场景。清理磁盘后我重新执行 explain发现某个未分配主分片的反馈大致是这样当前节点因为磁盘水位和历史分片损坏不能当选另一节点因为已经有同一分片的副本被排除再一个节点因为正在处理大量迁入分片被节流。这意味着什么问题意味着我没有必要去手动 reroute只要让磁盘水位彻底降下来、等节点恢复稳定、然后重试分配分片自然能落到合适位置。如果当时脑子一热强制 reroute 到那个已有副本的节点ES 会发现“同一分片的主副本放在同一节点”从而拒绝而且会把分配器的状态搞得更混乱。所以我对_cluster/allocation/explain的定位是它像是法庭上的证人证词你必须先听它把话说清楚。不要在没有解释的情况下执行任何强制分配动作。4.4 清理新旧分片副本时的注意事项有时 explain 会告诉你某个节点上的分片副本“损坏”或“不可恢复”。这种时候可以先把节点上的旧分片副本挪走或删除再让 ES 重新分配。操作时我会非常谨慎遵循下面的顺序先把索引设为只读防止新的写入污染数据确认该索引有其它可用副本至少保留一份健康副本用_cluster/reroute加retry_failedtrue重试看能不能自动分配如果仍然失败再考虑关闭索引、删除损坏分片、重新恢复。删除节点数据目录里某个索引的分片文件夹是最后手段不是第一选择。因为在没有完整备份的情况下一旦删错数据就真的没了。我亲眼见过有人因为图省事直接rm -rf分片目录然后发现所有副本都是坏的导致索引彻底无法恢复。5. 完整修复清单从“稳住进程”到“放行恢复”5.1 第一步先把机器和进程稳定住我在这次问题里踩了一个比较重要的坑一开始只想着尽快把 unassigned 分片清掉结果把恢复并发调大了反而加重了内存压力。后来我从头梳理定下“先稳定节点再恢复分片”的原则具体操作如下。先调整 JVM 堆参数把不合理的 30G 降下来。不要小看这一步对 48G 物理内存来说堆外留 18G 是非常紧绷的。我改成-Xms24g -Xmx24g并且加上内存锁定参数让 ES 进程尽量不被换出内存# 在 jvm.options.d 里新增文件 memory-lock.options -Xms24g -Xmx24g然后在 elasticsearch.yml 里开启bootstrap.memory_lock: true关于bootstrap.memory_lock多说一句。它主要解决的是交换分区导致的性能悬崖但对 OOM 场景也有帮助内存被锁住后ES 进程的内存使用更稳定系统不容易因为频繁 swap 产生额外的内存碎片。开启后如果节点启动失败查看日志里关于 mlockall 的提示一般就是操作系统对锁定内存的限制问题。还有一个重要的操作检查 systemd 或者容器平台对节点的重启策略。如果进程一挂就被立即拉起可能造成连续重启连正常恢复的窗口都没有。建议将其设置为开机自启但不要设置无限次快速重启。至少让我在操作的时候能有足够时间看日志和抓现场。5.2 第二步临时压低恢复对系统的影响节点稳定后先不要一次性放开全部恢复流。我是先把恢复相关参数调小让节点先喘口气然后再逐步加大。具体修改如下PUT _cluster/settings { transient: { cluster.routing.allocation.node_concurrent_incoming_recoveries: 2, cluster.routing.allocation.node_concurrent_outgoing_recoveries: 2, indices.recovery.max_bytes_per_sec: 20mb, cluster.routing.allocation.cluster_concurrent_rebalance: 2 } }这里需要说明如果你的版本比较老node_concurrent_incoming_recoveries和outgoing可能合并成node_concurrent_recoveries具体以文档为准。对于磁盘 IO 较差的环境我更倾向于先用20mb限速观察节点内存曲线稳定后再逐步提到40mb或更高。同时我把延迟分配时间调大了一点目的是让刚重启的节点有充分时间重新加入避免 master 因为等不到它就激进地把分片分配到其它节点PUT _cluster/settings { transient: { index.unassigned.node_left.delayed_timeout: 10m } }这个参数默认只有一分钟对于内存抖动频繁的集群太短了。调大之后即使节点意外退出集群也会给分片一个等待期不会立刻触发大规模迁移。5.3 第三步处理残留分片与重试分配在确定节点稳定运行了大约十五分钟之后我才开始处理未分配分片。先把 explain 再跑一遍确认没有磁盘水位、节点数量限制等硬性条件阻碍分配。如果有先解决如果没有执行一次强制重试POST _cluster/reroute?retry_failedtrue注意这个命令不是“手动指定分片去哪”而是“让分配器把之前失败的状态清掉重新尝试分配”。它比具体的 move/reallocate 操作安全得多。执行完再观察_cluster/health如果 unassigned 数量开始下降说明方向正确。如果某个索引的 unassigned 分片一直顽固我会看它是不是卡在初始化阶段。对确实无法恢复的分片可以选择从一个健康副本重建主分片。这个操作要结合业务情况和备份策略来判断并且最好在业务低峰期做。5.4 第四步验证并恢复日常参数当集群健康状态从 red 回到 yellow最后再回到 green 后不要觉得自己完事了。我这次就是在“回到 green”之后又花了不少时间做参数复位。把临时调低的恢复并发调回默认把max_bytes_per_sec调回我们团队日常的值把延迟分配时间调回 1 分钟或者按业务需求设成 5 分钟。同时在 Kibana 或监控面板里观察接下来两小时的磁盘增长、堆内存使用和分片迁移情况。这里特别要留意一个事情恢复结束后ES 会进入 merge 阶段磁盘占用可能不降反升这不是故障而是 Lucene 在做段合并等它自然收敛就好。我这次再确认 green 状态后还跑了一次全索引健康检查把之前因为磁盘洪水水位变成只读的索引手动解除只读PUT your-index-name/_settings { index.blocks.read_only_allow_delete: null }如果这一步忘了做业务层写入请求会一直报错但集群健康显示却没问题非常容易被忽略。6. 复盘总结这类问题的共性规律和监控防线6.1 一条连锁反应链经过复盘这次故障从头到尾其实是一条完整的链条磁盘空间偏高触发高水位分配器把分片集中压到其它节点恢复并发让内存峰值提高节点内存不足被内核 OOM守护进程自动拉起节点重启后的节点反复参与分配造成资源浪费和分配失败最后表现为集群 red、恢复 99% 时重启、unassigned 分片堆积。我以前处理类似问题总是盯着链条的某一个点打比如疯狂清磁盘、或者反复 reroute 分片结果问题总在别处冒头。现在我的习惯是先把链条拉通问自己三个问题哪个环节是起点哪一步加剧了问题哪些参数能降低恢复阶段的风险这三个问题想清楚排障思路会清晰很多。6.2 监控告警里真正值得看的指标如果只有一两个监控指标我建议优先盯这四个集群健康状态、节点磁盘水位、JVM 堆使用率、系统内存可用量。这四个指标几乎能覆盖我这次故障的所有早期信号。磁盘水位超过 85% 时要准备清理超过 90% 时必须立刻处理。系统内存可用量持续下降时不要指望它自己回升去查是不是有大字段查询、分片恢复或者 merge 在吃内存。JVM heap 使用率也要看趋势不要只看瞬时值GC 日志里的频繁 Full GC 往往比堆占用率更早暴露问题。另外对节点“自动重启”要有告警。别让它成为无人感知的背景噪音。我的经验是进程重启超过 2 次/小时就必须人工介入而不是自动拉起后听之任之。6.3 踩过几次坑之后我总结的排查顺序把这次经验和以前遇到过的类似问题放在一起我总结了一份比较稳定的排查顺序。以后再碰到“ES 集群红色 分片未分配 节点重启”的时候可以按这个顺序走第一轮抓集群健康、分片分布、节点磁盘和节点内存三十秒内先做到“知道发生了什么”。如果进程已经重启立刻翻dmesg和 systemd 日志确认是否 OOM。第二轮根据_cluster/allocation/explain理解分配器判定不要手工强制分配。如果有磁盘水位问题清理空间等水位降下来。第三轮调整恢复并发和延迟分配让系统先稳定。不要一次性放开全部流量逐步试探。第四轮用retry_failedtrue触发第二轮分配观察 unassigned 数量下降。此时如果节点还再重启回到第一轮重新排查。第五轮全部 green 后恢复参数、解除只读索引、继续观察 merge 阶段。这套顺序我用了很长一段时间基本把之前那种“东一榔头西一棒子”的排障方式彻底改掉了。6.4 最后再分享一个小技巧很多人在排查分片恢复慢时花费大量时间盯_cat/recovery却忽略了节点的thread_pool和 IO 等待。如果你发现恢复进度卡住、节点反复重启但 JVM 堆又不像满的时候记得看一眼系统层面的 IO 指标。恢复阶段的瓶颈往往是磁盘 IOPS而不是 CPU 或网络。我这次就是因为在 99% 进度阶段多看了几眼 IO 数据才更确认内存和磁盘其实是相互作用的。根据我个人的经验ES 集群恢复阶段就好比一辆满载的卡车在爬坡你给它加再多的油门并发路况不好也一样会憋熄火。先把路修平也就是稳住磁盘水位和内存余量再踩油门放恢复并发才能真正顺畅地走到绿色状态。相关监控和参数调优最好在平时就固化下来别等故障来了再临场发挥。