深入理解 EKS 节点自愈架构:NPD + npd-node-replace 的设计与实现
深入理解 EKS 节点自愈架构:NPD + npd-node-replace 的设计与实现
管 K8s 集群的人都绕不开一个问题——节点故障处理。内核崩溃、OOM、硬件坏了,节点变成 NotReady。手动 drain、手动替换,半夜来一次谁都受不了。这篇从架构层面拆解一下 npd-node-replace 这个方案,看看它是怎么把节点自愈做稳的。
背景:节点故障处理的演进
在亚马逊云科技 EKS 集群中,节点故障的处理经历了几个阶段:
阶段 1:纯人肉。告警来了,登录查看,kubectl drain,手动替换。耗时长,体验差。
阶段 2:NPD + 人工。Node Problem Detector 自动检测问题并上报 Event,但处理还是靠人。好处是问题发现变快了。
阶段 3:NPD + Karpenter。Karpenter 可以自动回收和替换问题节点。但只支持 Karpenter 自己管理的节点,托管节点组和自管理节点组不行。
阶段 4:NPD + npd-node-replace。这就是今天要聊的方案。覆盖托管节点组和自管理节点组,故障持久化记录,三重防误操作机制。
整体架构
npd-node-replace 由三个核心 Controller 组成,各司其职:
EventController
职责:从 API Server 接收 NPD 上报的节点问题 Event。
NPD 以 DaemonSet 方式部署在每个节点上。它通过检查系统日志、内核消息等方式发现问题,然后向 API Server 上报 Event。
EventController 监听这些 Event,解析出事件类型(OOMKilling、KernelOops、KernelHang 等),创建或更新对应节点的 NodeIssueReport 自定义资源。
NodeIssueReport(NIR)是这个方案的核心数据结构。它记录了每个节点的完整故障历史,包括事件类型、发生时间、处理状态。这比 K8s 原生 Event(默认只保留 1 小时)持久化得多。
NIRController
职责:分析 NodeIssueReport,决定是否触发自愈操作。
NIRController Watch NodeIssueReport 的变更。每当 NIR 被创建或更新时,它会:
- 统计指定时间窗口内特定事件类型的发生次数
- 与 Tolerance 配置的阈值对比
- 达到阈值则触发对应的 Action(Reboot 或 Replace)
- 检查节点是否在白名单中(有
npd-node-replace-enabled=true标签)
NodeController
职责:监控节点 Ready Condition 的变化。
不是所有问题都能通过 NPD Event 捕获。比如 kubelet 进程挂了,节点直接变成 Unknown 状态,可能不会产生 Event。
NodeController 监控所有节点的状态。当发现某个节点从 Ready 变成 NotReady 或 Unknown 时:
- 记录到对应的 NodeIssueReport
- 启动 Double Check 计时
- 等待配置的时间后再次检查
- 如果仍然异常,触发 Replace 操作
防误操作机制详解
节点自动替换是个高风险操作。如果误触发,可能比节点故障本身造成的影响还大。npd-node-replace 在这方面做了很细致的设计。
时间窗口机制
Tolerance 配置支持 timewindowinminutes 参数:
{
"tolerancecollection": {
"OOMKilling": {
"times": 2,
"action": "reboot",
"timewindowinminutes": 30
},
"KernelOops": {
"times": 3,
"action": "replace",
"timewindowinminutes": 60
}
}
}
以 KernelOops 为例:只有在 60 分钟内累计出现 3 次 KernelOops,才会触发 Replace。
为什么不用总次数?因为节点运行时间长了,偶发的小问题可能累积很多次。用总次数的话,一台跑了半年的节点可能因为历史上偶发的几次小问题被误替换。
时间窗口让判断聚焦在「近期是否频繁出问题」,更合理。
Double Check 机制
- name: NODE_DOULBE_CHECK_GRACE_TIME
value: "15"
节点状态变成 NotReady 或 Unknown 后,不立即执行替换。等待 15 分钟(可配置)后再次检查状态。
为什么需要这个?几个场景:
- 节点重启过程中会短暂出现 NotReady,这是正常的
- 网络抖动可能导致 API Server 暂时收不到心跳
- kubelet 重启也会出现短暂的状态异常
Double Check 过滤掉了这些瞬态问题。
注意:这个值必须大于新节点启动和节点重启所需的时间。否则节点正在恢复的过程中就被判定为异常了。
白名单机制
kubectl label nodes <node-name> npd-node-replace-enabled=true
默认情况下,npd-node-replace 只做问题收集和记录,不会对任何节点执行 Reboot 或 Replace。只有打了标签的节点才会被自动处理。
这个设计让你可以:
- 先在非关键节点上验证
- 逐步扩展到更多节点
- 对关键业务节点保持人工确认
Replace 流程深入
Replace 的流程比简单的删除重建复杂得多:
第一步:从 Auto Scaling 组分离
调用 EC2 Auto Scaling API,将问题节点从 ASG 中分离(Detach)。分离后,ASG 会自动拉起一台新实例来维持 Desired Count。
第二步:等待新节点就绪
不是分离了就完事。npd-node-replace 会等待新节点加入集群并且状态变成 Ready。这一步确保替换后集群的计算容量没有减少。
第三步:Drain 旧节点
新节点就绪后,执行 kubectl drain 操作,优雅地驱逐旧节点上的 Pod。
第四步:通知管理员
通过 Amazon SNS 发送通知邮件,包含节点问题详情和执行的自愈动作。
第五步:删除旧 Node 对象
从 K8s 集群中删除旧的 Node 对象。但关键的是——不终止 EC2 实例。
为什么不终止?因为要保留故障现场。运维人员可以通过 EC2 控制台找到这个实例,SSH 登录上去收集日志、检查内核 dump,做根因分析。分析完了再手动终止。
Reboot 流程
Reboot 相对简单:
- 设置节点为不可调度(Cordon)
- Drain Pod
- 调用 EC2 RebootInstances API 重启实例
- 等待节点恢复 Ready
- 取消不可调度(Uncordon)
- SNS 通知管理员
部署实践
前置条件
- EKS 集群 + IAM OIDC 提供商
- Fargate Profile(重要:npd-node-replace 自身需要跑在 Fargate 上,避免处理自己所在的节点)
- 已部署 Node Problem Detector
- Amazon SNS 主题 + 订阅
- Amazon ECR 仓库
IAM 策略
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Effect": "Allow",
"Action": [
"ec2:RebootInstances",
"ec2:DescribeInstances",
"autoscaling:DetachInstances",
"sns:Publish"
],
"Resource": ["*"]
}
]
}
aws iam create-policy \
--policy-name NPDNodeReplacePolicy \
--policy-document file://iam_policy.json
eksctl create iamserviceaccount \
--cluster=<cluster-name> \
--namespace=<fargate-namespace> \
--name=npd-node-replace-sa \
--attach-policy-arn=arn:aws-cn:iam::<account-id>:policy/NPDNodeReplacePolicy \
--override-existing-serviceaccounts \
--region <region> \
--approve
Helm 部署
docker pull zxxxxzz/npd-node-replace:v1.2
docker tag zxxxxzz/npd-node-replace:v1.2 \
<account-id>.dkr.ecr.<region>.amazonaws.com.cn/<repo-name>:v1.2
aws ecr get-login-password --region <region> | \
docker login --username AWS --password-stdin \
<account-id>.dkr.ecr.<region>.amazonaws.com.cn
docker push <account-id>.dkr.ecr.<region>.amazonaws.com.cn/<repo-name>:v1.2
helm repo add npd-replace https://normalzzz.github.io/npd-node-replace/
values.yaml 关键配置:
kubernetesClusterDomain: cluster.local
npdNodeReplace:
npdNodeReplace:
env:
snsTopicArn: <sns-topic-arn>
nodeDoubleCheckGraceTime: 15
image:
repository: <account-id>.dkr.ecr.<region>.amazonaws.com.cn/<repo-name>
tag: v1.2
imagePullPolicy: Always
replicas: 1
sa:
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: <irsa-iam-role-arn>
toleranceConfig:
toleranceJson: |-
{
"tolerancecollection": {
"OOMKilling": {
"times": 2,
"action": "reboot",
"timewindowinminutes": 30
},
"KernelOops": {
"times": 3,
"action": "replace",
"timewindowinminutes": 60
}
}
}
helm install npd-replace npd-replace/npd-node-replace \
--namespace <fargate-namespace> \
--set serviceAccount.create=false \
-f values.yaml
# 验证
kubectl get deployment -n <fargate-namespace> | grep npd-node-replace
测试验证
模拟节点问题
# 模拟 OOM
echo "Killed process 1234 (myapp) total-vm:102400kB, anon-rss:51200kB, file-rss:2048kB" \
| sudo tee /dev/kmsg
# 模拟内核错误
echo "<1>BUG: unable to handle kernel NULL pointer dereference at 0x00000000" \
| sudo tee /dev/kmsg
echo "<1>divide error: 0000 [#1] SMP" | sudo tee /dev/kmsg
查看故障记录
kubectl get nodeissuereport
kubectl describe nodeissuereport <name>
模拟节点状态异常
systemctl stop kubelet
# 节点变 Unknown → 15分钟 Double Check → Replace
上线建议
阶段 1:观察期。部署组件,不打标签。观察 NPD 检测和 SNS 通知是否正常。
阶段 2:验证期。选几台非关键节点打标签,验证 Reboot 和 Replace 流程。
阶段 3:推广期。逐步扩展到更多节点。
日志建议输出到 CloudWatch。
总结
npd-node-replace 的设计思路很清晰:NPD 管发现,它管处理。通过 NodeIssueReport 实现故障持久化,通过时间窗口 + Double Check + 白名单实现三重防护。
比 Karpenter 方案的优势在于支持更多节点形态。Replace 时保留 EC2 实例的设计也很实用——出了问题能回去查。
代码仓库:npd-node-replace
参考:

浙公网安备 33010602011771号