Kubernetes 集群架构实战:基于高可用控制平面与多可用区部署的设计要点与落地方案
Kubernetes 集群一旦从“测试能跑”走向“生产必须稳”,架构问题就会立刻变得具体:控制平面挂了怎么办?单个可用区网络抖动怎么办?etcd 放哪里?业务要不要跨可用区分布?这些问题如果在建设初期没想清楚,后面通常只能靠熬夜补课。
这篇文章我会从生产可用性的角度,带你梳理一套相对稳妥的 Kubernetes 集群架构方案:高可用控制平面 + 多可用区部署。重点不是堆概念,而是解释清楚为什么这么设计、有哪些取舍,以及怎么落地。
背景与问题
在很多团队的早期实践里,Kubernetes 集群常见是这样的:
- 1 个 control plane 节点
- 若干 worker 节点
- 所有节点在同一个可用区
- etcd 跟控制平面混布
- 入口是单个 SLB 或者甚至直接绑某台机器 IP
这套架构在开发、测试、小流量场景能工作,但到了生产环境,风险非常集中:
- 控制平面单点
kube-apiserver不可用时,虽然已有 Pod 可能继续跑,但调度、扩缩容、发布、控制器修复都会受影响。
- 单可用区故障域过大
- 一个 AZ 出问题,整个集群可能失联。
- etcd 选址不当
- etcd 对网络时延和磁盘性能很敏感,跨区部署方式不合理时,容易引发选主抖动、写延迟变高。
- 业务层与基础设施层耦合
- 业务 Pod 没有感知拓扑,结果副本全打到一个 AZ,所谓“多可用区”只是节点看起来分散。
- 网络与流量路径复杂化
- 跨区访问会引入额外延迟和带宽成本,尤其在 Service、Ingress、存储复制层。
所以,真正的目标不是“把节点放到多个 AZ”这么简单,而是要实现:
- 控制平面高可用
- 业务工作负载跨可用区分布
- 单 AZ 故障可降级而非整体崩溃
- 运维复杂度可控
- 成本与稳定性平衡
方案全景:推荐的目标架构
先给出一个比较常见、也比较适合中型生产环境的架构:
- 3 个 control plane 节点,分布在 3 个可用区
- etcd 采用 3 节点奇数仲裁,通常与 control plane 同机或独立小集群
kube-apiserver前置一个四层负载均衡器(VIP / LB)- worker 节点分布到多个可用区
- CoreDNS、Ingress Controller、关键系统组件做跨区副本分散
- 业务 Pod 通过
topologySpreadConstraints、反亲和性等手段分布 - 存储根据场景选择:
- 强一致数据库:优先用云托管
- StatefulSet:明确卷与 AZ 的绑定策略
- 入口流量采用多副本、多 AZ 的 Ingress 或云 LB
架构示意图
flowchart TD
U[用户流量/运维请求] --> LB[API Server LB / VIP]
LB --> CP1[Control Plane AZ-A]
LB --> CP2[Control Plane AZ-B]
LB --> CP3[Control Plane AZ-C]
CP1 --> E1[etcd-1]
CP2 --> E2[etcd-2]
CP3 --> E3[etcd-3]
subgraph AZ-A
W1[Worker A1]
W2[Worker A2]
end
subgraph AZ-B
W3[Worker B1]
W4[Worker B2]
end
subgraph AZ-C
W5[Worker C1]
W6[Worker C2]
end
CP1 -.调度/控制.-> W1
CP1 -.调度/控制.-> W3
CP1 -.调度/控制.-> W5
核心原理
这一部分不追求百科式铺开,而是抓住做架构时最容易影响成败的几个关键点。
1. 控制平面高可用的本质
Kubernetes 控制平面的核心组件包括:
kube-apiserverkube-controller-managerkube-scheduleretcd
其中真正对外承载“所有控制入口”的是 kube-apiserver。所以高可用控制平面的第一步,是让客户端永远访问一个稳定入口,这个入口后面连接多个 apiserver 实例。
请求路径
sequenceDiagram
participant Admin as kubectl/Controller
participant LB as API LB
participant API1 as kube-apiserver-1
participant API2 as kube-apiserver-2
participant ETCD as etcd Cluster
Admin->>LB: HTTPS 请求
LB->>API1: 转发到健康实例
API1->>ETCD: 读写集群状态
ETCD-->>API1: 返回结果
API1-->>LB: 响应
LB-->>Admin: 响应
Note over LB,API2: API1 故障时,LB 自动切到 API2
关键点
kube-apiserver可以多实例无状态扩展controller-manager和scheduler通常多实例部署,但通过 leader election 保证同一时刻只有一个活跃 leader- etcd 则不是“多多益善”,而是依赖奇数节点仲裁
2. etcd 为什么通常建议 3 节点而不是 4 节点
这是我见过非常高频的误区。很多人直觉上会觉得节点越多越稳,但 etcd 不是这么算的。
etcd 基于 Raft 共识,需要超过半数节点同意才算提交成功:
- 3 节点:容忍 1 节点故障
- 4 节点:仍然只能容忍 1 节点故障
- 5 节点:容忍 2 节点故障
所以多数场景下:
- 中小规模生产:3 节点 etcd
- 大规模且确有需要:5 节点 etcd
- 不建议为了“凑整”搞 4 节点
3. 多可用区部署的收益和代价
收益
- 单个 AZ 故障时,集群仍可继续服务
- 工作负载更容易实现冗余
- 维护窗口影响更小
代价
- 跨 AZ 网络时延更高
- Service 流量可能绕路
- 存储卷调度更复杂
- 带宽成本上升
- 排障链路变长
换句话说,多 AZ 不是“白捡高可用”,而是拿复杂度换容灾能力。
4. 控制平面跨 AZ,业务也必须感知拓扑
很多团队会把节点铺到多个 AZ,然后以为大功告成。实际上如果 Deployment 没有限制,调度器仍可能把 3 个副本都落到同一个 AZ。
真正有效的做法是让工作负载显式表达“分散部署”的意图,例如:
topologySpreadConstraints- Pod 反亲和性
- 节点亲和性
PodDisruptionBudget
5. “高可用”不是“零故障”,而是“故障有边界”
设计时我建议把目标定义得更精确一些:
- 单节点故障:无感或轻微抖动
- 单个控制平面节点故障:运维入口可继续工作
- 单 AZ 故障:业务容量下降,但核心服务可持续
- etcd 单节点故障:集群可读写
- 网络分区:要知道谁会失去仲裁,谁保留服务
这比一句“我们做了 HA”有用得多。
方案对比与取舍分析
方案一:单 AZ 单控制平面
优点
- 成本低
- 部署简单
- 适合测试环境
缺点
- 明显单点
- 不适合生产关键业务
方案二:单 AZ 多控制平面
优点
- 控制平面高可用
- 运维复杂度较低
缺点
- 仍有 AZ 级故障风险
- 无法应对机房级中断
方案三:多 AZ 高可用控制平面
优点
- 故障域更小
- 更符合生产要求
- 可支撑跨区业务调度
缺点
- 网络、存储、调度、流量路径都更复杂
- 成本上升
方案四:多集群多 AZ / 多 Region
优点
- 容灾能力最强
- 适合超高可用要求
缺点
- 运维体系、发布体系、流量治理复杂
- 不适合一开始就上
如果你现在是“准备建设第一套正式生产集群”,我通常建议优先落地方案三,不要一口气冲到多 Region 多集群,复杂度太容易失控。
容量估算与节点规划
在设计阶段,节点数量不要只按业务 Pod 算,还要把故障场景考虑进去。
一个实用的估算思路
假设:
- 业务峰值需要 12 台 worker 的容量
- 集群分布在 3 个 AZ
- 目标是任意 1 个 AZ 故障后,剩余 2 个 AZ 仍能承载核心业务
那么至少要满足:
- 总容量 >= 峰值容量 / 可用容量比例
- 如果 1 个 AZ 挂掉,剩余容量仍 >= 核心需求
举个简化计算:
- 3 个 AZ 平均铺开,每个 AZ 4 台 worker,总共 12 台
- 若 AZ-C 故障,只剩 8 台
- 那么你的核心业务必须能在 8 台上运行,否则这不叫真正的 AZ 级容灾
所以很多生产集群会选择:
- 实际峰值只跑到总容量的 60%~70%
- 预留一定冗余,换取单 AZ 故障时的可承载空间
实战代码(可运行)
下面给一套基于 kubeadm 的示例,演示如何搭建一个高可用控制平面集群。示例假设:
- API LB 地址:
10.0.0.100:6443 - 3 个控制平面节点:
10.0.1.1110.0.2.1110.0.3.11
- Pod 网段:
10.244.0.0/16 - 使用 containerd
- CNI 选择 Calico
说明:示例可直接执行,但你需要按自己的环境替换 IP、网卡名、主机名和证书 SAN。
1)所有节点基础准备
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
cat <<'EOF' | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
cat <<'EOF' | sudo tee /etc/sysctl.d/99-kubernetes-cri.conf
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6tables=1
net.ipv4.ip_forward=1
EOF
sudo sysctl --system
2)安装 containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd
3)安装 kubeadm / kubelet / kubectl
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | \
sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /' | \
sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
sudo systemctl enable kubelet
4)准备 kubeadm 配置文件
在第一个控制平面节点上创建:
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.28.0
controlPlaneEndpoint: "10.0.0.100:6443"
networking:
podSubnet: "10.244.0.0/16"
apiServer:
certSANs:
- "10.0.0.100"
- "k8s-api.internal"
etcd:
local:
dataDir: /var/lib/etcd
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
nodeRegistration:
criSocket: "unix:///run/containerd/containerd.sock"
kubeletExtraArgs:
node-labels: "topology.kubernetes.io/zone=az-a,node-role.kubernetes.io/control-plane="
5)初始化第一个控制平面节点
sudo kubeadm init --config kubeadm-ha.yaml --upload-certs
初始化完成后,配置 kubectl:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown "$(id -u)":"$(id -g)" $HOME/.kube/config
6)安装 Calico 网络插件
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/calico.yaml
7)加入其他控制平面节点
以下命令由 kubeadm init 输出,示例形式如下:
sudo kubeadm join 10.0.0.100:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:1111111111111111111111111111111111111111111111111111111111111111 \
--control-plane \
--certificate-key 2222222222222222222222222222222222222222222222222222222222222222 \
--cri-socket unix:///run/containerd/containerd.sock
8)加入 worker 节点
sudo kubeadm join 10.0.0.100:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:1111111111111111111111111111111111111111111111111111111111111111 \
--cri-socket unix:///run/containerd/containerd.sock
9)为节点打上可用区标签
kubectl label node worker-a1 topology.kubernetes.io/zone=az-a --overwrite
kubectl label node worker-a2 topology.kubernetes.io/zone=az-a --overwrite
kubectl label node worker-b1 topology.kubernetes.io/zone=az-b --overwrite
kubectl label node worker-b2 topology.kubernetes.io/zone=az-b --overwrite
kubectl label node worker-c1 topology.kubernetes.io/zone=az-c --overwrite
kubectl label node worker-c2 topology.kubernetes.io/zone=az-c --overwrite
10)部署一个跨 AZ 分散的示例应用
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-nginx
spec:
replicas: 6
selector:
matchLabels:
app: demo-nginx
template:
metadata:
labels:
app: demo-nginx
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: demo-nginx
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: demo-nginx
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: demo-nginx-pdb
spec:
minAvailable: 4
selector:
matchLabels:
app: demo-nginx
应用部署:
kubectl apply -f demo-nginx.yaml
kubectl get pod -o wide
11)验证 Pod 是否真正跨 AZ 分布
kubectl get pods -l app=demo-nginx -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName
kubectl get nodes -L topology.kubernetes.io/zone
12)模拟单节点故障
kubectl drain worker-a1 --ignore-daemonsets --delete-emptydir-data
kubectl get pods -l app=demo-nginx -o wide
kubectl uncordon worker-a1
13)模拟 API Server 高可用验证
在某个 control plane 节点停掉 apiserver 静态 Pod:
sudo mv /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/
sleep 20
kubectl get nodes
sudo mv /tmp/kube-apiserver.yaml /etc/kubernetes/manifests/
如果 LB 和其他 apiserver 正常,kubectl get nodes 应该仍然可用,最多有短暂抖动。
多可用区调度设计建议
实际生产里,业务类型不同,调度策略也不应一刀切。
无状态服务
优先使用:
DeploymenttopologySpreadConstraints- Pod 反亲和
- HPA
- PDB
适合目标:
- 副本跨 AZ 分布
- 单 AZ 故障时自动收敛到剩余节点
有状态服务
需要重点考虑:
- 卷的 AZ 绑定
- 数据复制机制
- 恢复时长
- 是否支持跨 AZ 写入
一般建议:
- 核心数据库尽量优先使用成熟托管服务
- 如果必须自建,先验证:
- 跨 AZ 网络延迟
- 存储类
volumeBindingMode - 节点故障与卷重挂载时间
推荐的存储类配置思路
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: zonal-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
WaitForFirstConsumer 很关键,它能让卷在 Pod 实际调度时再决定绑定位置,避免“卷先落在 AZ-A,Pod 却想调度到 AZ-B”的尴尬。
常见坑与排查
这一节我会把比较常见、也最容易让人误判的问题拎出来。
坑一:控制平面做了 HA,但 kubectl 还是偶发超时
常见原因
- LB 健康检查没配对,错误地把异常节点当健康
- LB 转发到 6443 正常,但后端节点证书 SAN 不匹配
- API Server 资源不足,CPU 被打满
- etcd 写入延迟过高,导致 apiserver 整体变慢
排查命令
kubectl get --raw='/readyz?verbose'
kubectl get --raw='/livez?verbose'
kubectl -n kube-system get pod -o wide
journalctl -u kubelet -f
检查 etcd 健康:
sudo 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
坑二:Pod 明明配了 3 个副本,却全跑到一个 AZ
常见原因
- 没打可用区标签
topologySpreadConstraints的labelSelector不匹配- 节点资源不足
- 某个 AZ 有 taint,但业务没容忍
排查思路
kubectl get nodes -L topology.kubernetes.io/zone
kubectl describe deploy demo-nginx
kubectl describe pod <pod-name>
kubectl get events --sort-by=.lastTimestamp
重点看 Events 里是否出现:
0/6 nodes are availablenode(s) didn't match pod topology spread constraintsInsufficient cpuhad taint ...
坑三:单 AZ 故障后,Service 还在但响应很慢
常见原因
- 跨 AZ 转发比例过高
- CoreDNS 副本不足或未分散
- Ingress Controller 没跨 AZ
- ExternalTrafficPolicy / InternalTrafficPolicy 选择不当
我比较建议检查的点
kubectl -n kube-system get deploy coredns -o yaml
kubectl -n ingress-nginx get pod -o wide
kubectl get svc -A
如果 CoreDNS 只有 2 个副本,且刚好都在同一个 AZ,那故障时服务发现链路会很脆弱。
坑四:etcd 很“活着”,但集群就是卡
这是很容易踩的坑。etcdctl endpoint health 显示 healthy,不代表性能没问题。
更关键的是看:
- fsync 延迟
- 网络 RTT
- leader 频繁切换
- 数据库大小是否膨胀
- 磁盘是否是低性能盘
建议关注:
kubectl -n kube-system get pod -l component=etcd
kubectl -n kube-system logs etcd-$(hostname) | tail -n 100
如果日志里反复出现 leader 变更、request took too long,就要高度警惕。
安全/性能最佳实践
高可用架构不是只讲可用性,安全和性能往往一起决定“能不能长期跑”。
安全最佳实践
1)etcd 不对外暴露
- etcd 只允许控制平面节点访问
- 使用 TLS 双向认证
- 禁止业务网络直接访问 2379/2380
2)最小化 API Server 暴露面
- 仅通过内网 LB 暴露给运维和节点
- 公网访问走堡垒机、VPN 或专线
- 严格控制安全组 / 防火墙规则
3)开启审计日志
如果集群是多人协作环境,审计日志非常有必要。很多误操作排查,最后都得靠它。
4)RBAC 不要图省事给 cluster-admin
尤其是 CI/CD 账户、运维脚本账户,建议按 namespace、资源类型做最小权限授权。
5)Secret 管理外置化
- 可以考虑 External Secrets、Vault、云 KMS
- 不建议把所有核心凭据长期明文留在 Git 仓库
性能最佳实践
1)控制平面节点用更稳定的磁盘和 CPU
控制平面不是“跑得动就行”,特别是 etcd:
- 优先高 IOPS 磁盘
- 避免与高负载业务混部
- 预留足够 CPU、内存
2)CoreDNS、Ingress、监控组件做资源保障
这些基础组件不是业务,但它们挂了,业务看起来就像全挂了。
建议:
- 设置 requests / limits
- 做副本分散
- 配置 PDB
3)减少不必要的跨 AZ 流量
可从几个方向优化:
- Service 尽量本地优先
- Ingress Controller 按 AZ 分布
- 业务间高频调用尽量同 AZ 就近访问
- 大流量数据面与控制面分离考虑
4)定期做故障演练
纸面高可用和真实高可用之间,差的就是演练。
我通常建议至少演练这些场景:
- 单 worker 故障
- 单 control plane 故障
- 单 AZ 节点批量不可用
- etcd 单节点故障
- API LB 后端摘除
一套更实用的落地清单
如果你准备建设生产集群,我建议按下面这个顺序推进:
- 明确 SLA 目标
- 是要抗单节点,还是抗单 AZ?
- 决定故障域
- 3 AZ 还是 2 AZ
- 确定控制平面部署方式
- 3 control plane + 3 etcd
- 设计 API 入口
- 内网 LB / VIP / Keepalived + HAProxy
- 统一节点标签规范
- 区域、可用区、机器类型、业务池
- 确定调度策略
- topology spread、亲和性、taint/toleration
- 规划存储
- 哪些业务能跨 AZ,哪些必须托管
- 补齐基础组件高可用
- CoreDNS、Ingress、Metrics、日志、监控
- 建立发布与回滚策略
- 尤其是 AZ 故障场景下的容量策略
- 完成故障演练与基线监控
- 不演练,HA 就只是 PPT
总结
基于高可用控制平面与多可用区部署的 Kubernetes 集群,真正的设计重点可以归纳为三句话:
- 控制平面要有稳定入口,etcd 要有正确仲裁结构
- 节点跨 AZ 只是开始,业务调度必须显式感知拓扑
- 高可用要以故障场景为单位验证,而不是只看部署拓扑
如果你现在正准备落地生产集群,我的建议很明确:
- 中型生产环境:优先采用
3 control plane + 3 AZ + worker 跨 AZ的方案 - 关键业务:把数据库等强状态组件尽量托管,别一开始就在集群里“自建一切”
- 资源规划:至少预留单 AZ 故障后的承载空间
- 验证方式:务必做节点、控制平面、可用区级别的故障演练
最后补一句经验之谈:
Kubernetes 架构设计最怕“看起来高可用”。真正靠谱的方案,不是图里画了几个节点,而是当你真的拔掉一部分节点、关掉一台 control plane、甚至拿掉一个 AZ 时,业务还能不能稳住。这才是架构落地的分水岭。