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

《Kubernetes 集群架构实战:基于多可用区的高可用控制平面与工作负载容灾设计》

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

背景与问题

很多团队第一次做 Kubernetes 高可用,脑海里的目标通常很朴素:“挂一台机器不要紧,业务别停。”
但真正落地时,会很快发现问题没这么简单:

  • 控制平面高可用,不等于业务高可用
  • 多副本部署,不等于跨可用区容灾
  • 节点分散到多个 AZ(Availability Zone,可用区),不等于整个系统具备故障隔离能力
  • etcd 能跑,不等于在网络抖动和少数节点失联时还能稳定选主

我自己在做多 AZ 集群设计时,最容易踩的坑有两个:

  1. 只关注 API Server 是否“有多个副本”
  2. 忽略调度、存储、流量入口这些“业务路径”上的单点

因此,这篇文章不只讲“怎么搭”,还会从控制平面、工作负载调度、网络入口、数据层、容量余量几个角度,把一个多可用区 Kubernetes 架构拆开讲清楚。


先给结论:多可用区高可用到底要解决什么

如果把 Kubernetes 集群看成一个系统,那么多 AZ 容灾通常要覆盖三类故障:

  1. 单节点故障
    某个 control-plane 或 worker 宕机

  2. 单可用区故障
    一个 AZ 整体不可用,或者网络严重退化

  3. 局部网络分区
    节点没挂,但 AZ 间链路抖动,导致 etcd、kubelet、ingress 行为异常

一个真正靠谱的方案,至少要做到:

  • 控制平面在失去一个 AZ 后仍可用
  • 工作负载可自动调度到健康 AZ
  • 入口流量能绕开故障 AZ
  • 关键状态数据有明确的持久化与恢复策略
  • 剩余 AZ 有足够容量接住故障转移后的负载

方案全景:推荐的多 AZ 集群架构

下面是一种比较常见、也比较稳妥的生产设计。

flowchart TB
    U[用户流量] --> GLB[全局/区域负载均衡]
    GLB --> ILB[控制平面 VIP / LB]
    GLB --> ING[Ingress / Gateway]

    subgraph AZ1[可用区 AZ-1]
      CP1[Control Plane 1\napiserver/controller/scheduler]
      ET1[etcd 1]
      W11[Worker 1]
      W12[Worker 2]
      ING1[Ingress Pod]
    end

    subgraph AZ2[可用区 AZ-2]
      CP2[Control Plane 2]
      ET2[etcd 2]
      W21[Worker 3]
      W22[Worker 4]
      ING2[Ingress Pod]
    end

    subgraph AZ3[可用区 AZ-3]
      CP3[Control Plane 3]
      ET3[etcd 3]
      W31[Worker 5]
      W32[Worker 6]
      ING3[Ingress Pod]
    end

    ILB --> CP1
    ILB --> CP2
    ILB --> CP3

    CP1 --- ET1
    CP2 --- ET2
    CP3 --- ET3
    ET1 --- ET2
    ET2 --- ET3
    ET1 --- ET3

    ING --> ING1
    ING --> ING2
    ING --> ING3

这个架构有几个关键点:

  • 3 个 control-plane,分别位于 3 个 AZ
  • etcd 采用 3 节点奇数仲裁
  • worker 节点跨 AZ 分布
  • 入口层(Ingress/Gateway)也跨 AZ 部署
  • 上层负载均衡能够做健康检查并剔除故障节点/故障区

如果你只有 2 个 AZ,不是不行,但仲裁和故障处理会别扭很多。
从架构完整性看,3 AZ 是最理想的基础盘


核心原理

1. 控制平面高可用的本质:API 可达 + etcd 可仲裁

控制平面是否高可用,核心看两件事:

  • kube-apiserver 是否仍然可访问
  • etcd 是否还能形成法定多数(quorum)

以 3 节点 etcd 为例:

  • 需要 2/3 存活才能正常读写
  • 如果失去 1 个节点,仍可用
  • 如果失去 2 个节点,集群不可写,通常也很难稳定服务

所以多 AZ 控制平面最常见的推荐就是:

  • 3 个 control-plane
  • 3 个 etcd 成员
  • 分散在 3 个 AZ

如果你把 3 个 control-plane 放在 2 个 AZ,例如 2 + 1 分布,那么一旦承载 2 个节点的那个 AZ 挂掉,etcd 直接失去多数派,控制平面就会瘫。


2. 工作负载容灾的本质:副本分散 + 调度约束 + 剩余容量

业务高可用不是“部署 3 个 Pod”就结束了,还要确保这些 Pod 不会全被调度到一个 AZ

Kubernetes 里最常用的几个机制:

  • topologySpreadConstraints:尽量按 AZ 均匀分布
  • podAntiAffinity:避免相同应用实例挤在一起
  • PodDisruptionBudget:限制主动驱逐时可同时中断的副本数
  • priorityClass:资源紧张时优先保关键业务
  • taint/toleration:给不同 AZ 或节点池做明确隔离策略

调度逻辑可以理解成下面这样:

sequenceDiagram
    participant D as Deployment
    participant S as Scheduler
    participant N1 as AZ-1 Nodes
    participant N2 as AZ-2 Nodes
    participant N3 as AZ-3 Nodes

    D->>S: 创建 6 个 Pod
    S->>N1: 调度一部分 Pod
    S->>N2: 调度一部分 Pod
    S->>N3: 调度一部分 Pod
    Note over S: 根据 topologySpreadConstraints\n和 anti-affinity 尽量打散
    N2--xS: AZ-2 故障
    S->>N1: 在健康节点补副本
    S->>N3: 在健康节点补副本

这背后有个现实问题:
剩余 AZ 必须有容量接住漂移过来的 Pod。

换句话说,多 AZ 容灾不能只讲“打散”,还要讲“接盘能力”。


3. 存储高可用不是 Kubernetes 自动送你的

很多人默认觉得:Pod 跨 AZ 调度了,业务就容灾了。
这在无状态服务上大体成立,但对有状态服务经常不成立。

例如:

  • 使用单 AZ 云盘的 PVC,Pod 被调度到别的 AZ 后,卷可能无法挂载
  • 本地盘 hostPath / local PV 更不适合 AZ 级漂移
  • 数据库即使跑在 K8s 上,也仍然要按数据库自己的复制和选主机制来设计

所以要明确区分:

  • 计算层容灾:Pod 能否在别的 AZ 拉起
  • 数据层容灾:Pod 拉起后,数据是否可用、是否一致

方案对比与取舍分析

方案 A:单集群跨 3 AZ

特点:

  • 一套 Kubernetes 集群
  • 3 个 control-plane 跨 3 个 AZ
  • worker 节点跨 3 AZ 分布

优点:

  • 运维面最统一
  • 集群资源池共享,调度灵活
  • 成本通常低于多集群

缺点:

  • 控制平面仍属于“一个集群”
  • 集群级误操作、版本缺陷、CNI 故障会同时影响所有 AZ
  • 大规模故障时,问题域更大

适用场景:

  • 大多数中型生产环境
  • 希望在成本和可用性之间平衡的团队

方案 B:每个 AZ 一个集群,再做多集群容灾

优点:

  • 故障域更清晰
  • 一个集群出问题不一定影响其他集群
  • 更适合强隔离场景

缺点:

  • 资源碎片化明显
  • 发布、配置、流量切换复杂度更高
  • 监控和治理成本提升

适用场景:

  • 金融、核心交易、强监管业务
  • 单集群风险不可接受的环境

我更推荐的落地思路

如果你正在从“单区单集群”演进到高可用:

  1. 先做好单集群跨 3 AZ
  2. 把入口、调度、PDB、存储策略补齐
  3. 再根据业务等级决定是否走多集群

也就是说,先把一个集群做扎实,再谈更复杂的容灾编排


容量估算:别让“高可用”只停留在拓扑图里

多 AZ 设计里最容易被忽略的是容量。

假设你有 3 个 AZ,每个 AZ 平时承担 1/3 业务。如果要求在失去 1 个 AZ 后仍维持服务,那么剩下两个 AZ 至少要能承接原本全部流量。

一个常见估算方式:

  • 正常流量总量:100%
  • 任一 AZ 故障后,剩余 2 个 AZ 接管全部流量
  • 那么每个健康 AZ 需要承担约 50%

也就是说,每个 AZ 平时最多只应该跑到约 50%~60% 的安全上限,为故障转移预留空间。

可以粗略理解为:

单 AZ 承载上限 ≈ 故障后目标负载 / 剩余 AZ 数量 + 弹性缓冲

如果你平时就把 3 个 AZ 都打满到 80%,那 AZ 失效后根本没有容灾空间,调度器只会不断报资源不足。


实战代码(可运行)

下面给一套偏实战的示例,重点演示:

  • 应用如何跨 AZ 均匀分布
  • AZ 故障时如何避免全部副本被动中断
  • 如何验证分布效果

1)一个跨可用区部署的无状态服务

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-ha
  namespace: default
spec:
  replicas: 6
  selector:
    matchLabels:
      app: web-ha
  template:
    metadata:
      labels:
        app: web-ha
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                topologyKey: topology.kubernetes.io/zone
                labelSelector:
                  matchLabels:
                    app: web-ha
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: web-ha
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: web-ha
      containers:
        - name: nginx
          image: nginx:1.25
          ports:
            - containerPort: 80
          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: 4
  selector:
    matchLabels:
      app: web-ha
---
apiVersion: v1
kind: Service
metadata:
  name: web-ha
  namespace: default
spec:
  selector:
    app: web-ha
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP

这里有几个设计点:

  • replicas: 6:让 3 个 AZ 更容易均匀铺开
  • topologySpreadConstraints:强约束按 zone 打散
  • podAntiAffinity:尽量避免副本扎堆
  • PDB minAvailable: 4:节点升级或主动驱逐时,保护最少可用副本数

2)验证 Pod 是否真的跨 AZ 分布

先查看节点标签:

kubectl get nodes -L topology.kubernetes.io/zone

部署应用:

kubectl apply -f web-ha.yaml

查看 Pod 落点:

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

如果想更直观看每个 Pod 在哪个 AZ,可以用下面的脚本:

kubectl get pods -l app=web-ha -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\n"}{end}' \
| while read pod node; do
    zone=$(kubectl get node "$node" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}')
    echo -e "$pod\t$node\t$zone"
  done

3)模拟一个 AZ 故障

在测试环境里,可以通过 cordon + drain 模拟某个 AZ 不可调度、节点被驱逐。

先找出某个 AZ 的节点:

kubectl get nodes -L topology.kubernetes.io/zone

假设要模拟 az-2 故障:

for node in $(kubectl get nodes -l topology.kubernetes.io/zone=az-2 -o name); do
  kubectl cordon "${node#node/}"
done

再驱逐业务 Pod:

for node in $(kubectl get nodes -l topology.kubernetes.io/zone=az-2 -o name); do
  kubectl drain "${node#node/}" \
    --ignore-daemonsets \
    --delete-emptydir-data \
    --force
done

然后观察 Pod 是否在其他 AZ 重建:

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

恢复时:

for node in $(kubectl get nodes -l topology.kubernetes.io/zone=az-2 -o name); do
  kubectl uncordon "${node#node/}"
done

注意:drain 会受到 PDB 影响,如果发现驱逐卡住,往往不是命令坏了,而是你的可用副本保护在生效。


4)给关键业务设置更高调度优先级

当 AZ 故障后,集群资源可能变紧。这时建议给关键业务设置优先级。

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: critical-service
value: 100000
globalDefault: false
description: "关键业务优先级,AZ 故障时优先保障"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: payment-api
  template:
    metadata:
      labels:
        app: payment-api
    spec:
      priorityClassName: critical-service
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: payment-api
      containers:
        - name: app
          image: registry.k8s.io/pause:3.9
          resources:
            requests:
              cpu: "200m"
              memory: "256Mi"

控制平面设计要点

1. API Server 的访问入口

控制平面通常需要一个统一访问入口,例如:

  • 云厂商内网 LB
  • keepalived + HAProxy
  • 外部四层负载均衡

要求是:

  • 后端只挂健康的 kube-apiserver
  • 健康检查足够敏感,但不要过度误判
  • 对 kubelet、kubectl、controller-manager 等客户端透明

2. etcd 放置策略

生产里最常见的是 stacked etcd,也就是 etcd 与 control-plane 同机部署。优点是简单,缺点是控制面和存储面耦合更深。

适合大多数场景的建议:

  • 3 节点 etcd,分别在 3 个 AZ
  • 使用 SSD
  • 独立资源保障,避免和普通业务争抢 I/O
  • 定期 snapshot
  • 关注延迟,不要忽略跨 AZ RTT

如果跨 AZ 网络延迟较高,etcd 的写入性能会明显受到影响,这个影响往往会传导到整个控制平面。


3. 不要让系统组件都挤在一个 AZ

除了业务 Pod,下面这些组件也应该跨 AZ:

  • CoreDNS
  • Ingress Controller / Gateway Controller
  • metrics-server
  • 日志与监控采集 agent 之外的中心组件
  • webhook / admission controller

否则你会看到一种很尴尬的情况:
控制平面没挂,但 DNS、入口或者准入链挂了,业务照样抖得很厉害。


常见坑与排查

坑 1:Pod 明明配了多副本,还是集中在一个 AZ

典型原因

  • 节点没有正确打 topology.kubernetes.io/zone 标签
  • 只配了 preferred,没有强约束
  • 某个 AZ 资源不足,调度器退而求其次
  • Cluster Autoscaler 没有按节点组补齐目标 AZ 容量

排查命令

kubectl get nodes --show-labels | grep topology.kubernetes.io/zone
kubectl describe pod <pod-name>
kubectl get events --sort-by=.lastTimestamp

我会怎么修

  • 先确认节点标签齐全
  • 对关键业务使用 whenUnsatisfiable: DoNotSchedule
  • 给各 AZ 节点池设置对称容量
  • 配合 HPA/CA 做容量联动

坑 2:AZ 故障后 Pod 起不来,提示卷无法挂载

典型原因

  • PVC 绑定的是单 AZ 存储
  • StorageClass 没有考虑跨 AZ 场景
  • StatefulSet 的卷拓扑和调度拓扑冲突

排查命令

kubectl describe pod <pod-name>
kubectl get pvc,pv
kubectl get storageclass

处理建议

  • 无状态服务尽量真正做到无状态
  • 有状态服务优先使用应用层复制
  • 明确云盘是否支持跨 AZ 附着
  • 不要指望所有存储都能跟着 Pod 跨区漂移

坑 3:控制平面节点都在,API 却时不时超时

典型原因

  • etcd 跨 AZ 延迟抖动
  • LB 健康检查策略不合理
  • webhook 响应慢,拖累 API 请求链路
  • CoreDNS 或网络插件异常导致系统组件互相调用变慢

排查思路

先看 API Server 和 etcd:

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

如果是 kubeadm 部署,可在控制平面节点上查看静态 Pod 日志:

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

再看 etcd 健康度:

ETCDCTL_API=3 etcdctl \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  endpoint health --cluster

坑 4:drain 时业务一直卡住

典型原因

  • PDB 过严
  • 剩余节点容量不足
  • Pod 使用了本地临时数据或 DaemonSet 相关限制

排查命令

kubectl get pdb
kubectl describe pdb <pdb-name>
kubectl top nodes
kubectl describe node <node-name>

建议

  • PDB 要结合真实副本数设计
  • 节点维护前先看容量余量
  • 关键业务和普通业务分优先级

安全/性能最佳实践

安全最佳实践

1. 控制平面只暴露必要入口

  • kube-apiserver 尽量走内网 LB
  • 限制来源网段
  • 审计日志必须开启
  • 访问控制交给 RBAC,别图省事给太大权限

2. etcd 必须启用 TLS

  • client 到 etcd
  • peer 到 peer
  • 证书轮换要有计划

3. Admission/Webhook 要高可用

很多团队把 webhook 当“附属组件”,其实它经常卡住整个创建链路。建议:

  • webhook 服务至少 2 副本跨 AZ
  • 设置合理超时
  • 区分 FailIgnore 策略,不要一刀切

性能最佳实践

1. 控制跨 AZ 延迟

etcd 对延迟很敏感。
如果你的 AZ 之间 RTT 较高,控制平面体验会明显下降,比如:

  • 创建资源变慢
  • controller 收敛变慢
  • 大规模滚动发布卡顿

建议:

  • 先测 AZ 间网络延迟
  • 对 etcd 使用高性能磁盘
  • 避免把控制平面和重 I/O 工作负载混跑

2. 资源请求要真实

如果所有业务都“虚报”很低的 requests,调度器会高估剩余容量,AZ 故障时就容易发生挤兑。
我一般建议:

  • 关键服务 requests 贴近稳态真实值
  • limit 不要乱设过高
  • 用历史监控数据校准资源模型

3. 做故障演练,而不是只看架构图

至少定期验证这些场景:

  • 单节点宕机
  • 单 AZ 不可调度
  • Ingress 某个 AZ 全部失效
  • 一个 control-plane 节点消失
  • etcd 单节点失联
  • 存储卷挂载失败

很多问题平时看不出来,一演练就原形毕露。


一份可执行的落地检查清单

如果你准备把现有集群升级成多 AZ 高可用,我建议按这个顺序检查:

flowchart TD
    A[节点分布到 3 AZ] --> B[3 个 control-plane 跨 AZ]
    B --> C[etcd 3 节点仲裁可用]
    C --> D[API Server 统一 LB 入口]
    D --> E[关键系统组件跨 AZ 部署]
    E --> F[业务 Deployment 加 topologySpreadConstraints]
    F --> G[PDB 与 PriorityClass 补齐]
    G --> H[确认存储是否支持跨 AZ 策略]
    H --> I[预留 N-1 容量]
    I --> J[执行 AZ 故障演练]

建议把这个清单真正写进你的上线标准,而不是停留在文档里。


总结

多可用区 Kubernetes 高可用,真正要做的不是“把节点分散开”,而是把控制面、调度面、流量入口、存储面、容量模型一起设计完整。

你可以记住这几个最关键的落地建议:

  1. 控制平面至少 3 节点,分布在 3 个 AZ
  2. etcd 用奇数仲裁,优先 3 节点,不要把多数派压在同一 AZ
  3. 工作负载用 topologySpreadConstraints + anti-affinity + PDB 组合治理
  4. 关键系统组件也要跨 AZ,不只是业务 Pod
  5. 有状态服务单独评估存储拓扑,不要默认 Pod 能漂移就等于业务能容灾
  6. 为 N-1 故障预留容量,否则高可用只是幻觉
  7. 定期演练 AZ 故障,演练结果比架构图更真实

最后说个很实际的边界条件:
如果你的业务规模还不大、团队对 Kubernetes 的控制面原理不够熟,先把单集群跨 3 AZ做稳,通常比一上来搞多集群更划算。
而当你已经能稳定驾驭单集群多 AZ,再去做跨集群容灾,才更像“升级”,而不是“叠复杂度”。


分享到:

上一篇
《大模型应用中的 RAG 实战:从知识库构建、检索优化到回答质量评估》
下一篇
《从单体到高可用:基于 Kubernetes 的中小规模集群架构设计与故障切换实战》