深入理解 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 被创建或更新时,它会:

  1. 统计指定时间窗口内特定事件类型的发生次数
  2. 与 Tolerance 配置的阈值对比
  3. 达到阈值则触发对应的 Action(Reboot 或 Replace)
  4. 检查节点是否在白名单中(有 npd-node-replace-enabled=true 标签)

NodeController

职责:监控节点 Ready Condition 的变化。

不是所有问题都能通过 NPD Event 捕获。比如 kubelet 进程挂了,节点直接变成 Unknown 状态,可能不会产生 Event。

NodeController 监控所有节点的状态。当发现某个节点从 Ready 变成 NotReady 或 Unknown 时:

  1. 记录到对应的 NodeIssueReport
  2. 启动 Double Check 计时
  3. 等待配置的时间后再次检查
  4. 如果仍然异常,触发 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。只有打了标签的节点才会被自动处理。

这个设计让你可以:

  1. 先在非关键节点上验证
  2. 逐步扩展到更多节点
  3. 对关键业务节点保持人工确认

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 相对简单:

  1. 设置节点为不可调度(Cordon)
  2. Drain Pod
  3. 调用 EC2 RebootInstances API 重启实例
  4. 等待节点恢复 Ready
  5. 取消不可调度(Uncordon)
  6. 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


参考

posted @ 2026-05-05 11:04  亚马逊云开发者  阅读(4)  评论(0)    收藏  举报