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

《Kubernetes 集群高可用架构设计与故障切换实战指南》

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

Kubernetes 集群高可用架构设计与故障切换实战指南

很多团队一开始做 Kubernetes 集群,目标都很朴素:先跑起来。但业务一旦真上量,问题就会很现实:

  • 单个 master 挂了,集群还能不能管?
  • etcd 抖动一下,API Server 为什么全红?
  • 节点宕机后,Pod 为啥迟迟没漂移?
  • 负载均衡切换了,kubectl 却还是连不上?

我自己第一次接手生产集群高可用改造时,最大的感受是:Kubernetes 的“高可用”不是加几个副本那么简单,它是控制面、数据面、存储面、网络面一起配合的结果。
这篇文章不打算只讲概念,而是从故障现象、架构设计、切换原理、可运行示例、排障路径、止血手段几个角度,把这件事讲透。


背景与问题

在 Kubernetes 里,“高可用”通常至少包含两层意思:

  1. 控制面高可用
    kube-apiserver、kube-controller-manager、kube-scheduler、etcd 任一节点故障,不影响集群继续管理和调度。

  2. 工作负载高可用
    某个 worker 节点、Pod、网络链路异常时,业务流量能自动绕过故障点,服务尽量不中断。

在实际生产里,常见故障现象通常长这样:

  • kubectl get pod 卡住或超时
  • 新 Pod 无法调度,但旧业务似乎还在跑
  • Deployment 副本数足够,但 Service 访问间歇性失败
  • 节点 NotReady 很久,Pod 不迁移
  • etcd leader 频繁切换,控制面整体抖动

这些问题背后,往往不是单点故障本身,而是高可用链路上某一环设计不完整


先明确一个目标:Kubernetes 高可用到底要保什么

如果你要设计一套“靠谱”的 K8s 高可用架构,我建议先把目标拆成 4 个问题:

  • API 是否持续可访问
  • etcd 是否多数派可写
  • 控制器和调度器是否能自动选主
  • 业务流量是否能自动绕过故障节点

换句话说,K8s 高可用不是“所有节点都不挂”,而是:

在部分节点或组件失效时,集群仍然具备核心能力,并在可接受时间内完成故障切换。


核心原理

这一节不绕,直接讲最关键的几个机制。

1. 控制面高可用的基本拓扑

生产环境常见方案是:

  • 3 台或 5 台 control plane 节点
  • 前面挂一个 VIP 或四层负载均衡器
  • 每个节点运行 kube-apiserver
  • kube-controller-manager / kube-scheduler 以多副本运行,通过 leader election 工作
  • etcd 采用奇数节点形成 quorum

下面这张图可以先建立整体认知。

flowchart TB
    U[运维/CI/CD/kubectl] --> LB[VIP / LoadBalancer / HAProxy]
    LB --> A1[kube-apiserver CP1]
    LB --> A2[kube-apiserver CP2]
    LB --> A3[kube-apiserver CP3]

    A1 --> E1[etcd-1]
    A2 --> E2[etcd-2]
    A3 --> E3[etcd-3]

    C1[kube-controller-manager CP1] -. leader election .- C2[kube-controller-manager CP2]
    C2 -. leader election .- C3[kube-controller-manager CP3]

    S1[kube-scheduler CP1] -. leader election .- S2[kube-scheduler CP2]
    S2 -. leader election .- S3[kube-scheduler CP3]

    A1 --> W1[worker-1]
    A2 --> W2[worker-2]
    A3 --> W3[worker-3]

2. etcd 决定了控制面的“命门”

很多人把 API Server 当成控制面的核心,但真正决定生死的是 etcd

原因很简单:

  • kube-apiserver 是无状态的,多实例容易横向扩展
  • etcd 保存了集群状态,是一致性存储
  • etcd 要想正常写入,必须满足多数派(quorum)

例如 3 节点 etcd:

  • 存活 3 台:可读可写
  • 存活 2 台:可读可写
  • 存活 1 台:通常不可写,集群控制面基本瘫痪

所以高可用设计里,宁可先确认 etcd 稳不稳,也别只盯着 apiserver 副本数。

3. Controller Manager 和 Scheduler 通过选主避免“双写”

控制器管理器和调度器通常会多副本部署,但并不是所有实例同时执行业务逻辑,而是通过 Leader Election 选出一个 leader。

它的意义是:

  • leader 挂了,其他实例会接管
  • 避免多个实例同时更新同一资源,造成状态冲突

这类切换一般是秒级到十几秒级,具体取决于:

  • --leader-elect
  • --leader-elect-lease-duration
  • --leader-elect-renew-deadline
  • --leader-elect-retry-period

4. 节点故障切换不是“瞬时”的

这是非常容易误判的一点。

一个 worker 节点宕机后,Pod 不会在 1 秒内立刻迁走,通常流程是:

  1. kubelet 心跳中断
  2. Node Controller 发现节点异常
  3. 节点状态变成 NotReady
  4. 超过 pod-eviction-timeout 或 taint 相关阈值
  5. 控制器重新创建 Pod
  6. Service / EndpointSlice 更新流量转发目标

过程图如下:

sequenceDiagram
    participant Kubelet as kubelet(NodeA)
    participant APIServer as kube-apiserver
    participant NodeCtl as node-controller
    participant Scheduler as kube-scheduler
    participant NodeB as NodeB

    Kubelet->>APIServer: 定期上报心跳
    Note over Kubelet: 节点宕机/网络中断
    NodeCtl->>APIServer: 检测心跳超时
    NodeCtl->>APIServer: 将 NodeA 标记为 NotReady
    NodeCtl->>APIServer: 驱逐受影响 Pod
    Scheduler->>APIServer: 为新 Pod 选择节点
    Scheduler->>NodeB: 调度到 NodeB
    NodeB->>APIServer: 新 Pod Ready

5. 业务高可用依赖的不只是 Pod 副本

即使你写了 replicas: 3,也不代表高可用已经完成。还要看:

  • 副本有没有分散到不同节点
  • 有没有跨可用区分布
  • 有没有 PodDisruptionBudget
  • 有没有健康检查
  • 有没有拓扑感知调度
  • Service 是否正确剔除不健康端点

方案设计:一套更贴近生产的 HA 架构

这里给一套中型生产集群常见设计。

推荐拓扑

  • 3 台 control plane
  • 3 台 etcd(与 control plane 同机或独立,取决于规模)
  • 2 台 HAProxy + Keepalived 提供 VIP
    或者云上用 SLB/NLB
  • 至少 3 台 worker
  • 容器网络插件选择支持 NetworkPolicy、稳定性较好的方案,例如 Calico / Cilium

取舍建议

方案优点缺点适用场景
单 control plane + 单 etcd成本低,搭建快单点明显测试环境
3 control plane + stacked etcd结构简单,常见control plane 与 etcd 资源竞争中小型生产
3 control plane + external etcd职责分离,稳定性更好运维复杂度更高中大型生产
云厂商托管控制面省运维,稳定性高可控性较弱,成本可能更高云上优先

控制面状态切换图

stateDiagram-v2
    [*] --> Healthy
    Healthy --> APIServerDegraded: 单个 apiserver 故障
    APIServerDegraded --> Healthy: LB 切走故障实例

    Healthy --> LeaderSwitching: scheduler/controller leader 故障
    LeaderSwitching --> Healthy: 新 leader 产生

    Healthy --> EtcdMinorFailure: 单个 etcd 节点故障
    EtcdMinorFailure --> Healthy: 成员恢复且重新加入

    EtcdMinorFailure --> ControlPlaneUnavailable: etcd 失去多数派
    LeaderSwitching --> ControlPlaneUnavailable: 多个关键组件同时失效
    ControlPlaneUnavailable --> [*]

现象复现:模拟节点故障与控制面切换

排障之前,我建议先在测试环境里亲手做一次“可控故障演练”。不做演练,很多参数只停留在纸面上。

场景 1:模拟 worker 节点宕机

先部署一个 3 副本 Nginx 服务。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ha-nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ha-nginx
  template:
    metadata:
      labels:
        app: ha-nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.25
          ports:
            - containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 3
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 10
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: ha-nginx
spec:
  selector:
    app: ha-nginx
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP

应用:

kubectl apply -f ha-nginx.yaml
kubectl get pod -o wide -l app=ha-nginx

然后把某个节点停机,或者先做一个温和点的操作:

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

观察:

kubectl get node -w
kubectl get pod -o wide -l app=ha-nginx -w
kubectl get endpointslice -l kubernetes.io/service-name=ha-nginx -w

你会看到:

  • 节点状态变化
  • Pod 被逐出后在其他节点重建
  • EndpointSlice 更新,流量目标变化

场景 2:模拟单个 apiserver 故障

如果前面挂了 HAProxy/NLB,可以直接在某个 control plane 节点上停止 apiserver:

sudo systemctl stop kubelet

或者如果是静态 Pod 模式,临时移走 manifest(测试环境才建议这么做):

sudo mv /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/

然后在客户端连续访问:

for i in {1..20}; do
  kubectl get nodes >/dev/null && echo "ok-$i" || echo "fail-$i"
  sleep 1
done

如果负载均衡和健康检查配置合理,通常只会有极短时间抖动,或者几乎无感。


实战代码(可运行)

这一节给出一套更完整的可运行示例:
通过 Pod 反亲和 + PDB + 拓扑分散,提升业务在节点故障下的存活能力。

1. 高可用 Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-ha
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-ha
  template:
    metadata:
      labels:
        app: web-ha
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: web-ha
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: web-ha
              topologyKey: kubernetes.io/hostname
      containers:
        - name: web
          image: nginx:1.25
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
          readinessProbe:
            httpGet:
              path: /
              port: 80
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 10
            periodSeconds: 10

2. PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-ha-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: web-ha

3. Service

apiVersion: v1
kind: Service
metadata:
  name: web-ha
spec:
  selector:
    app: web-ha
  ports:
    - name: http
      port: 80
      targetPort: 80
  type: ClusterIP

应用命令:

kubectl apply -f web-ha.yaml
kubectl apply -f web-ha-pdb.yaml
kubectl apply -f web-ha-svc.yaml

kubectl get pods -o wide -l app=web-ha
kubectl get pdb

4. 一个简单的故障切换验证脚本

下面这个 Bash 脚本会持续访问 Service,并打印结果,适合在你 drain 某个节点时观察业务是否出现明显中断。

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

NAMESPACE=default
SERVICE=web-ha
PORT=80

while true; do
  if kubectl run curl-tmp \
    --rm -i --restart=Never \
    --image=curlimages/curl:8.5.0 \
    --command -- \
    curl -s -o /dev/null -w "%{http_code}\n" http://${SERVICE}.${NAMESPACE}.svc.cluster.local:${PORT}; then
    echo "$(date '+%F %T') request ok"
  else
    echo "$(date '+%F %T') request failed"
  fi
  sleep 2
done

保存为 check-failover.sh 后执行:

chmod +x check-failover.sh
./check-failover.sh

5. 控制面健康检查脚本

这个脚本适合在每个 control plane 节点上执行,用来快速检查 apiserver、etcd、关键组件状态。

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

echo "== kube-apiserver /readyz =="
kubectl get --raw='/readyz?verbose' | head -n 30 || true

echo
echo "== component pods =="
kubectl get pods -n kube-system -o wide | egrep 'etcd|kube-apiserver|kube-controller-manager|kube-scheduler' || true

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

echo
echo "== etcd health (if etcdctl exists) =="
if command -v etcdctl >/dev/null 2>&1; then
  export ETCDCTL_API=3
  etcdctl \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
    --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
    endpoint health --cluster
else
  echo "etcdctl not found"
fi

定位路径:出了故障,到底先查哪里

我比较推荐按“从外到内、从现象到根因”的顺序排查。不要一上来就翻几十页日志。

第 1 步:确认是控制面故障,还是业务面故障

先问自己两个问题:

  1. kubectl get nodes 能不能正常返回?
  2. 集群内业务流量是不是只有部分失败?

如果 kubectl 都连不上,优先看:

  • VIP / LB 是否存活
  • apiserver 进程是否在
  • 6443 端口是否健康
  • 证书是否过期
  • etcd 是否有多数派

如果 kubectl 正常,但业务访问失败,优先看:

  • Service 对应 Endpoints / EndpointSlice
  • Pod readiness
  • CNI 网络
  • kube-proxy / eBPF datapath
  • 节点路由与安全组

第 2 步:检查 Node 状态与事件

kubectl get nodes
kubectl describe node <node-name>
kubectl get events -A --sort-by='.lastTimestamp' | tail -n 50

重点看:

  • NotReady
  • NetworkUnavailable
  • DiskPressure
  • MemoryPressure
  • Taint 是否导致 Pod 无法落盘

第 3 步:检查 Pod 是否真的“健康”

很多时候 Pod 是 Running,但并不代表可服务。

kubectl get pod -A -o wide
kubectl describe pod <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> --previous

重点看:

  • readinessProbe 失败
  • livenessProbe 把容器反复拉起
  • OOMKilled
  • ImagePullBackOff
  • 节点网络问题导致探针超时

第 4 步:检查控制面组件日志

如果是 kubeadm 部署,控制面常以静态 Pod 运行:

kubectl get pod -n kube-system -o wide | egrep 'kube-apiserver|kube-controller-manager|kube-scheduler|etcd'
kubectl logs -n kube-system <apiserver-pod-name>
kubectl logs -n kube-system <etcd-pod-name>

或者直接上节点看容器运行时日志。

第 5 步:确认 etcd 是否发生 leader 抖动或丢失多数派

export ETCDCTL_API=3

etcdctl \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
  --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
  endpoint status --cluster -w table

关注:

  • 是否所有 endpoint 都 healthy
  • leader 是否频繁变化
  • raft term 是否异常增长
  • db size 是否过大
  • 延迟是否明显升高

常见坑与排查

这里我把最容易踩的坑列出来,很多都是线上真实高频问题。

坑 1:以为 2 节点 etcd 也能高可用

这是个经典误区。
2 节点不是高可用,而是高风险

因为 etcd 依赖多数派:

  • 2 个节点,多数派是 2
  • 挂 1 个就失去 quorum

建议:etcd 一定使用奇数节点,通常 3 或 5。


坑 2:LB 只做了转发,没做健康检查

你可能部署了多个 apiserver,但如果负载均衡器不会摘除故障后端,那客户端还是会不断打到坏节点。

症状

  • kubectl 间歇性失败
  • 有时成功,有时 TLS/EOF 超时

建议

  • 对 6443 做 TCP 健康检查
  • 更进一步可探测 /livez/readyz
  • 故障后端要自动摘除,恢复后自动加入

坑 3:Pod 有副本,但都调度到同一台节点

这在资源紧张或者没做反亲和时非常常见。

症状

  • Deployment 3 副本都在一个 worker
  • 一台节点宕机,业务全没

建议

  • 配置 podAntiAffinity
  • 使用 topologySpreadConstraints
  • 跨可用区时按 zone 做拓扑分散

坑 4:PDB 配置不当,导致升级/驱逐卡死

比如只有 2 个副本,你设置了 minAvailable: 2
这会让 drain、升级、节点维护无法继续。

建议

  • PDB 要结合副本数和维护策略设计
  • 不是越严格越好
  • 先明确“允许同时损失几个副本”

坑 5:Readiness 没配,故障切换看起来“慢得离谱”

如果没有 readinessProbe,Pod 可能刚启动就被加入流量;
如果探针太宽松,异常 Pod 也可能长时间不被摘掉。

建议

  • 所有对外服务都配 readinessProbe
  • livenessProbe 不要过于激进
  • 探针阈值要和应用启动时间匹配

坑 6:节点故障后 Pod 没迁移,以为调度器坏了

很多时候不是调度器故障,而是:

  • StatefulSet 卷无法挂载到新节点
  • 本地盘数据绑死
  • DaemonSet 不会“迁移”
  • PDB 阻止驱逐
  • 节点污点与容忍配置冲突

止血方案

  1. 先确认是不是无状态业务
  2. 必要时手工删除卡死 Pod
  3. 检查 PVC / StorageClass / VolumeAttachment
  4. 临时放宽 PDB 或修正污点策略

坑 7:etcd 磁盘慢,控制面像“时好时坏”

这个问题很隐蔽。
控制面不是完全挂掉,而是偶发超时、leader 切换、写入慢。

排查方向

  • 磁盘 IOPS
  • fsync 延迟
  • etcd compaction / defrag 是否长期没做
  • 是否与其他高 IO 任务混部

建议

  • etcd 使用低延迟 SSD
  • 避免与重 IO 业务混部
  • 定期 compact / defrag
  • 做磁盘与延迟监控

止血方案:线上已经出问题了,先怎么保业务

这部分是 troubleshooting 场景里很关键的一段。
理想很重要,但线上先恢复服务更重要。

情况 1:单个 apiserver 故障

操作建议

  1. 确认 LB 已摘除故障节点
  2. 检查该节点 kubelet、容器运行时、证书、磁盘
  3. 恢复后再重新加入流量池

不要做的事

  • 未确认原因就重启所有控制面节点
  • 在 etcd 不稳定时同时操作多个 control plane

情况 2:单个 etcd 成员故障,但集群仍有 quorum

操作建议

  1. 先确认剩余 etcd 成员健康
  2. 检查故障节点网络、磁盘、证书
  3. 如数据目录损坏,按官方流程移除并重新加入成员
  4. 先稳住 quorum,再谈修复

原则

  • 3 节点 etcd 挂 1 个,还能撑住
  • 但这时已经没有冗余,不要再动第二个

情况 3:worker 故障,业务副本不足

止血动作

  1. kubectl cordon 故障节点,防止新 Pod 再调度过去
  2. 若节点已不可恢复,删除卡死 Pod
  3. 临时扩容 Deployment 副本
  4. 若资源不足,优先保障核心服务的 requests/priorityClass

情况 4:LB/VIP 出问题,控制面其实没挂

这是很容易误判为“集群挂了”的情况。

检查命令

nc -zv <vip-or-lb> 6443
curl -k https://<vip-or-lb>:6443/livez

再分别登录各 control plane 节点:

curl -k https://127.0.0.1:6443/livez

如果节点本地正常、VIP 不通,那问题在 LB 层,不在 Kubernetes 本身。


安全/性能最佳实践

高可用如果只追求“不停机”,但把安全和性能弄丢了,最后还是会反噬运维成本。

安全最佳实践

1. etcd 必须启用 TLS

etcd 是集群大脑,证书、密钥、快照都要严管。
不要裸奔,更不要把客户端证书散落到普通节点。

2. 最小化控制面暴露面

  • 6443 仅开放给必要来源
  • etcd 端口只允许控制面内部访问
  • 使用安全组 / 防火墙限制管理面网络

3. 定期轮换证书并监控过期时间

证书过期是控制面“突然死亡”的高危原因之一。
特别是 kubeadm 集群,要定期检查:

kubeadm certs check-expiration

4. 对 RBAC 做权限收敛

很多排障脚本习惯性用 cluster-admin,但生产环境建议细化权限,避免误操作扩大影响面。


性能最佳实践

1. etcd 资源不要抠得太狠

  • 独占高性能磁盘优先
  • 预留足够 CPU 和内存
  • 避免控制面节点混跑重负载业务

2. 监控这几个关键指标

建议至少纳入告警:

  • apiserver 请求延迟、5xx
  • etcd leader changes
  • etcd fsync duration
  • node not ready
  • pod pending 数量
  • CoreDNS 延迟和错误率

3. 控制故障切换阈值,不要一味调太激进

有些团队为了“秒切”,把各种超时调得很低,结果网络轻微抖动就频繁误判。
高可用不是只看快,还要看稳定

4. 尽量做跨节点、跨可用区部署

如果业务确实关键,建议至少做到:

  • 副本跨节点
  • 核心服务跨 zone
  • 存储与网络方案支持跨故障域恢复

一份建议的验证清单

上线前,我建议至少做下面这些演练。只要你真做过一遍,心里会踏实很多。

  • 关停单个 worker,确认业务副本自动恢复
  • drain 单个 worker,确认 PDB 不会阻塞维护
  • 停掉单个 apiserver,确认 kubectl 基本无感
  • 停掉单个 controller-manager / scheduler,确认 leader 可切换
  • 停掉单个 etcd 成员,确认 quorum 正常
  • 模拟 VIP/LB 后端摘除,确认健康检查生效
  • 检查证书剩余有效期
  • 验证 etcd 快照可恢复
  • 验证监控与告警是否真正触发

总结

Kubernetes 的高可用,真正难的不是“搭一个三节点控制面”,而是把这些链路补齐:

  • API 入口高可用:VIP / LB + 健康检查
  • 控制组件高可用:多实例 + leader election
  • 状态存储高可用:etcd 奇数节点 + 多数派
  • 业务副本高可用:反亲和、拓扑分散、健康检查、PDB
  • 排障与演练能力:明确定位路径,提前做故障演习

如果你让我给一套最实用的落地建议,我会这样总结:

  1. 生产集群至少 3 control plane,不要把 etcd 做成 2 节点。
  2. 前置做 LB 健康检查,不然多 apiserver 没意义。
  3. 业务层必须加 readiness、反亲和、拓扑分散。
  4. 把 etcd 当成重点保护对象,优先保障磁盘和网络质量。
  5. 定期演练故障切换,不演练的高可用,基本等于没验证。

最后说句很实在的话:
高可用不是“永不故障”,而是故障来了你知道会怎么坏、坏到什么程度、多久能恢复。
只要这三件事你心里有数,这套 Kubernetes 集群就算真的具备生产级韧性了。


分享到:

上一篇
《微服务架构中分布式事务的实战落地:基于 Saga 模式的设计、补偿与一致性保障》
下一篇
《Node.js 中基于 Worker Threads 与事件循环监控的 CPU 密集型任务性能优化实战》