背景与问题
很多团队第一次做 Kubernetes 高可用,脑海里的目标通常很朴素:“挂一台机器不要紧,业务别停。”
但真正落地时,会很快发现问题没这么简单:
- 控制平面高可用,不等于业务高可用
- 多副本部署,不等于跨可用区容灾
- 节点分散到多个 AZ(Availability Zone,可用区),不等于整个系统具备故障隔离能力
- etcd 能跑,不等于在网络抖动和少数节点失联时还能稳定选主
我自己在做多 AZ 集群设计时,最容易踩的坑有两个:
- 只关注 API Server 是否“有多个副本”
- 忽略调度、存储、流量入口这些“业务路径”上的单点
因此,这篇文章不只讲“怎么搭”,还会从控制平面、工作负载调度、网络入口、数据层、容量余量几个角度,把一个多可用区 Kubernetes 架构拆开讲清楚。
先给结论:多可用区高可用到底要解决什么
如果把 Kubernetes 集群看成一个系统,那么多 AZ 容灾通常要覆盖三类故障:
-
单节点故障
某个 control-plane 或 worker 宕机 -
单可用区故障
一个 AZ 整体不可用,或者网络严重退化 -
局部网络分区
节点没挂,但 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-plane3个 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 一个集群,再做多集群容灾
优点:
- 故障域更清晰
- 一个集群出问题不一定影响其他集群
- 更适合强隔离场景
缺点:
- 资源碎片化明显
- 发布、配置、流量切换复杂度更高
- 监控和治理成本提升
适用场景:
- 金融、核心交易、强监管业务
- 单集群风险不可接受的环境
我更推荐的落地思路
如果你正在从“单区单集群”演进到高可用:
- 先做好单集群跨 3 AZ
- 把入口、调度、PDB、存储策略补齐
- 再根据业务等级决定是否走多集群
也就是说,先把一个集群做扎实,再谈更复杂的容灾编排。
容量估算:别让“高可用”只停留在拓扑图里
多 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
- 设置合理超时
- 区分
Fail与Ignore策略,不要一刀切
性能最佳实践
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 高可用,真正要做的不是“把节点分散开”,而是把控制面、调度面、流量入口、存储面、容量模型一起设计完整。
你可以记住这几个最关键的落地建议:
- 控制平面至少 3 节点,分布在 3 个 AZ
- etcd 用奇数仲裁,优先 3 节点,不要把多数派压在同一 AZ
- 工作负载用
topologySpreadConstraints + anti-affinity + PDB组合治理 - 关键系统组件也要跨 AZ,不只是业务 Pod
- 有状态服务单独评估存储拓扑,不要默认 Pod 能漂移就等于业务能容灾
- 为 N-1 故障预留容量,否则高可用只是幻觉
- 定期演练 AZ 故障,演练结果比架构图更真实
最后说个很实际的边界条件:
如果你的业务规模还不大、团队对 Kubernetes 的控制面原理不够熟,先把单集群跨 3 AZ做稳,通常比一上来搞多集群更划算。
而当你已经能稳定驾驭单集群多 AZ,再去做跨集群容灾,才更像“升级”,而不是“叠复杂度”。