背景与问题
很多团队第一次做 Kubernetes 高可用,往往把重点放在“多加几台 master”上,结果上线后才发现:控制平面节点变多,不等于集群真的高可用。
我见过几类非常典型的事故:
- 控制平面有 3 台,但前面只放了一个单点负载均衡,LB 一挂整个集群 API 不可用
- etcd 做了 3 节点,但部署在同一台物理宿主机或同一可用区,宿主机故障直接团灭
- kubeadm 初始化时
controlPlaneEndpoint配置不规范,后续证书、加入节点、切换流量都出问题 kube-apiserver虽然有多副本,但控制器和调度器的 Leader Election 配置异常,故障切换慢到业务已经报警- 节点健康检查阈值设置不合理,结果不是“高可用”,而是“高抖动”
所以这篇文章不只是讲“标准架构长什么样”,而是从 “为什么看起来冗余了,实际仍然会中断” 这个 troubleshooting 视角出发,带你梳理:
- 控制平面高可用真正依赖哪些组件
- 故障切换链路是怎么工作的
- 如何用一套可运行的配置快速搭建
- 出现 API 不通、切换慢、选主异常时怎么排查
- 哪些安全和性能优化值得优先做
先看全局:高可用不是“堆机器”,而是“消灭单点”
一个可靠的 Kubernetes 高可用集群,至少要同时覆盖下面几个层次:
- 访问入口高可用:客户端访问 API Server 不能依赖单点
- 控制平面高可用:多个 apiserver、controller-manager、scheduler
- 状态存储高可用:etcd 多副本并保持法定多数
- 网络与 DNS 可恢复:节点故障后服务发现和网络插件不拖后腿
- 故障切换可验证:不是“理论上能切”,而是演练过、观测过
下面这张图可以先建立整体认知。
flowchart TD
A[kubectl / CI / Controller] --> B[VIP 或 LB]
B --> C1[kube-apiserver-1]
B --> C2[kube-apiserver-2]
B --> C3[kube-apiserver-3]
C1 --> E1[etcd-1]
C2 --> E2[etcd-2]
C3 --> E3[etcd-3]
C1 --> F1[kube-controller-manager]
C2 --> F2[kube-controller-manager]
C3 --> F3[kube-controller-manager]
C1 --> G1[kube-scheduler]
C2 --> G2[kube-scheduler]
C3 --> G3[kube-scheduler]
C1 -. watch/list .-> H[Worker Nodes]
C2 -. watch/list .-> H
C3 -. watch/list .-> H
背景与问题
常见故障现象
在排障现场,通常不是“高可用失败”这么抽象,而是以下这些可观测问题:
kubectl get nodes卡住或超时- 新 Pod 长时间
Pending - Deployment 无法滚动发布
- Node NotReady 后很久业务流量还没摘除
- 部分控制平面节点恢复后,集群出现频繁 leader 切换
- etcd 告警:
failed to reach the peerURL、leader changed
为什么控制平面最容易被低估
因为业务 Pod 运行在 worker 上,很多人天然认为“只要 worker 多,业务就稳”。但实际上:
- 调度 需要 scheduler
- 副本自愈 需要 controller-manager
- Service/Ingress/Operator 大量依赖 apiserver
- 集群状态一致性 靠 etcd
换句话说,控制平面不是“管理面可有可无”,它一旦不可用,集群很快从“还能跑”进入“不能变更、不能恢复、故障扩散”的状态。
核心原理
这一部分重点回答两个问题:
- 为什么 3 控制平面是常见选择?
- 故障切换到底是怎么发生的?
1. 控制平面冗余的最小合理形态
对于生产环境,最常见的是:
- 3 台控制平面节点
- 1 个虚拟入口(VIP 或外部 LB)
- 3 节点 etcd 集群(可堆叠在控制平面,也可独立)
原因很简单:
etcd 基于 Raft,需要多数派(quorum)才能写入。
- 1 节点:无高可用
- 2 节点:任意一台故障后失去多数,设计上不划算
- 3 节点:允许 1 台故障,成本与可靠性最平衡
- 5 节点:可容忍 2 台故障,但写入延迟、运维复杂度更高
2. 控制器和调度器并不是“同时工作”,而是“抢主工作”
kube-controller-manager 和 kube-scheduler 通常都会开多个实例,但真正对外生效的通常只有一个 Leader,其余是热备。
stateDiagram-v2
[*] --> Standby1
[*] --> Leader
[*] --> Standby2
Leader --> Failed: 节点宕机/进程退出
Failed --> ReElection: 租约失效
ReElection --> NewLeader: 其他实例抢占成功
NewLeader --> Leader
这里的关键依赖是 Leader Election 租约机制。
如果租约参数过大,切换会慢;如果太小,又容易抖动。
常见参数包括:
--leader-elect=true--leader-elect-lease-duration--leader-elect-renew-deadline--leader-elect-retry-period
3. API Server 高可用依赖入口层
kube-apiserver 可以多实例,但客户端是否能自动切换,取决于前面的入口。
常见做法:
- 云上:使用云厂商 SLB/NLB
- 自建机房:HAProxy + Keepalived
- 裸金属:LVS / F5 / 硬件 LB
这里有个容易忽略的点:
API Server 的高可用不只是“流量分发”,还涉及健康检查是否准确。
如果 LB 只检查 TCP 6443 端口连通,而不检查更深层的健康状态,就可能把流量仍然导向“端口活着但内部卡死”的 apiserver。
4. etcd 是高可用链路里最脆弱、也最关键的点
etcd 存的是集群事实状态,所有控制器基本都围着它转。
etcd 故障的典型后果:
- apiserver 读写失败
- 资源创建/更新失败
- 调度停滞
- 控制器无法推进状态
因此高可用设计里最重要的一条经验就是:
控制平面节点挂一台还能顶住,etcd 多数派一丢,整个集群就进入危险区。
下面这张时序图展示一个控制平面节点故障后的切换过程。
sequenceDiagram
participant Client as kubectl/业务控制器
participant LB as LB/VIP
participant API1 as apiserver-1
participant API2 as apiserver-2
participant CM as controller-manager
participant ETCD as etcd集群
Client->>LB: 请求 /api
LB->>API1: 转发请求
API1->>ETCD: 读写资源
API1--xLB: 节点故障/无响应
Client->>LB: 重试请求
LB->>API2: 健康检查后切换转发
API2->>ETCD: 继续处理请求
CM--xCM: 原 Leader 故障
CM->>ETCD: 其他实例基于租约重新选主
ETCD-->>CM: 新 Leader 生效
架构选型与取舍
方案一:堆叠式控制平面(Stacked etcd)
即 etcd 与控制平面组件部署在同一批 master 节点上。
优点
- 部署简单,适合中小规模集群
- kubeadm 支持成熟
- 节点数量少,运维成本相对低
缺点
- 控制平面与存储状态机耦合
- 节点资源争抢时,etcd 更容易受影响
- 故障域重叠,需要更严格的资源隔离
方案二:外部 etcd
控制平面节点和 etcd 节点分开部署。
优点
- etcd 独立,资源更可控
- 故障域更清晰
- 大规模集群更稳
缺点
- 组件更多,证书和网络配置更复杂
- 维护门槛更高
我给中级团队的建议
如果你们当前:
- 集群规模中等
- 运维团队人数有限
- 主要目标是“先把高可用做扎实”
那么优先选:
3 控制平面 + stacked etcd + HAProxy/Keepalived 或云 LB
如果你们已经有:
- 多可用区部署
- 大量 CRD/Operator
- 高并发 API 请求
- 独立平台团队
再考虑外部 etcd。
实战代码(可运行)
下面给出一个比较实用的落地示例:
使用 HAProxy + Keepalived + kubeadm 搭建 3 控制平面高可用入口。
说明:示例基于 Linux 环境,适合自建机房或虚拟机环境。
假设:
- VIP:
10.10.0.100- HAProxy/Keepalived 节点:
10.10.0.1010.10.0.11- 控制平面节点:
10.10.0.2110.10.0.2210.10.0.23
1. HAProxy 配置
在两台 LB 节点上安装 HAProxy:
sudo apt-get update
sudo apt-get install -y haproxy
配置 /etc/haproxy/haproxy.cfg:
global
log /dev/log local0
log /dev/log local1 notice
daemon
maxconn 4000
defaults
log global
mode tcp
option tcplog
timeout connect 10s
timeout client 1m
timeout server 1m
frontend kubernetes-apiserver
bind 0.0.0.0:6443
default_backend kubernetes-control-plane
backend kubernetes-control-plane
mode tcp
balance roundrobin
option tcp-check
default-server inter 3s fall 3 rise 2
server cp1 10.10.0.21:6443 check
server cp2 10.10.0.22:6443 check
server cp3 10.10.0.23:6443 check
检查配置并启动:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl enable haproxy
sudo systemctl restart haproxy
2. Keepalived 配置 VIP
安装:
sudo apt-get install -y keepalived
主节点 /etc/keepalived/keepalived.conf:
global_defs {
router_id LVS_K8S_01
}
vrrp_script chk_haproxy {
script "pidof haproxy"
interval 2
weight 20
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 120
advert_int 1
authentication {
auth_type PASS
auth_pass K8SHA2023
}
virtual_ipaddress {
10.10.0.100/24
}
track_script {
chk_haproxy
}
}
备节点 /etc/keepalived/keepalived.conf:
global_defs {
router_id LVS_K8S_02
}
vrrp_script chk_haproxy {
script "pidof haproxy"
interval 2
weight 20
}
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass K8SHA2023
}
virtual_ipaddress {
10.10.0.100/24
}
track_script {
chk_haproxy
}
}
启动服务:
sudo systemctl enable keepalived
sudo systemctl restart keepalived
ip addr show
验证 VIP 是否漂移成功:
ping 10.10.0.100
nc -zv 10.10.0.100 6443
3. kubeadm 初始化第一个控制平面
在 10.10.0.21 上创建配置文件 kubeadm-ha.yaml:
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.27.3
controlPlaneEndpoint: "10.10.0.100:6443"
networking:
podSubnet: "10.244.0.0/16"
apiServer:
certSANs:
- "10.10.0.100"
- "10.10.0.21"
- "10.10.0.22"
- "10.10.0.23"
controllerManager: {}
scheduler: {}
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: "10.10.0.21"
bindPort: 6443
nodeRegistration:
criSocket: "unix:///var/run/containerd/containerd.sock"
初始化:
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
安装网络插件,这里以 Flannel 为例:
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
4. 加入其余控制平面节点
在第二、第三台控制平面节点上执行 kubeadm init 输出中的 join 命令,类似:
sudo kubeadm join 10.10.0.100:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
--control-plane \
--certificate-key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
5. 验证高可用状态
kubectl get nodes -o wide
kubectl get pod -n kube-system -o wide
kubectl get endpoints kubernetes -o yaml
查看 etcd 成员状态:
export ETCDCTL_API=3
sudo 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 status --write-out=table
现象复现:故障切换是否真的生效
很多高可用方案“搭好了”,但没有演练过。建议至少做下面三种验证。
场景一:杀掉一个 apiserver
在某个控制平面节点上:
sudo crictl ps | grep kube-apiserver
sudo crictl stop <apiserver-container-id>
观察:
kubectl get --raw='/readyz?verbose'
kubectl get nodes
预期结果:
- LB 在数秒内摘除该节点
kubectl请求有短暂抖动,但整体可恢复- 其他 apiserver 可继续服务
场景二:停掉 leader scheduler 或 controller-manager
sudo crictl ps | egrep 'kube-scheduler|kube-controller-manager'
sudo crictl stop <container-id>
查看 leader 切换:
kubectl -n kube-system get lease
kubectl -n kube-system describe lease kube-scheduler
kubectl -n kube-system describe lease kube-controller-manager
预期结果:
- 新 leader 在租约过期后接管
- 调度与控制回路恢复
场景三:模拟单个 etcd 节点故障
如果是 stacked etcd,可以在一台控制平面节点停掉 etcd:
sudo crictl ps | grep etcd
sudo crictl stop <etcd-container-id>
检查:
export ETCDCTL_API=3
sudo 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 \
member list
预期结果:
- 3 节点 etcd 故障 1 个仍能保持多数
- apiserver 仍可读写
- 若再掉 1 个,写入会明显异常
常见坑与排查
这一节我尽量按“先止血,再定位”的思路来写。
坑一:有 3 台 master,但 kubectl 仍频繁超时
常见原因
- VIP 漂移正常,但 HAProxy 后端健康检查太宽松
- LB 仅做 TCP 检查,未及时摘除半死不活的 apiserver
controlPlaneEndpoint用了某一台真实 IP,而不是 VIP/LB 地址- 客户端 kubeconfig 指向旧地址
排查路径
先看 kubeconfig:
kubectl config view --minify | grep server:
确认是否指向:
https://10.10.0.100:6443
再从 LB 节点检查后端:
echo "show stat" | sudo socat stdio /run/haproxy/admin.sock
如果没有 admin socket,也可以先直接验证端口:
for ip in 10.10.0.21 10.10.0.22 10.10.0.23; do
echo "== $ip =="
nc -zv $ip 6443
done
再看 apiserver 本身日志:
sudo crictl ps | grep kube-apiserver
sudo crictl logs <apiserver-container-id> | tail -n 100
止血方案
- 先把异常控制平面节点从 LB 后端摘除
- 确认 kubeconfig 指向统一入口
- 将不稳定节点 cordon,避免进一步影响
坑二:Leader Election 反复抖动,调度时好时坏
常见原因
- 控制平面节点时间不同步
- etcd 延迟高,租约续约失败
- 控制器资源不足,被频繁 OOM 或 CPU throttle
- 网络抖动导致租约更新延迟
排查路径
先看 lease:
kubectl -n kube-system get lease
kubectl -n kube-system describe lease kube-scheduler
kubectl -n kube-system describe lease kube-controller-manager
看事件和切换频率是否异常。
检查时间同步:
timedatectl status
chronyc tracking
检查控制平面负载:
top
vmstat 1 5
iostat -xz 1 3
查看组件日志:
sudo crictl logs <scheduler-container-id> | tail -n 100
sudo crictl logs <controller-manager-container-id> | tail -n 100
止血方案
- 先确保 NTP/Chrony 正常
- 控制平面节点预留足够 CPU、内存、IOPS
- 不要和高负载业务进程混部
坑三:etcd 明明是 3 节点,还是容易雪崩
常见原因
- 3 个 etcd 实际在同一宿主机或同一机架
- 磁盘延迟高,fsync 慢
- 快照和压缩策略缺失,数据库膨胀
- 网络 MTU 或防火墙规则导致 peer 通信异常
排查命令
健康检查:
export ETCDCTL_API=3
sudo 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 --write-out=table
查看状态:
export ETCDCTL_API=3
sudo 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 status --write-out=table
查看日志:
sudo crictl ps | grep etcd
sudo crictl logs <etcd-container-id> | tail -n 200
重点关注:
- leader 频繁切换
- apply/commit 延迟高
- peer 连接失败
- WAL 或 snapshot 相关报错
止血方案
- 故障节点先隔离,保证剩余多数派稳定
- 不要急着重建多个 etcd 成员,避免误操作丢多数
- 优先确认磁盘和网络,再决定是否 remove/add member
坑四:API Server 活着,但业务还是不恢复
这也是个很真实的坑。
很多人看到 6443 通了,就以为集群恢复了。其实还要看:
- CoreDNS 是否正常
- CNI 是否正常
- kube-proxy / iptables / IPVS 是否正常
- ingress controller 是否恢复 watch
- Operator 是否从 apiserver 重新建立连接
排查建议
kubectl get pods -A
kubectl get events -A --sort-by='.lastTimestamp'
kubectl get componentstatuses
kubectl -n kube-system get pods -o wide
注意:
componentstatuses在新版本中不再推荐作为唯一判断依据,只能辅助看。
可以重点查这些组件:
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=100
kubectl -n kube-system logs -l app=flannel --tail=100
定位路径:从“API 不可用”到“根因确认”的实战顺序
如果线上报警说 Kubernetes API 不可用,我通常按这个顺序排查,效率比较高:
flowchart TD
A[API 不可用/超时] --> B{VIP/LB 是否可达}
B -- 否 --> C[检查 Keepalived/SLB/NLB/网络]
B -- 是 --> D{6443 后端是否健康}
D -- 否 --> E[检查 apiserver 进程/证书/端口]
D -- 是 --> F{apiserver 是否能访问 etcd}
F -- 否 --> G[检查 etcd quorum/磁盘/网络]
F -- 是 --> H{是否为 leader election 或控制器异常}
H -- 是 --> I[检查 scheduler/controller-manager lease]
H -- 否 --> J[检查 CNI/CoreDNS/业务控制器]
这个顺序的核心思想是:
- 先看入口
- 再看服务端
- 再看状态存储
- 最后看上层控制器和插件
不要一开始就钻进某个 Pod 日志里,那样很容易迷路。
安全/性能最佳实践
高可用做完后,如果不补齐安全与性能细节,后面还是会出事。
安全最佳实践
1. API Server 入口只暴露必要范围
- 6443 不要对全网开放
- 使用安全组、ACL、内网白名单限制来源
- 管理入口与业务入口尽量隔离
2. 证书 SAN 一开始就规划完整
至少包含:
- VIP/LB 地址
- 各控制平面节点 IP
- 必要的 DNS 名称
否则后面补证书会很麻烦,尤其在已有节点加入后更痛苦。
3. etcd 一定启用 TLS
- client 通信加密
- peer 通信加密
- 证书定期轮转
- 备份文件妥善保管
4. RBAC 最小权限
很多排障脚本喜欢直接给 cluster-admin,我不建议这么做。
运维角色、CI 角色、只读巡检角色应分开。
性能最佳实践
1. etcd 用低延迟磁盘
这是最重要的一条。
如果 etcd 在慢盘上,控制平面再多都救不了。
建议:
- SSD/NVMe 优先
- 避免和高 IO 业务混部
- 监控 fsync 延迟
2. 控制平面节点做资源保留
例如为 kubelet 保留资源,避免系统和控制组件争抢:
kubeReserved:
cpu: "500m"
memory: "1Gi"
systemReserved:
cpu: "500m"
memory: "1Gi"
evictionHard:
memory.available: "200Mi"
3. 健康检查阈值别“过度灵敏”
太激进会导致抖动式摘除,太宽松又切换太慢。
建议结合实际网络延迟做压测后调整,不要照搬网上参数。
4. 做监控,但更要做演练
至少监控这些指标:
- apiserver 请求延迟、5xx
- etcd leader 变更次数
- etcd fsync/commit 延迟
- controller-manager 和 scheduler 选主变化
- Node Ready/NotReady 数量
- LB 后端健康状态
同时定期演练:
- 单控制平面节点故障
- 单 etcd 节点故障
- LB 主节点故障
- 网络隔离场景
一份简单的巡检脚本
下面给一个可直接运行的 Bash 脚本,用于快速检查控制平面关键状态。
#!/usr/bin/env bash
set -euo pipefail
VIP="${1:-10.10.0.100}"
PORT="${2:-6443}"
echo "== 1. 检查 VIP/LB 连通性 =="
if nc -z -w 2 "$VIP" "$PORT"; then
echo "[OK] $VIP:$PORT reachable"
else
echo "[FAIL] $VIP:$PORT unreachable"
fi
echo
echo "== 2. 检查节点状态 =="
kubectl get nodes -o wide || true
echo
echo "== 3. 检查控制平面 Pod =="
kubectl get pods -n kube-system -o wide | egrep 'etcd|kube-apiserver|kube-controller-manager|kube-scheduler' || true
echo
echo "== 4. 检查 Leader Election =="
kubectl -n kube-system get lease || true
echo
echo "== 5. 检查 apiserver readyz =="
kubectl get --raw='/readyz?verbose' || true
echo
echo "== 6. 检查最近事件 =="
kubectl get events -A --sort-by='.lastTimestamp' | tail -n 20 || true
执行:
chmod +x k8s-ha-check.sh
./k8s-ha-check.sh 10.10.0.100 6443
这个脚本不是万能的,但在“先看大盘有没有明显异常”这件事上很省时间。
边界条件:哪些情况下 3 master 也不够
高可用不是银弹。以下场景中,仅有 3 控制平面并不能真正满足需求:
- 跨地域容灾:这已经不是单集群高可用,而是多集群灾备问题
- 强隔离合规要求:需要独立 etcd、独立网络域、严格审计
- 超大规模集群:控制面与 etcd 资源、API QPS、控制器复杂度都上升
- 多可用区网络质量差:跨 AZ 延迟过高会影响 etcd 与选主稳定性
这时候要考虑:
- 多集群架构
- 联邦或 GitOps 统一管理
- 外部 etcd
- 分层入口与多级流量治理
总结
Kubernetes 高可用的关键,不是“把 master 数量从 1 变成 3”,而是把整条链路梳理完整:
- 入口层:VIP / LB 不能单点
- 控制平面:apiserver 多副本,controller-manager 与 scheduler 正常选主
- 状态存储:etcd 保持多数派,磁盘和网络必须稳定
- 切换机制:健康检查、租约参数、恢复路径要可验证
- 排障方法:先入口,再 apiserver,再 etcd,再控制器与插件
如果你现在正准备落地,建议按这个顺序推进:
- 先搭 3 控制平面 + 统一 controlPlaneEndpoint
- 再补上 LB/VIP 高可用
- 验证 单节点故障切换
- 建立 etcd 与 apiserver 监控
- 固化 故障演练与巡检脚本
最后给一句很实在的建议:
没有演练过的高可用,只能叫“看起来像高可用”。
真正上线前,务必亲手断一台、停一个、切一次,你对集群的信心会完全不一样。