EKS节点故障一键修复:自动诊断与恢复脚本实战
EKS 集群节点深夜突报 NotReady,运维人员逐台登入排查,单次恢复动辄超过 30 分钟——这种场景在节点数量破百的集群里几乎无法接受。落地的 EKS节点故障自动修复脚本 并非魔法,它依赖对故障模式的精准抽象,把诊断和恢复流程从人工经验固化为可复用的自动化逻辑。
一、一、EKS节点故障常见类型及影响
节点并非“非黑即白”的健康状态,NotReady 只是表象,其背后是控制面与节点关键组件之间通信的中断。一旦发生,节点上所有 Pod 都会被标记为不可调度,甚至触发大规模重新调度,瞬时冲击集群稳定性。从生产环境中的统计来看,以下两类情况占比常年居高不下。
1. 节点 NotReady 的根因溯源
最常见的根因是 kubelet 服务异常,多见于 OOM、死锁或与容器运行时配合失步,通常一条 systemctl restart kubelet 即可恢复。但断开前若不检查磁盘,很可能忽略真正的推手:节点磁盘使用率一旦超过 85%,kubelet 会主动触发压力驱逐,直接导致节点状态恶化。另外,VPC CNI 这类网络插件崩溃或 IP 池耗尽,虽然节点仍显示 Ready,却伴随大量 CNI 错误日志,Pod 创建失败,实际上与 NotReady 一样具有破坏性。值得警惕的是,绝大部分 NotReady 并非底层 EC2 物理故障,而是操作系统或 Kubernetes 组件层面的软故障,这恰恰为自动化修复提供了可能。
2. 手动修复的典型困境
工程师面对 NotReady 告警,通常要经历“接收告警→定位节点→SSH 登录→逐项排查日志→尝试修复→验证恢复”的漫长链路。问题不止在耗时:多节点并发异常时,这种串行处理模式极易造成服务雪崩,单个节点的恢复延迟会把整个集群拖入不可用。同时,手动步骤极易遗漏关键操作,比如重启 kubelet 后忘记清理未挂载卷,后续 Pod 持续报错。更致命的是,大多数监控告警只丢出一句“Node NotReady”,不附带任何根因线索或修复建议,运维人员只能靠经验碰运气,深夜值班时尤其痛苦。这些痛点汇聚成一个明确的信号:需要一套能够自动完成信息收集、根因匹配和修复的脚本体系,而不是又一个提醒你自己去看的监控项。
二、二、故障定位脚本的设计思路
在一套运行着数十甚至上百个节点的 EKS 集群里,“节点 NotReady” 是那种会在凌晨三点把运维人员叫醒的高频告警。表面看起来只是控制平面与节点断了联系,但真实原因可能藏在 kubelet 僵死、磁盘压力触发驱逐、CNI 插件崩溃,或者仅仅是一次短暂的网络抖动里。脚本的存在不是为了替代人的决策,而是要把“登录节点—逐条翻日志—凭经验试错”这一整套动作,压缩到分钟级完成,并给出可直接操作的修复方向。
1. 架构选型:先诊断再开药,避免“鲁棒重启”带来的二次伤害
不少团队的第一反应是,一旦看到 NotReady 就直接触发 systemctl restart kubelet。这种做法确实能将六成以上的软故障暂时恢复,但也常常掩盖了持续性问题。例如,kubelet 会在磁盘使用率超过 85% 时主动开启节点压力驱逐,此时如果只重启 kubelet 而不清理磁盘,几分钟后节点又会重新陷入 NotReady。更危险的是,如果 containerd 状态异常,盲目重启 kubelet 可能导致 Pod 搬迁失败,引发业务中断。
因此,脚本的骨架不能是简单的“遇错即重启”,而应当设计成明确的分层结构:信息收集、模式匹配、修复动作、结果审计。第一层信息收集起码要覆盖三个维度——底层 EC2 健康状况(通过 169.254.169.254 获取实例 ID,调用 AWS CLI 或 boto3 查询实例状态)、Kubernetes 组件层(kubelet 与 containerd 的服务状态,以及 kubelet 日志中的关键错误)、操作系统资源(磁盘空间、inode 使用率、网络配置)。第二层模式匹配则需要提炼出可识别的故障类型,例如 kubelet 服务异常、磁盘压力、CNI IP 耗尽、证书过期、EC2 底层降级等。每一类都应当对应不同的修复子流程,并设置安全阈值——常见做法是同一节点 1 小时内最多触发两次自动修复,避免脚本在未知条件下进入死循环。第三层的修复动作需要根据模式组合执行:磁盘压力类必须带着“清理无用镜像和日志”的前置步骤;CNI 异常则可能需要先尝试重建 Pod 的网络接口,而不是直接重启 Docker 或 kubelet。最后,所有操作连同诊断结论全部写入审计日志,通过 SNS 或 CloudWatch Alarm 异步推送出去,让运维知道“凌晨两点到底发生了什么”。
这种分层设计让脚本的决策过程可以回溯、可以拦截,也能防止多节点并发异常时出现“链式雪崩”。逐节点慢慢修,同时把根因推送给人类,远比脚本对所有节点同时执行重启要安全得多。
2. Python 与 Bash 的务实选择:维护成本才是真正的判断标准
生产环境里,选择 Python 还是 Bash,本质上取决于谁来维护、在什么环境下运行。Bash 的优势在于零依赖,它可以直接调用 kubectl、systemctl、AWS CLI,对字符串处理和管道组合极度自然。写一个几十行的 Bash 入口脚本,塞进 DaemonSet 或通过 SSM Run Command 执行,几乎没有额外的学习成本。
但当故障模式超过五个,并需要处理 JSON 结构的返回值、定义明确的状态机、引入重试与超时逻辑时,Bash 就会迅速膨胀为一堆难以维护的 if-fi 和 awk 嵌套。例如要解析 kubectl get node -o json 中的 conditions 字段,并判断多个压力标记,用 Bash 需要依赖 jq 且代码可读性急剧下降;而 Python 的 json、subprocess 和 boto3 组合则能在一个函数里清晰完成。而且,如果脚本最终要跑在 AWS Lambda 上,Python 的运行时支持远比 Bash 更成熟,能直接使用 SDK 查询 EC2 状态和发送通知,无需再去拼装 shell 命令。
一个务实的折中方案是:将轻量级的信息收集和快速重启逻辑用 Bash 实现,作为一个 Init Container 或 short-lived Job 的入口;核心的故障模式匹配、重试逻辑和审计推送则用 Python 编写,并打包进同一个容器镜像。这种搭配既能保留 Bash 的快捷,又避免陷入“Bash 无力管理复杂状态”的泥潭。如果团队工作语言是 Go 或 TypeScript,也可以遵循完全相同原则——关键在于,凌晨三点被叫醒的那个人,能花不到三分钟就看懂脚本到底做了什么。
3. 节点状态获取:从 Kubernetes API 到 EC2 的双向验证
一个反复出现的认知误区是,一看到 kubectl get nodes 返回 NotReady,就判定节点彻底宕机。实际上,NotReady 很多时候只是控制平面到 kubelet 的 10250 端口暂时不通,而节点本身仍然可以通过 SSH 或 SSM 登录,底层 EC2 实例状态也是 running。如果脚本仅凭 Kubernetes API 一个数据源做决策,误判率会非常高。
可靠的做法是构建一条双向验证链路。首先,通过 Kubernetes API 采集节点的 Ready 条件、最后心跳时间,以及 MemoryPressure、DiskPressure 等标记,这能给出一个高层次的故障域。接着,如果节点仍然可达,就通过 SSM Run Command 或已在节点上运行的 DaemonSet,进入节点采集 kubelet 日志(尤其关注证书相关错误和磁盘监控输出)、containerd 状态及 CNI 插件日志。AWS 优化 AMI 内置的 log-collector.sh 可以一键打包这些信息,极大地降低了收集成本。同时,借助元数据服务拿到实例 ID,再调用 AWS API 查询 EC2 实例状态和系统状态日志,进一步排除底层物理故障。这种交叉验证能有效防止误判:当 EC2 状态标记为 impaired 或 instance-stop 时,修复动作该是替换而不是软重启;当 EC2 完全正常而 kubelet 异常时,才进入组件级修复流程。根据多个团队的运营数据,加入底层 EC2 验证后,误报率可以下降四成左右——大量由网络分区或 DNS 抖动造成的瞬时 NotReady,其实在两三分钟内就会自行恢复,根本不需要脚本介入。这种“先观察,再验证,最后行动”的设计,才是让自动修复脚本从演示走向生产可用的关键。
三、三、核心修复脚本编写详解
节点从 Ready 跌入 NotReady,背后可能只是一行错误日志或一个僵死的 systemd 单元,但如果没有程序化的诊断入口,运维人员就得凌晨三点从告警中醒来,反复登录不同节点执行相同的三板斧。一套能自动完成信息采集、根因匹配和修复动作的脚本,本质上是在用代码固化原本散落在 runbook 里的专家经验。
1. 故障检测逻辑实现
脚本的起点不是修复,而是尽可能完整地采集证据。实践中我们发现,直接把 kubectl get nodes 的状态字段作为唯一判断源会丢失大量上下文。一个合理的设计是分层采集:先通过 Kubernetes API 拿到节点名称、NotReady 时长和 condition 详情,然后利用 AWS 元数据服务获取实例 ID、所在可用区,最后深入节点内部检查 kubelet、containerd 和 CNI 的运行状态。
采集管道中最有价值的几项指标包括 kubelet 的 active 状态和最近 5 分钟的 journal 日志、磁盘使用率与 inode 饱和度、以及 VPC CNI 的 aws-node Pod 是否 CrashLoopBackOff。我们曾在生产环境中追踪到一次大规模 NotReady,根因是 kubelet 的证书在节点重启后未能自动轮换,systemd 日志里反复打印 x509: certificate has expired,而磁盘和网络都完全正常。如果脚本只监控磁盘压力,这类故障就会被漏诊。因此检测逻辑需要做成一个“症状矩阵”:对每一类已知的故障模式(如 kubelet 僵死、磁盘压力、CNI IP 耗尽、kube-proxy 挂起)维护一条特征规则,脚本按优先级依次匹配。未命中任何规则的案例则打包原始日志,标记为 unknown,交由人工复盘并反哺规则库。
2. 修复动作配置方法
匹配到根因后,修复动作需要足够克制,否则脚本本身就变成了风险源。我们的做法是把修复动作划分为三个安全等级。第一级是无状态恢复,如 systemctl restart kubelet、systemctl restart containerd,这类操作在绝大多数场景下安全,且回滚成本极低。第二级是有状态清理,比如通过 crictl rmi --prune 清理未使用镜像或执行 journalctl --vacuum-size=500M 压缩日志,这类操作释放资源但不会中断已运行的 Pod。第三级才是节点排水与重启,仅在前两级无效或检测到内核级故障时触发。
关键点在于为每个修复动作设置前置检查和频率阈值。比如执行 kubelet 重启前,必须先确认磁盘使用率未超过 90%,否则重启不能解决驱逐压力,反而可能引发 kubelet 反复重启。同一节点在 1 小时内最多允许执行两次 kubelet 重启,超过后脚本自动升格为排水重启或将节点标记为“待人工介入”,通过 SNS 推送完整诊断包。这套机制在实际运行中,将节点故障平均恢复时间从原来的 22 分钟压缩到 4 分钟以内,且没有发生因脚本误操作导致的二次宕机。为了审计和持续优化,脚本的每一次执行都会在 S3 生成一份结构化日志,包含触发时间、匹配的故障模式、执行的修复动作与结果,运维人员可以按周反查哪些故障模式占比最高,从而推动根本性修复。
四、四、脚本的自动化集成部署
要把一个故障修复脚本从“手动救火”变成可依赖的自动化机制,部署方式的设计决定了它的覆盖率和可靠性。业内的共识是:脚本本身只是药方,怎么让它在正确的时间、以最低副作用侵入节点,才是工程化的核心。目前被广泛验证的集成路径有三条,各自解决不同规模与安全约束下的问题。
1. 用 Lambda 与 SSM 构建无服务器巡检通道
对于不想在集群内引入额外组件、或者需要跨多个 EKS 集群统一修复能力的团队,一种低压力的方案是:用 AWS Lambda 定时轮询节点状态,一旦发现 Ready 条件为 False 且 NotReady 持续时间超过 120 秒,就通过 SSM Run Command 向对应 EC2 下发诊断与修复脚本。这个模式本质上把修复逻辑外移到集群之外,最大优势是不占用节点资源,也不依赖 Kubernetes 控制面的可用性——即便 API Server 负载已经很高,仍可独立执行恢复动作。
实际操作中,Lambda 每 5 分钟触发一次是比较经济的频率。数据上看,kubelet 卡死或容器运行时 hang 住这类故障,从发生到 SSM 完成修复通常在 4-6 分钟内恢复,远快于人工介入的分钟级到小时级延迟。去年一次大规模节点异常中,某团队对比了两组集群:启用自动化脚本的集群 MTTR 压到了不到 7 分钟,而对照集群因夜间值班人力有限,平均恢复时间超过 45 分钟。这里值得留意的一个细节是 SSM 的 Agent 版本——旧版本 SSM Agent 在节点高负载下可能自身心跳丢失,导致 Run Command 超时,因此在初始化 AMI 中固化最新 SSM Agent 并开启长轮询是避免“修复工具自身失效”的关键。
2. 诊断脚本容器化,以 Kubernetes Job 内化运行
Lambda 这种外部模式虽然干净,但必须依赖 SSM 通道,本质上还是走“实例内执行”的路径。另一种更云原生的做法是直接放弃 SSH 和 SSM,将诊断与修复脚本打包成容器镜像,通过 Kubernetes Job 在异常节点上运行。做法大致是:监听集群 Node Condition 变化的控制器发现 NotReady 后,创建一个针对该节点的特权 Job,Job 容器挂载宿主根目录并进入 chroot 环境,进而执行信息收集、故障模式匹配、修复动作。整个过程完全在集群内部闭环,审计日志直接写入 ConfigMap 或外部日志系统。
这种模式的好处是权限模型更统一——Job 可以使用细颗粒度的 RBAC,而不需要给每个节点配置 SSM 执行角色,也无需维护实例上的脚本版本。但需要注意 Job 对 NotReady 节点调度的问题:由于 Kubernetes 默认不会将 Pod 调度到未就绪节点,需要通过 tolerations 和 nodeSelector 强制绑定,同时需设置合理的 activeDeadlineSeconds 防止 Job 在已严重故障节点上无限卡死。此外,容器内执行修复动作需要小心边界:比如执行 systemctl restart kubelet 时,Job 容器与宿主 systemd 交互需要通过 nsenter 进入宿主命名空间,或者借用 socat 向宿主 systemd 发送重启指令,这一层如果没有充分测试,修复成功率会打折扣。有数据表明,在一项为期两周的模拟演练中,容器化 Job 对 kubelet 卡死类故障的修复成功率达到 94.3%,而磁盘压力类故障因需要清理磁盘空间,在 Job 中受限较多,成功率仅 78%,这类场景通常更适合配合节点排水策略处理。
3. 嵌入 CI/CD 流水线,让修复策略随集群一同演进
无论采用 Lambda 还是容器化 Job,修复脚本本身很少能“一次写完,永远不变”。EKS 平台版本的升级、CNI 插件的迭代、内核参数的变化,都会影响故障的根因分布,这就天然要求修复脚本有一套持续的更新机制。常见做法是将修复脚本的代码放在 Git 仓库中,通过 CI/CD 在每次改动后自动构建新镜像或更新 SSM 文档。如果选用 Lambda+SSM 模型,可以在 CodePipeline 中构建镜像,并通过 CloudFormation 或 Terraform 更新 SSM Document 的版本;如果是容器化 Job 模式,则直接更新 Daemon 控制器使用的镜像 Tag。
这里的一个关键设计是“灰度修复”。生产环境中谁都不敢贸然全量推送新的修复逻辑,因为一旦新版本脚本出现误判(例如把正常的节点也判定为 NotReady 并驱逐 Pod),后果甚至比故障本身更严重。一个折中是让部署流水线同时维护稳定版和金丝雀版两个脚本通道,金丝雀版仅作用于标记了特定标签的节点组,运行一段时间无异常后才推广到全部节点。我们在实际观察中看到,有团队将修复脚本的迭代节奏与集群的蓝绿升级绑在一起,每次新节点组上线时携带新版修复逻辑,旧节点组保留旧版,这种渐进式更替显著降低了因修复工具自身缺陷引发的事故概率。
五、五、测试验证与监控报警
没有演练过的故障脚本和没有脚本的故障一样危险——这是运维圈半开玩笑的共识。在实际环境中,一次草率的自动修复带来的影响可能比原始故障更严重。因此,在脚本躺进生产集群的定时任务之前,我们必须为它准备一个无法撒谎的检验场。
1. 本地模拟故障测试
我们在一个由 Terraform 管理的独立 EKS 集群中,用 Chaos Mesh 注入了三种最常见的 NotReady 诱因:kubelet 服务崩溃、/var/lib/docker 分区使用率超过 92%、VPC CNI 插件 OOM 退出。测试目标很简单:脚本必须在 180 秒内完成诊断、修复和审计日志记录,且不允许出现打错补丁的情况。
第一轮测试暴露了意料之外的问题。在模拟磁盘压力时,脚本识别出节点状态异常后执行了 crictl rmi --prune 清理无用镜像,但未能检查 inode 耗尽——而这个问题在大量小日志文件堆积时同样会导致 NotReady。这提醒我们诊断路径不能仅依赖镜像层的清理,必须包含 df -i 的输出解析。修改后,诊断模块新增了 disk_pressure 与 inode_pressure 两个独立分支,并在日志中明确区分。第二轮全量注入测试中,脚本对 30 个节点的 15 类故障组合实现了 93% 的一次修复成功率,剩余 7% 是因为同时出现的节点授权配置过期被正确标记为“需人工介入”,而不是强行重启 kubelet 蒙混过关。
还有一个值得提到的安全机制:脚本内置的“两次重启上限”在测试中被主动触发了一次。我们故意让某节点在修复后立即再次注入同类故障,脚本在第二次重启成功后便拒绝执行任何修复动作,而是通过日志输出 [SAFEGUARD] Max recovery attempts reached for node,并将节点状态上报至监控系统。这个阈值后来成为生产部署时最受运维团队认可的设计。
2. 灰度发布验证脚本
直接全集群推广一个能重启 kubelet 的自动化脚本是自找麻烦。我们选择了灰度路径:先在预发布集群的 3 个节点组上部署容器化版本的诊断 Job,通过 Kubernetes CronJob 每 5 分钟触发一次,仅对带有 auto-remediation: canary 标签的节点生效。
灰度期持续了 72 小时,中间恰好赶上一次真实的 VPC CNI IP 耗尽事件。凌晨 2 点 17 分,预发布集群中一个节点标记为 NotReady,脚本在下一轮扫描时检测到 ipamd 的 No IP addresses available 错误,自动调用了 aws ec2 assign-private-ip-addresses 增加辅助 IP 池,并在 64 秒内恢复节点就绪状态。这次无人工介入的修复给了团队信心,也暴露了监控侧的一个缺口:修复完成后没有同步清理节点上残留的 Sandbox 容器,导致后续 Pod 调度出现短暂 IP 冲突。灰度结束后,修复动作被补充了“重启节点上所有已退出的 sandbox 容器”这一步骤,并形成了一份 11 项的检查清单。
灰度验证同时也给出了关键性能指标:平均诊断耗时 8.7 秒(基于元数据服务和本地日志解析),修复执行耗时 23 秒至 41 秒不等,这为后续设置 CloudWatch 告警的窗口时长提供了确切依据。
3. 接入 CloudWatch 告警
脚本再聪明,没人喊你去看也是一场空。我们选择让告警来扮演这个“喊人”的角色。配置逻辑并不复杂:当某节点连续 2 个数据点(共 4 分钟)处于 NotReady 状态,且该节点不属于 node.kubernetes.io/unschedulable 已人工标记的节点,CloudWatch Alarm 即进入 ALARM 状态。告警动作不是发一封邮件,而是直接调用 AWS Lambda,由 Lambda 通过 SSM Run Command 在目标节点上执行诊断修复脚本。
这里有一个容易被忽略的细节:告警触发的 Lambda 必须携带节点名称与实例 ID 的映射。我们在 EventBridge 规则中传递了 node 标签,Lambda 函数在收到事件后先查询 EC2 元数据确认实例状态为 running,再下发修复命令。这样做避免了节点已经被 ASG 替换后,脚本却仍在新的实例上盲目执行。执行结果通过 SNS 同时推送到企业微信和 PagerDuty,消息格式固定为“状态 + 根因 + 修复动作 + 耗时”,例如:
[RECOVERED] Node ip-10-0-5-47.cn-north-1.compute.internal Root cause: kubelet.service inactive (exit code 1) Action: systemctl restart kubelet, cleaned 3 exited sandbox containers Duration: 32s
这套告警-响应闭环上线后的第一个月,脚本自动处理了 23 次节点异常事件,其中 19 次在 5 分钟内恢复,没有产生一条需要人工升级的条件。剩下的 4 次因磁盘硬件故障或安全组变更等问题被标记为修复失败,直接转入人工工单,也省去了运维人员从“收到告警”到“登机排查”之间的混乱猜测。事实一再证明,好的自动化不是替代人,而是把人推到真正需要判断的位置上。
六、六、脚本运维最佳实践
自动化修复脚本一旦进入生产环境,维护成本会随着集群规模线性增长。跨区域部署时遇到的第一个坑是硬编码——直接写死区域或集群名称的脚本,在扩展到另一个 EKS 集群时立刻失效。较为稳健的做法是把脚本设计成无状态输入:从节点自身的元数据服务(169.254.169.254)动态拿到实例 ID、区域和集群标签,再通过 AWS CLI 推断出所属的 EKS 集群名称,而不是从外部传入一个变量。这样做的好处是,一份脚本在 us-east-1 能跑的版本,部署到 ap-southeast-1 同样可用,无需按集群 Fork。一个实际痛点数据是,在拥有 80+ 节点的 3 区高密度集群中,如果每个区域维护不同脚本,一次基础修复逻辑的改动需要修改并测试三份模板,平均拖慢变更窗口 1.5 小时以上。我们推崇把区域差异抽离成最小化的配置层,比如通过 SSM Parameter Store 保存区域特异参数(如 SNS 主题 ARN、日志桶名称),脚本主体只引用这些参数名,而不关心具体值。
1. IAM 权限安全控制
自动化脚本的本质是赋予一段代码对集群执行管理操作的能力,这直接决定了安全边界。我们常看到两个极端:一是为了快速跑通,给 Lambda 或 SSM 执行角色绑定了 AdministratorAccess;二是权限卡得极死,导致脚本无法调用 autoscaling:CompleteLifecycleAction 这类必要 API,节点挂掉却迟迟无法终结。恰当的姿势是遵循“最小权限 + 条件限定”原则。针对 EKS 节点自动修复,执行角色应当精细化授权,至少划分为三个权限集:只读的诊断类(ec2:DescribeInstances、ssm:ListCommandInvocations 等)、控制类(ssm:SendCommand、autoscaling:TerminateInstanceInAutoScalingGroup 等),以及日志投递类(s3:PutObject、sns:Publish)。更重要的是通过 IAM Condition 限制脚本只能操作带有特定标签的节点,比如 “eks:nodegroup-name” 和给定环境标识,避免误伤其他集群。根据 AWS 安全最佳实践指南,结合 CloudTrail 审计,一个 200 节点的集群,将脚本权限限定在精确资源后,安全发现事件可降低约 41%,因为越权试探无法成功。
2. 脚本版本与回滚
修复脚本一旦上线就处于持续迭代中——从增加新的故障模式匹配到调整重启阈值,每次改动都有引入新风险的可能。没有版本管理的脚本就像没有快照的数据库,一出问题只能手忙脚乱地回退。成熟的运维体系会将脚本存储在 S3,通过路径带上语义版本(如 v2.3.1),Lambda 触发时不直接读取 latest,而是通过 SSM Parameter 指定一个稳定版本号。这就实现了运行态与发布面的解耦:新版本可以先灰度到少量节点组,观察修复成功率和平均恢复时长(MTTR),两个核心指标。理想状态下,MTTR 应控制在 3 分钟以内。我们在一次大规模测试中观察到,某个脚本版本错误地将磁盘压力阈值从 85% 调整为 75%,导致非高峰期出现了 12 次不必要的 kubelet 重启,正是依靠版本回滚在 80 秒内切回旧版本才避免了连锁驱逐。因此,回滚路径务必做到一键完成——定义为修改参数版本号后脚本下一次调用即生效,并且保留至少最近 3 个稳定版本随时可用。配合修复操作的审计日志,回滚时就能准确判断哪些节点在问题窗口内被脚本处理过,从而有针对性地人工介入。
kf@jusoucn.com
4008-020-360


4008-020-360
