ARTICLE DETAIL

资讯详情

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

K8s中Hadoop Datanode故障排查:探针失败与磁盘满根因

K8s中Hadoop Datanode故障排查:探针失败与磁盘满根因 周四下午我正在盯着k8s集群的监控面板突然收到Hadoop集群一条告警某个datanode掉线了。这已经不是第一次处理这种问题但每次的诱因都不完全一样。最开始我接到这类故障的第一反应是直接重启pod但后来发现——重启往往只能解决表象如果不找到根因过两天同样的故障还会换个花样再来一遍。这篇文章我准备把一次完整的k8s集群中Hadoop datanode故障排查过程整理出来从最初的pod状态检查到最终的根因定位与修复方案每一步怎么查、怎么看输出、怎么排除干扰因素都会讲清楚。如果你正在维护一个运行在k8s上的Hadoop集群或者正打算把Hadoop集群往容器化方向迁移这篇文章里的排查思路和实操命令应该能帮你省不少时间。1. 故障现场与第一波排查现象往往比想象中复杂1.1 从“一个datanode掉线”说起的实际故障现象先说下我们当时的集群状况。一套运行了大半年的Hadoop集群以StatefulSet方式部署在k8s里共有5个datanode节点每个datanode对应一个pod底层数据目录用的是节点本地SSD。整体架构不算复杂但足够覆盖生产使用场景。告警触发点有两个第一是k8s侧datanode的某个pod状态变成了CrashLoopBackOff第二是HDFS侧监控脚本检测到存活datanode数量从5降到了4。两边的告警几乎同时过来说明故障不只是容器层面的已经影响到了数据面。这里先说一个我从实践中得到的判断标准如果只是pod状态异常但HDFS侧没有告警问题往往出在容器生命周期上如果两边同时告警那基本可以确认是datanode进程本身真的出了问题。处理优先级上我会先从HDFS侧切入因为数据安全是整个集群的底线。1.2 kubectl视角下的pod状态先看表象再做判断第一波排查无非是那三板斧看状态、看事件、看日志。我先执行了$ kubectl get pods -n hadoop -o wide NAME READY STATUS RESTARTS AGE datanode-0 1/1 Running 0 45d datanode-1 0/1 CrashLoopBackOff 3 45d datanode-2 1/1 Running 0 45d datanode-3 1/1 Running 0 45d datanode-4 1/1 Running 0 45d问题pod是datanode-1RESTARTS次数是3状态CrashLoopBackOff。这个状态说明kubelet一直在尝试重启容器但每次起来后不久就退出。CrashLoopBackOff本身不告诉你是进程崩溃还是探针失败必须往下看日志和事件才能区分。紧接着查describe$ kubectl describe pod datanode-1 -n hadoop ... Events: Warning Unhealthy Liveness probe failed: HTTP probe failed with statuscode: 500 Warning Unhealthy Readiness probe failed: HTTP probe failed with statuscode: 500 Warning BackOff Back-off restarting failed containerEvent里出现了两个关键信息Liveness probe failed和Readiness probe failed状态码500。Hadoop里HTTP状态码500通常意味着datanode的web服务还活着但健康状态检查不过——比如磁盘空间不足、线程池阻塞等情况。这里已经能初步排除“进程没起来”这种最简单的情况问题大概率出在运行环境层面。1.3 第一波排查中容易忽略的warning与event很多人看到CrashLoopBackOff就直接重启pod或删掉重建我不建议这么做。在删除或重启之前必须先保存现场。尤其是describe里的Events记录和崩溃前的容器日志这些是判断根因最重要的依据。我补充了两个操作$ kubectl logs datanode-1 -n hadoop --tail200 $ kubectl logs datanode-1 -n hadoop --previous --tail200--previous参数非常关键它能看到上一次容器崩溃前留下的日志而当前容器的日志往往因为进程刚启动就退出什么都看不到。另外我还检查了节点的状态$ kubectl get nodes -o wide $ kubectl describe node node-name | grep -A 20 Conditions这一步是为了确认故障节点是否存在内存压力、磁盘压力、PID压力等问题避免遗漏kubelet层面的诱因。排查到这一步时我已经知道datanode-1所在的节点本身是健康的问题大概率出在容器内部或数据目录上。2. 从数据面到控制面的逐层复盘这波排查链路踩过的关键点2.1 先确认NAMENODE侧到底怎么看这个datanode在k8s侧看到的状态是表象接下来要做的是从HDFS自己视角确认datanode的实际情况。Hadoop集群中datanode是否“活着”是由namenode根据心跳来判定的和k8s的pod状态完全是两套体系。这两套体系各自的判定结果还经常不一致——比如pod是Running但namenode已经把datanode标记为dead。我当时执行了$ hdfs dfsadmin -report ... Live datanodes (4): Name: 10.244.1.17:9866 (datanode-2.hadoop-ns.hadoop.svc.cluster.local) ... Dead datanodes (1): Name: 10.244.1.12:9866 (datanode-1.hadoop-ns.hadoop.svc.cluster.local) Hostname: datanode-1.hadoop-ns.hadoop.svc.cluster.local注意这里datanode-1已经出现在Dead datanodes列表里。这说明至少在namenode的判定周期内datanode-1一直没有恢复心跳而不是临时抖一下的问题。在HDFS的设计里datanode会定期向namenode发送心跳心跳丢失后一段时间内取决于心跳间隔和失效阈值配置namenode就会把它标记为dead。这个时间窗口很关键。如果你在事件发生后的几分钟内就介入排查还能从namenode的日志里看到datanode最后上报心跳的时间点精确到毫秒级。这对于判断故障发生的具体时刻很有帮助。2.2 磁盘空间、数据目录与block report的微妙关系Datanode这个角色本质上是管数据的它最敏感的就是磁盘空间。一旦数据目录所在的分区满了datanode的很多操作都会跟着出问题没法写新block、没法上报block变动、甚至心跳都会变得不正常。我检查了故障pod挂载的数据目录$ kubectl exec -it datanode-1 -n hadoop -- df -h Filesystem Size Used Avail Use% Mounted on ... /dev/nvme0n1 100G 100G 0G 100% /data/dn果然数据目录所在的卷使用率已经是100%。这是个非常典型的场景datanode的数据目录满了导致健康检查接口返回500最终liveness探针失败、容器被kubelet杀掉。容器重启后datanode进程尝试启动但数据目录还是满的启动过程中无法正常完成block上报于是又触发探针失败再次被杀。这个循环就表现为CrashLoopBackOff。这里有个很多人没意识到的细节就算datanode进程能起来它也不会像没事一样继续服务。磁盘满的情况下datanode对外提供的是“能用但无法写入”的降级服务看起来活着实际已经失去数据服务能力。所以不能只看进程状态必须确认数据目录可用空间。2.3 K8s的探针机制和Hadoop健康检查是怎么互相影响的K8s的livenessProbe和readinessProbe是两套不同用途的探针liveness决定要不要重启容器readiness决定要不要把流量调度过去。配置在Hadoop这类Java服务上时有它特殊的坑。我们当时用的是HTTP探针目标是datanode的JMX或健康检查接口。探针的执行逻辑是kubelet每隔periodSeconds执行一次HTTP请求如果请求在timeoutSeconds内没有返回或者返回的状态码不是预期结果就记为一次失败连续失败次数超过failureThreshold就会触发杀容器或摘流量。这里存在一个隐蔽的错位kubelet的探针是个“外部视角”它不管你的JVM是不是在做FullGC、磁盘是不是在刷盘。只要响应慢它就认为你挂了。Java进程在FullGC时整个进程会有一段很长的停顿时间几十秒甚至几分钟都有可能。如果探针的timeoutSeconds配置得比较激进比如2秒、3秒而JVM刚好在做FullGC探针就失败如果failureThreshold设置得也很低那一次FullGC就足以让kubelet判你死刑。更麻烦的是当时的情况恰好是磁盘满磁盘IO已经非常差JVM的GC时间被无限拉长探针几乎每次都失败。所以表面上看是探针失败导致的CrashLoopBackOff实际根因是磁盘空间耗尽。这才是整个链路里最容易看走眼的地方你看到的故障点是探针真正的引爆点是磁盘。3. 根因锁定健康检查误杀、资源驱逐还是存储泄压3.1 三个候选原因是怎么筛出来的结合前两轮排查我已经把候选原因缩小到三类健康检查误杀、资源驱逐、存储泄压。接下来就是用排除法逐个确认。先排除资源驱逐。检查节点条件和pod的存活性节点没有MemoryPressure或DiskPressure至少当时没有pod也没有被驱逐过Resources的requests和limits配置也正常。可以把这项排除。再排除健康检查误杀本身作为根因。虽然是探针失败导致容器被杀但探针失败是有诱因的——磁盘满。如果只调探针参数而不解决磁盘问题下次磁盘再满一样的循环还会出现。所以健康检查误杀是整个故障链路的“执行环节”但不是“根因”。最后锁定存储泄压。数据目录100%满datanode无法正常写入和上报block这是整个链路的起点。磁盘满这件事不是一蹴而就的它通常经历了一个缓慢的累积过程。我在复盘时去查了监控确认了数据目录的用量在过去三周里从60%缓慢爬升到满但我们的磁盘告警阈值设置为90%按常理应该在90%时就已经收到告警——但实际上告警确实触发了只是被当成了一般性通知没有引起足够重视。这个点后面在预防措施里我也做了调整。3.2 livenessProbe参数与JVM停顿时间的关系确定根因后再回头看探针参数就很清楚了。当时我们的配置大致如下参数当时配置建议参考配置initialDelaySeconds3060periodSeconds1030timeoutSeconds210failureThreshold33问题就出在timeoutSeconds2这个配置上。Java进程在任何一次FullGC超过2秒探针就会失败连续3次失败容器被重启。在正常情况下这偶发的概率不高但一旦磁盘IO恶化GC时间急剧上升这个配置就成了一颗定时炸弹。这里我要强调一句探针参数不是越小越“灵敏”而是要匹配Java进程的实际响应特性。Java服务因为GC的存在天然就有几百毫秒到几秒的响应波动区间探针超时时间至少要留足10秒以上才算稳健。同时如果你发现探针频繁失败不要急着改参数——先搞清楚为什么失败探针只是信号不是问题本身。3.3 K8s的驱逐机制和HDFS冗余策略叠加后的连锁反应另外一个需要警惕的点是当一个datanode故障时HDFS不会坐视不管它会自动把缺失的副本补回来。但副本恢复是有代价的。Namenode会把缺失的block任务分发给其他健康的datanode这些datanode会复制数据块并上报。整个过程中集群的磁盘IO和网络IO都会明显上升。如果在短时间内有多个datanode一起出问题副本数会急剧下降恢复时引发的额外负载可能拖垮剩余的datanode——这就是故障“雪崩”的典型路径。虽然这次我们只有datanode-1一个节点出问题但我在排查时也特意检查了其他datanode的负载情况确认它们的IO和CPU没有异常高企才放心进行修复操作。建议固定一个意识任何单个datanode故障都要检查集群整体负载。尤其是HDFS副本恢复期间其他节点如果本来就处在很高负载下很容易被“压垮”变成连锁故障。4. 修复落地与数据安全的双重验证4.1 恢复datanode的完整操作序列修复的第一步不是重启pod而是先给数据目录腾出空间。当时临时清理了一批日志文件和一个长期未删除的临时目录空间从100%降到了85%左右。然后才开始恢复datanode。我给出的操作序列是这样的1. 确认磁盘空间已释放 2. 删除异常pod让StatefulSet重新调度 kubectl delete pod datanode-1 -n hadoop 3. 确认pod重新创建并变为Running kubectl get pods -n hadoop -w 4. 观察日志确认datanode启动和注册过程 kubectl logs datanode-1 -n hadoop -f这里有个细节StatefulSet的pod删除后会自动按名字重建而且因为StatefulSet管理的pod名字是固定的新pod会沿用原有的存储卷——前提是你使用的是RWO类型的PVC且卷能够重新挂载到新pod。如果你的datanode使用的是local PV删除pod后新pod会被调度到同一节点因为volumeAffinity的限制这一步通常没问题但如果有节点亲和、调度约束等配置需要提前验证。4.2 如何确认HDFS侧真的完成了数据再平衡Pod恢复Running只是第一步真正要确认的是HDFS侧恢复了数据服务能力。我先看datanode是否重新注册成功$ hdfs dfsadmin -report Live datanodes (5): ... Name: 10.244.1.12:9866 (datanode-1.hadoop-ns.hadoop.svc.cluster.local) Hostname: datanode-1.hadoop-ns.hadoop.svc.cluster.localLive datanodes数量和恢复前一致了。但这还不够还需要检查数据块的副本状态是否恢复到了正常水平$ hdfs fsck / -files -blocks -locations ... Status: HEALTHYfsck输出里的Status如果显示HEALTHY说明没有缺失块如果还有under-replicated的块会显示具体数量。正常情况下datanode恢复注册后namenode会把该datanode上缺失的block信息重新注册回来然后经过一段时间的block report集群会自行完成副本恢复。全程一般需要几分钟到几十分钟不等取决于数据量。4.3 容器重启之后block report恢复顺序的核对最后一步我要做的是核对block report是否正常完成。这个环节容易被忽略但它直接影响数据完整性。Datanode启动时会先扫描本地数据目录在内存中建立block列表然后向namenode发起一个最初的block report。之后是周期性的增量报告。如果block report一直没有成功上报namenode会认为这个datanode上的数据块全部不可用可能触发不必要的副本复制白白消耗集群资源。我在日志里确认了这段内容$ kubectl logs datanode-1 -n hadoop | grep block report 2025-05-20 14:32:18 INFO dfs.datanode.DataNode: DatanodeCommand: block report received 2025-05-20 14:32:18 INFO dfs.datanode.DataNode: Successfully sent block report to namenode看到这条日志基本可以确认datanode已经成功向namenode上报了本地block列表。到这里这次故障的完整修复流程算是走完了从发现到恢复大约40分钟。5. 这类故障的预防措施把排错经验变成运维防线5.1 给K8s里的Hadoop调优时的几个必查参数基于这次踩坑的经验我在后续维护中形成了一份datanode部署时的参数检查表在这里分享给你。检查项建议值/策略说明livenessProbe.timeoutSeconds10给JVM GC留出余地livenessProbe.periodSeconds30降低探针频率减少误判resources.requests.memory堆内存的1.5倍留有堆外内存余量resources.limits.memory略大于requests避免节点内存超卖导致OOMterminationGracePeriodSeconds60以上确保datanode能优雅停机、完成safe mode相关操作还有几个JVM层面的参数也值得注意-XX:UseG1GC、-XX:MaxGCPauseMillis200可以在一定程度上缓解GC长停顿的问题-Xmx和-Xms建议设置成相同的值避免运行时堆扩容引发额外停顿。5.2 监控与告警配置的参考指标这次故障暴露出来的最大监控盲区是我们没有对磁盘使用率设置分级的告警阈值。只是设置了90%的告警结果真等到90%时距离实际挂掉已经没有多少缓冲时间了。我后来把磁盘监控改成了多级告警数据目录70%时警告属于提醒级别数据目录85%时严重告警需要立即处理数据目录95%时紧急告警需要马上停止写入并排查除此之外还有几个持续在盯的指标LiveNodes数量变化UnderReplicatedBlocks数量DataNode的JMX指标比如DfsDatanodeBlockCount、DfsDatanodeReadWriteStatusPod重启次数和探针失败次数UnderReplicatedBlocks这个指标特别重要但它的波动具有一定的滞后性。从datanode失联到namenode判定dead再到安排副本复制中间有数分钟的延迟。所以这个指标更适合在故障恢复后持续观察而不是当作实时告警。5.3 常规演练故障注入与恢复时间目标验证纸上谈兵没有意义我们后来把故障处理做成了定期的演练科目。方法是主动制造故障验证监控、告警、排查链路和恢复时间是否都符合预期。具体做法是每个月挑一个低峰时段在测试环境里手动执行kubectl delete pod datanode-xxx -n hadoop甚至模拟磁盘满的场景。然后记录从pod删除到HDFS侧完全恢复用了多长时间和预期的恢复时间目标RTO对比。如果发现恢复时间超过预期就回头检查是副本恢复太慢还是监控告警链路有延迟。这个演练帮我发现过一个问题当datanode被删除后因为StatefulSet自动重建pod短时间内就能起来但HDFS侧的副本恢复花了将近一个小时才完成。后来排查发现是dfs.namenode.replication.max的默认并发限制数量设小的时候对大批block的恢复速度影响很大。调大后恢复速度明显加快。最后再分享一点个人的实操体会踩过这次坑之后我对k8s集群里跑Hadoop组件有了一个比较深的感受——k8s的探针和HDFS的心跳是两套完全独立但又互相影响的机制排查任何datanode故障时必须同时从两个体系去看不能只信一边。你看pod状态正常但HDFS侧已经标记dead的情况可能发生相反pod处于CrashLoopBackOff但HDFS还没来得及判dead的情况也可能发生。更重要的一个体会是排错时优先找“源头”不要被表面现象牵着走。这次故障的第一现场是探针失败但如果我当时只盯着探针参数去调整磁盘满的问题就会继续潜伏迟早还会用另一种方式爆出来。探针失败、容器重启这些都是结果不是原因。先找到那个触发这一切的底层变化——无论是磁盘、内存、网络还是配置才能让集群真正稳定下来。下次如果你也遇到类似的datanode故障建议按照这个顺序排查先看pod和事件再看HDFS侧的心跳状态然后确认磁盘空间和系统负载最后再决定是调整探针参数、释放磁盘空间还是需要重新调度pod。这套思路放在大多数容器化的大数据组件比如hbase的RegionServer、kafka的broker上也都是通用的。
返回列表