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

《Kubernetes 集群架构实战:基于高可用控制平面与多可用区部署的设计要点与落地方案》

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

Kubernetes 集群架构实战:基于高可用控制平面与多可用区部署的设计要点与落地方案

Kubernetes 集群一旦从“测试能跑”走向“生产必须稳”,架构问题就会立刻变得具体:控制平面挂了怎么办?单个可用区网络抖动怎么办?etcd 放哪里?业务要不要跨可用区分布?这些问题如果在建设初期没想清楚,后面通常只能靠熬夜补课。

这篇文章我会从生产可用性的角度,带你梳理一套相对稳妥的 Kubernetes 集群架构方案:高可用控制平面 + 多可用区部署。重点不是堆概念,而是解释清楚为什么这么设计、有哪些取舍,以及怎么落地。


背景与问题

在很多团队的早期实践里,Kubernetes 集群常见是这样的:

  • 1 个 control plane 节点
  • 若干 worker 节点
  • 所有节点在同一个可用区
  • etcd 跟控制平面混布
  • 入口是单个 SLB 或者甚至直接绑某台机器 IP

这套架构在开发、测试、小流量场景能工作,但到了生产环境,风险非常集中:

  1. 控制平面单点
    • kube-apiserver 不可用时,虽然已有 Pod 可能继续跑,但调度、扩缩容、发布、控制器修复都会受影响。
  2. 单可用区故障域过大
    • 一个 AZ 出问题,整个集群可能失联。
  3. etcd 选址不当
    • etcd 对网络时延和磁盘性能很敏感,跨区部署方式不合理时,容易引发选主抖动、写延迟变高。
  4. 业务层与基础设施层耦合
    • 业务 Pod 没有感知拓扑,结果副本全打到一个 AZ,所谓“多可用区”只是节点看起来分散。
  5. 网络与流量路径复杂化
    • 跨区访问会引入额外延迟和带宽成本,尤其在 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-apiserver
  • kube-controller-manager
  • kube-scheduler
  • etcd

其中真正对外承载“所有控制入口”的是 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-managerscheduler 通常多实例部署,但通过 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.11
    • 10.0.2.11
    • 10.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 应该仍然可用,最多有短暂抖动。


多可用区调度设计建议

实际生产里,业务类型不同,调度策略也不应一刀切。

无状态服务

优先使用:

  • Deployment
  • topologySpreadConstraints
  • 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

常见原因

  • 没打可用区标签
  • topologySpreadConstraintslabelSelector 不匹配
  • 节点资源不足
  • 某个 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 available
  • node(s) didn't match pod topology spread constraints
  • Insufficient cpu
  • had 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 后端摘除

一套更实用的落地清单

如果你准备建设生产集群,我建议按下面这个顺序推进:

  1. 明确 SLA 目标
    • 是要抗单节点,还是抗单 AZ?
  2. 决定故障域
    • 3 AZ 还是 2 AZ
  3. 确定控制平面部署方式
    • 3 control plane + 3 etcd
  4. 设计 API 入口
    • 内网 LB / VIP / Keepalived + HAProxy
  5. 统一节点标签规范
    • 区域、可用区、机器类型、业务池
  6. 确定调度策略
    • topology spread、亲和性、taint/toleration
  7. 规划存储
    • 哪些业务能跨 AZ,哪些必须托管
  8. 补齐基础组件高可用
    • CoreDNS、Ingress、Metrics、日志、监控
  9. 建立发布与回滚策略
    • 尤其是 AZ 故障场景下的容量策略
  10. 完成故障演练与基线监控
  • 不演练,HA 就只是 PPT

总结

基于高可用控制平面与多可用区部署的 Kubernetes 集群,真正的设计重点可以归纳为三句话:

  1. 控制平面要有稳定入口,etcd 要有正确仲裁结构
  2. 节点跨 AZ 只是开始,业务调度必须显式感知拓扑
  3. 高可用要以故障场景为单位验证,而不是只看部署拓扑

如果你现在正准备落地生产集群,我的建议很明确:

  • 中型生产环境:优先采用 3 control plane + 3 AZ + worker 跨 AZ 的方案
  • 关键业务:把数据库等强状态组件尽量托管,别一开始就在集群里“自建一切”
  • 资源规划:至少预留单 AZ 故障后的承载空间
  • 验证方式:务必做节点、控制平面、可用区级别的故障演练

最后补一句经验之谈:
Kubernetes 架构设计最怕“看起来高可用”。真正靠谱的方案,不是图里画了几个节点,而是当你真的拔掉一部分节点、关掉一台 control plane、甚至拿掉一个 AZ 时,业务还能不能稳住。这才是架构落地的分水岭。


分享到:

上一篇
《Java Web开发实战:基于Spring Boot与MyBatis实现高并发订单接口的幂等性与性能优化》
下一篇
《Node.js 中基于 Worker Threads 与消息队列的高并发任务处理实战-490》