跳转到内容
123xiao | 无名键客

《集群架构实战:基于 Kubernetes 的高可用控制面与工作节点故障自愈设计》

字数: 0 阅读时长: 1 分钟

背景与问题

很多团队刚上 Kubernetes 时,最容易误解的一点是:只要 Pod 配了副本数,集群就算高可用了。真到线上出故障,才发现问题远不止业务 Pod 这么简单。

我自己做过几次集群故障排查,最典型的现场通常长这样:

  • 控制面某台节点重启后,kubectl 偶发超时
  • API Server 看起来还活着,但调度明显变慢
  • 某个工作节点网络抖动,Pod 处于 TerminatingUnknown
  • DaemonSet 没完全恢复,业务侧开始报连通性异常
  • 节点“看似在线”,但 kubelet 已经半失联

这类问题的本质,不是单一组件挂了,而是控制面高可用设计工作节点故障自愈机制没有形成闭环。

本文我会从 troubleshooting 的角度,把这个问题拆开讲:

  1. 高可用控制面为什么不只是“多部署几台 master”
  2. 工作节点故障后,Kubernetes 是如何判断、驱逐、重建的
  3. 出问题时应该怎么定位
  4. 给一套可运行的实战配置和排查命令

现象复现

先看几个线上最常见的异常现象,后面排查会围绕它们展开。

现象 1:控制面单点隐患

只有 1 个控制面节点时:

  • kube-apiserver 所在机器宕机,整个集群 API 不可用
  • 控制器和调度器无法继续执行
  • 已运行的 Pod 可能暂时还在,但无法创建、删除、扩缩容

现象 2:工作节点失联后,业务恢复很慢

某个节点断网后:

  • 节点状态变成 NotReady
  • Pod 长时间停留在旧节点上
  • Deployment 有副本,但新 Pod 迟迟没补起来
  • Service 端点更新不及时,流量还可能打到故障节点

现象 3:etcd 正常率低导致控制面抖动

常见表现:

  • kubectl get pods -A 偶发卡住
  • API Server 日志出现 etcd request timed out
  • leader 频繁切换
  • 集群并未完全挂,但“哪哪都慢”

核心原理

这一节很关键。很多排查做不下去,是因为不知道 Kubernetes 到底按什么机制在“自愈”。

1. 高可用控制面的基本结构

一个相对标准的生产架构通常包含:

  • 3 台及以上控制面节点
  • 前置负载均衡器或 VIP
  • 每台控制面运行:
    • kube-apiserver
    • kube-controller-manager
    • kube-scheduler
    • etcd(堆叠式)或外部 etcd 集群

核心目标是:任意 1 台控制面节点故障,不影响 API 对外服务与控制循环继续运转

flowchart LR
    U[运维/CI/CD/kubectl] --> LB[负载均衡器 / VIP]
    LB --> CP1[Control Plane 1]
    LB --> CP2[Control Plane 2]
    LB --> CP3[Control Plane 3]

    CP1 --> E1[(etcd 1)]
    CP2 --> E2[(etcd 2)]
    CP3 --> E3[(etcd 3)]

    CP1 --> W1[Worker 1]
    CP2 --> W2[Worker 2]
    CP3 --> W3[Worker 3]

关键点

  • API Server 可多活
  • Controller Manager 和 Scheduler 通过 Leader Election 保证单活控制
  • etcd 依赖多数派(quorum)
    • 3 节点 etcd,允许挂 1 台
    • 5 节点 etcd,允许挂 2 台
  • 负载均衡器必须做健康检查,不能把流量继续打到失效 API Server

2. 工作节点故障自愈链路

工作节点出问题后,Kubernetes 并不是“立刻重建”,而是按一条链路逐步感知和处理:

  1. kubelet 周期性向 API Server 上报节点状态
  2. Node Controller 发现心跳超时
  3. 节点状态从 Ready 变为 NotReadyUnknown
  4. 节点被自动打上 taint,例如:
    • node.kubernetes.io/not-ready
    • node.kubernetes.io/unreachable
  5. 控制器按 Pod 的容忍时间决定是否驱逐
  6. Deployment/StatefulSet/DaemonSet 再按各自机制重建
sequenceDiagram
    participant K as kubelet
    participant A as API Server
    participant N as Node Controller
    participant S as Scheduler
    participant D as Deployment/ReplicaSet

    K->>A: 定期上报 NodeStatus/Lease
    A->>N: 节点状态更新
    Note over K,A: 节点故障或网络中断
    N->>A: 标记节点 NotReady/Unknown
    N->>A: 添加 unreachable/not-ready taint
    D->>A: 发现副本不足
    S->>A: 为新 Pod 选择健康节点
    A->>D: 新副本运行

这里最容易踩坑的点

很多人以为节点一 NotReady,Pod 就会马上迁走。其实不一定。

因为默认还受这些因素影响:

  • pod-eviction-timeout
  • Pod 是否配置了 tolerations
  • 是否使用本地存储
  • PDB(PodDisruptionBudget)是否限制驱逐
  • StatefulSet 是否存在有序重建约束

3. 控制面高可用的“真瓶颈”常在 etcd

控制面组件看起来很多,但真正最脆弱、也最容易被忽略的是 etcd。

etcd 决定了:

  • API Server 读写延迟
  • leader 选举稳定性
  • 资源对象一致性

如果 etcd 有这些问题:

  • 磁盘延迟高
  • 时钟漂移大
  • 网络丢包/抖动
  • 频繁 compact/defrag 不当

那么表面上看是“API Server 不稳定”,实际上根子在存储层。

stateDiagram-v2
    [*] --> Healthy
    Healthy --> Degraded: 磁盘/网络延迟升高
    Degraded --> LeaderFlap: 选主频繁切换
    LeaderFlap --> APISlow: API 请求超时
    APISlow --> SchedulingDelay: 调度变慢/控制器积压
    SchedulingDelay --> ServiceImpact: 业务恢复变慢
    Degraded --> Healthy: 恢复网络/磁盘性能
    APISlow --> Healthy: etcd 稳定后恢复

定位路径

下面给一条我更推荐的排查顺序:先判断是控制面问题,还是节点问题,再往下钻。

第一步:确认 API Server 是否稳定

kubectl get --raw='/readyz?verbose'
kubectl get --raw='/livez?verbose'
kubectl get componentstatuses

如果 readyz 都不稳定,先别急着查业务 Pod,优先看:

  • 负载均衡器健康检查
  • API Server 日志
  • etcd 状态

第二步:确认 etcd 健康

如果是 kubeadm 默认堆叠式 etcd,通常在控制面节点本机执行:

export ETCDCTL_API=3

etcdctl \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/peer.crt \
  --key=/etc/kubernetes/pki/etcd/peer.key \
  --endpoints=https://127.0.0.1:2379 \
  endpoint health

etcdctl \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/peer.crt \
  --key=/etc/kubernetes/pki/etcd/peer.key \
  --endpoints=https://127.0.0.1:2379 \
  endpoint status -w table

重点看:

  • 是否所有成员都 healthy
  • leader 是否频繁变化
  • raft term 是否异常跳变
  • db size 是否持续膨胀

第三步:确认节点心跳与 taint

kubectl get nodes -o wide
kubectl describe node <node-name>
kubectl get leases -n kube-node-lease

尤其看:

  • Ready 状态
  • 最近心跳时间
  • 是否出现 node.kubernetes.io/unreachable
  • Conditions 中的 NetworkUnavailableMemoryPressureDiskPressure

第四步:确认 Pod 为什么没被迁走

kubectl get pod -A -o wide | grep <node-name>
kubectl describe pod <pod-name> -n <namespace>
kubectl get pdb -A

排查方向:

  • 是否被 PDB 卡住
  • 是否有长时间 toleration
  • 是否使用本地卷导致无法快速迁移
  • 是否存在 finalizer 或 CNI 卸载不完整

实战代码(可运行)

下面给一套偏实战的最小落地方案,目标是:

  • 3 控制面节点
  • 通过 kubeadm 初始化高可用集群
  • 工作负载具备基本反亲和与中断保护
  • 故障时能更快完成节点级自愈

1. kubeadm 高可用控制面配置

假设前面有一个负载均衡地址 10.0.0.100:6443

apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.27.6
controlPlaneEndpoint: "10.0.0.100:6443"
networking:
  podSubnet: "10.244.0.0/16"
apiServer:
  certSANs:
    - "10.0.0.100"
    - "k8s-api.internal"
controllerManager:
  extraArgs:
    node-monitor-grace-period: "20s"
    pod-eviction-timeout: "30s"
scheduler: {}
etcd:
  local:
    dataDir: /var/lib/etcd
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "iptables"

初始化首个控制面节点:

kubeadm init --config kubeadm-ha.yaml --upload-certs

加入其他控制面节点:

kubeadm join 10.0.0.100:6443 \
  --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash> \
  --control-plane \
  --certificate-key <certificate-key>

加入工作节点:

kubeadm join 10.0.0.100:6443 \
  --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

说明:
node-monitor-grace-periodpod-eviction-timeout 可以缩短故障发现与驱逐时间,但别调得过激。网络稍微抖一下就误判,也会带来大量无意义迁移。我一般建议先在测试环境压测再定值。


2. 业务工作负载的高可用部署示例

下面这个 Deployment 做了几件事:

  • 3 副本
  • 节点反亲和,尽量打散
  • 就绪探针确保流量不进故障实例
  • 存活探针触发容器级自愈
  • 搭配 PDB,避免维护时全部一起掉
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-ha
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-ha
  template:
    metadata:
      labels:
        app: web-ha
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                topologyKey: kubernetes.io/hostname
                labelSelector:
                  matchLabels:
                    app: web-ha
      containers:
        - name: nginx
          image: nginx:1.25
          ports:
            - containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 3
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2
          livenessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-ha-pdb
  namespace: default
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: web-ha

部署:

kubectl apply -f web-ha.yaml
kubectl get pods -o wide

3. 模拟工作节点故障并验证自愈

先找一个有业务 Pod 的节点:

kubectl get pods -o wide
kubectl get nodes

然后在该节点上模拟 kubelet 停止:

sudo systemctl stop kubelet

观察节点状态:

kubectl get nodes -w

观察 Pod 重建过程:

kubectl get pods -o wide -w

预期现象:

  • 节点先变 NotReady
  • 原节点上的 Pod 不会瞬间消失
  • 超过驱逐阈值后,新 Pod 会被调度到其他健康节点
  • Service 端点会逐步切换到新 Pod

4. 一个简单的自动检查脚本

这个脚本用于快速检查控制面和节点状态,适合故障时先跑一遍。

#!/usr/bin/env bash
set -euo pipefail

echo "== API Server Readyz =="
kubectl get --raw='/readyz?verbose' || true
echo

echo "== Nodes =="
kubectl get nodes -o wide
echo

echo "== Non-Running Pods =="
kubectl get pods -A --field-selector=status.phase!=Running || true
echo

echo "== Node Leases =="
kubectl get leases -n kube-node-lease
echo

echo "== Events (tail) =="
kubectl get events -A --sort-by=.lastTimestamp | tail -n 30 || true

保存为 cluster-health-check.sh 后执行:

chmod +x cluster-health-check.sh
./cluster-health-check.sh

常见坑与排查

这一节我会把最容易碰到、而且最容易误判的坑直接列出来。

坑 1:有 3 个控制面节点,但入口还是单点

常见配置:

  • 前面只放了 1 台 Nginx/HAProxy
  • 没有 Keepalived/VIP
  • 或 DNS 只指向单 IP

结果是:

  • 控制面节点虽然多台
  • 但流量入口挂了,集群照样不可用

排查方式

nc -zv 10.0.0.100 6443
curl -k https://10.0.0.100:6443/livez

止血方案

  • 临时改 /etc/hosts 指向可用 API Server
  • 或直接用某个健康控制面节点 IP 进行紧急操作
  • 后续补齐双 LB 或 VIP 漂移方案

坑 2:节点故障后 Pod 长时间不迁移

常见原因

  • pod-eviction-timeout 太长
  • Pod 有默认 300 秒 tolerationSeconds
  • PDB 限制了驱逐
  • StatefulSet + PVC 绑定导致切换慢

排查方式

kubectl describe node <node-name>
kubectl describe pod <pod-name> -n <ns>
kubectl get pdb -A

止血方案

如果明确节点已经不可恢复,可手动驱逐或删除 Node:

kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data --force
kubectl delete node <node-name>

注意:
drain 适合节点还通的时候。
如果节点已经彻底失联,通常是 delete node 让控制平面尽快放弃旧状态。
但涉及本地卷、StatefulSet 时,一定先确认数据一致性。


坑 3:livenessProbe 配太猛,自己把自己打死

这是我踩过的一个坑。应用冷启动需要 20 秒,我把 livenessProbe 配成了 5 秒就检查,结果容器一直重启,看起来像“集群不稳定”,其实是探针误杀。

建议

  • readinessProbe 控流量
  • livenessProbe 控重启
  • 慢启动应用优先加 startupProbe

示例:

apiVersion: v1
kind: Pod
metadata:
  name: probe-demo
spec:
  containers:
    - name: app
      image: nginx:1.25
      startupProbe:
        httpGet:
          path: /
          port: 80
        failureThreshold: 30
        periodSeconds: 2
      readinessProbe:
        httpGet:
          path: /
          port: 80
        periodSeconds: 5
      livenessProbe:
        httpGet:
          path: /
          port: 80
        initialDelaySeconds: 20
        periodSeconds: 10

坑 4:etcd 磁盘性能差,所有问题都像“玄学”

表现

  • API 请求偶发超时
  • 控制器处理慢
  • leader 切换频繁
  • 但 CPU、内存看起来并不高

排查建议

在 etcd 节点上看磁盘延迟:

iostat -x 1

看 etcd 日志:

journalctl -u etcd -f

如果是容器化静态 Pod:

crictl ps | grep etcd
crictl logs <container-id>

止血方案

  • etcd 数据盘用 SSD
  • 避免与业务 IO 混跑
  • 定期 compact/defrag,但不要在高峰时段做

安全/性能最佳实践

高可用和自愈做得越深,越不能忽视安全和性能边界。否则很容易变成“故障恢复快了,但平时更脆”。

安全最佳实践

1. 控制面入口最小暴露

  • API Server 只对内网开放
  • 外部访问走堡垒机或专用代理
  • 负载均衡器加白名单和审计

2. etcd 一定启用 TLS

etcd 保存的是整个集群状态,基本就是核心资产。证书、网络隔离、访问控制都不能省。

3. RBAC 最小权限

自动化巡检脚本、CI/CD 账号不要直接给 cluster-admin
很多“误操作导致全局故障”的根源,不是系统自愈差,而是权限过大。

4. 审计日志要保留

至少保留:

  • API Server audit log
  • 关键节点系统日志
  • etcd 日志
  • CNI 插件日志

故障后没有日志,基本就是盲飞。


性能最佳实践

1. 不要把故障检测时间调得过低

参数调太小会有副作用:

  • 网络轻微抖动就误判节点失联
  • Pod 频繁迁移
  • 对存储型业务影响很大

建议:

  • 延迟敏感无状态业务可激进些
  • 有状态业务要保守
  • 先压测,再上线

2. 业务副本要跨节点、最好跨可用区

  • 至少做 podAntiAffinity
  • 更进一步做 topology spread constraints
  • 如果是云上,多可用区部署收益很大

3. 为关键系统组件预留资源

比如:

  • CoreDNS
  • kube-proxy
  • CNI 组件
  • ingress controller

这些组件没资源,业务再多副本也没意义。

4. 监控别只看 Pod,要看控制循环时延

建议重点监控:

  • API Server 请求延迟
  • etcd fsync 延迟
  • controller workqueue 积压
  • scheduler latency
  • 节点 lease 更新间隔

方案取舍分析

在控制面和自愈策略上,没有“唯一标准答案”,只有更适合当前阶段的取舍。

方案 1:堆叠式 etcd

即 etcd 与控制面在同一批节点上。

优点

  • 部署简单
  • 成本较低
  • kubeadm 支持成熟

缺点

  • 控制面和存储耦合
  • 故障域不够独立
  • 节点资源竞争更明显

适用场景

  • 中小规模集群
  • 团队运维能力一般
  • 追求快速落地

方案 2:外部 etcd 集群

优点

  • 控制面与数据存储解耦
  • 更容易独立扩展与维护
  • 故障隔离更清晰

缺点

  • 部署维护复杂度更高
  • 网络链路要求更高
  • 对团队经验有要求

适用场景

  • 多集群共享规范
  • 对控制面稳定性要求很高
  • 有专门平台团队维护

止血方案

当线上已经出问题时,不要一上来就“彻底修复”,先做止血。

场景 1:控制面入口挂了

  • 临时直连健康 API Server 节点
  • 校验 /readyz
  • 恢复或切换 LB/VIP

场景 2:某工作节点故障且不可恢复

  • 先确认业务副本是否已补齐
  • 删除故障 Node 对象
  • 必要时手工清理云主机或虚机残留

场景 3:大面积 Pod 重建慢

  • 检查镜像拉取是否受限
  • 检查 CNI / CoreDNS 是否正常
  • 暂时提升关键业务优先级
  • 必要时手工扩容健康节点池

总结

如果把 Kubernetes 高可用和故障自愈浓缩成一句话,我会这样说:

控制面负责“还能不能管”,工作节点自愈负责“业务多久恢复”。

真正可落地的设计,至少要做到这几件事:

  1. 控制面三节点起步,入口不能单点
  2. etcd 保证多数派与稳定 IO
  3. 节点故障检测参数要结合业务特性调优
  4. 工作负载本身要配合自愈
    • 多副本
    • 反亲和
    • 合理探针
    • 合理 PDB
  5. 排查顺序要对
    • 先 API Server
    • 再 etcd
    • 再 Node
    • 最后看 Pod 和业务层

最后给一个很实际的建议:
不要只在文档里写“我们集群支持高可用”,一定要定期做故障演练,比如:

  • 关一台控制面节点
  • 断一个工作节点网络
  • 模拟 etcd 单成员异常
  • 演练 drain、delete node、Pod 自动补副本

你只有亲手演练过,才知道你的“高可用”到底是设计出来的,还是想象出来的。


分享到:

上一篇
《分布式架构中一致性与可用性的取舍:基于 Redis、消息队列与幂等设计的高并发订单系统实战》
下一篇
《区块链节点数据同步与状态存储优化实战:从全节点部署到性能调优》