AI时代的Kubernetes教程36招-第7招-多Control Plane、etcd和Load Balancer怎么组成高可用集群?
单 Control Plane 到 HA:kubeadm 控制平面高可用实战
加两台 master 不叫 HA,单点只是换到了那一个 VIP 上
[读者] 管着一套 kubeadm 单控制平面集群的平台工程师
[痛点] 控制平面一停,发布、自愈、扩缩容全部停摆
[现在读] 要把控制平面从单点改成能滚动维护的 HA
[读完] 能独立规划 VIP、etcd 拓扑与证书 SAN,并完成落地验证
[旧方案] 单 master,所有 kubeconfig 直连该节点 IP
|
v
[新需求] 控制平面要能滚动维护,单节点故障不停服
|
v
[冲突] kubeconfig、证书 SAN、etcd quorum 全绑在「那一个地址、那一个节点」上
|
v
[后果] 维护窗口等于全集群不可用,etcd 单副本没有任何冗余
我是老李,一个内部平台的技术负责人。这套集群是三年前我用 kubeadm init 起来的:一台控制平面、三台 worker,admin.conf 里的 server 写死 https://10.0.0.11:6443。三年来它很安静,安静到我几乎忘了它是个单点。
直到那次内核安全更新。控制平面必须重启,我把窗口定在周六凌晨两点,提前发了通知。业务容器确实没死,kubelet 还在跑。但整个平台像被按了暂停:kubectl 转圈三十秒才超时,发布流水线卡在 connection refused,CI 红了一片,Pending 的 Pod 一动不动。
那晚我决定把它改成 HA。然后我发现,HA 不是一个开关。
kubeadm init 里那个 --control-plane-endpoint,会在第一次初始化时同时决定三件事:以后所有客户端连哪个地址、apiserver 证书里签进哪些 SAN、生成的 kubeconfig 里 server 字段长什么样。这三件事都落在磁盘上,事后想改,代价要按「证书轮换 + 重启 apiserver」来算。
所以在动手之前,有三个问题必须回答清楚:
控制平面重启的那二十分钟里,业务容器明明还活着,为什么整个平台却像死了一样?
如果确定要上 HA,第一个该动的到底是节点数量,还是那个「所有东西都指向它」的接入地址?
如果这个接入地址本身没有冗余,那我加两台 master,究竟买到了什么?
01、故事
那套系统不大:一台 4C8G 的控制平面,三台 8C16G 的 worker,上面跑着内部 CI runner、几个定时任务、一条发布流水线,还有业务方的测试环境。任务是把它从「我一个人的玩具」变成「全组依赖的基础设施」,而这一步在业务侧完成得比我预想的快得多。
变化是从依赖开始的。发布流水线的最后一段直接调 kubectl apply;监控告警的 webhook 走集群内 service;测试环境的开关做成了某个 Deployment 的副本数。到那个周六之前,已经有七八条链路把「控制平面可用」当成了默认前提。
冲突也在那里:控制平面只有一台。apiserver、controller-manager、scheduler、etcd 四个角色挤在同一台机器上,共用同一块盘、同一个内核。重启它,等于把这四个角色一起关了。
我当时的方案很土:挑业务最闲的时间重启,赌二十分钟里没有人发版。这个方案撑了三年,靠的是运气,不是设计。而这次触发点是不能拖的内核安全更新,一次重启躲不掉。
我把「一次控制平面维护」拆成四种角色分别停掉的后果:
- apiserver 停了:所有 kubectl、所有 webhook、所有 controller 与 apiserver 的通信全部超时;
- controller-manager 停了:Deployment 不再推进,副本数、滚动更新、PDB 全部冻结;
- scheduler 停了:新 Pod 卡在 Pending,节点故障后的重调度不会发生;
- etcd 停了:apiserver 连自己的存储都没了,前面三条一起发生。
真实现场是:容器没死,所以监控上「服务在线率」是绿的;但控制面没了,所以所有「变更类」操作全红。这种「业务看起来没事、平台彻底瘫」的状态,比直接宕机更难解释。
所以我给这件事定了目标:对单台控制平面节点做一次 drain、重启、uncordon,期间集群的读、写、调度、发布全部正常。
单点很安静
只在维护时说话
一次重启之后
才知道缺了什么
02、问题
旧方案不是「不够好」,而是根本不在同一个问题域里。
我原来那套方案有一个隐含前提:控制平面永远可用。所有客户端配置都建立在这个前提上。
客户端入口是写死的。 ~/.kube/config、/etc/kubernetes/admin.conf、CI 里那份 kubeconfig,server 字段都是同一个节点 IP。kubeconfig 不是负载均衡器,它只有一个地址,没有 failover 语义。地址背后的节点没了,客户端的表现是超时——不是干脆的报错,是转圈,一直转到 TCP 超时。
证书 SAN 是签死的。 apiserver 服务端证书只包含初始化当时能用到的地址。我查过一次:
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -text | grep -A1 'Subject Alternative Name'
SAN 里是节点 IP、节点主机名,以及 kubernetes、kubernetes.default、kubernetes.default.svc、kubernetes.default.svc.cluster.local。没有第二个节点,也没有任何 VIP。这意味着:就算你把 kubeconfig 改成 VIP,TLS 握手也会先失败。
etcd 只有一个成员。 单成员不讨论 quorum,因为它自己就是全部。没有副本,没有磁盘冗余,也没有「少数派还能读」这种退路。
控制平面组件只在一台机器上。 四个角色全是静态 Pod,清单在 /etc/kubernetes/manifests/,磁盘就那一台。
业务影响可以这样描述:维护窗口等于全平台变更冻结;节点内核、containerd、kubelet 的任何一次升级都要排停机窗口;etcd 数据只有一份,盘坏了就是控制平面数据的单点丢失。
技术表现说清楚之后,我给自己定了可验证的完成标准:
- 控制平面节点数不小于 3,
kubectl get nodes -l node-role.kubernetes.io/control-plane全部 Ready; - etcd 成员数为奇数(3),
etcdctl member list与etcdctl endpoint health --cluster全部健康; - 所有 kubeconfig 的 server 指向同一个稳定入口,且 apiserver 证书 SAN 包含该入口地址;
- 关掉任意一台控制平面节点,
kubectl get --raw='/readyz'仍返回 ok,kubectl apply仍能成功; - 任意一台控制平面节点可以 drain、重启、uncordon,上述检查全程通过。
这五条不是「更好」,是「可以拿去验收」。做不到,就说明 HA 没做完。
地址是写死的
证书是签死的
多数派不存在
维护只能靠赌
03、原理
要把上面五条做到,先要理解四件互相咬合的事。
第一件:控制平面需要一个稳定入口,而且这个入口必须是 L4。
kubeadm 的 HA 拓扑要求集群有一个 controlPlaneEndpoint,它的形态通常是一个 VIP 或者 L4 负载均衡器的地址加端口,后端是三台(或五台)控制平面节点的 6443。为什么是 L4 而不是 L7:kubelet、controller-manager、scheduler 以及你自己的 kubectl,和 apiserver 之间大量使用客户端证书做认证,某些链路还带 mTLS 语义。任何在 L7 终止 TLS 的反向代理都会破坏这套身份模型。L4 只做 TCP 转发,TLS 依然在 apiserver 那一端终止。
第二件:Stacked etcd 与 External etcd 是两种故障域切法。
Stacked 拓扑里,etcd 成员和控制平面节点一一绑定,kubeadm 直接在每个控制平面节点上起一个 etcd 静态 Pod。好处是节点少、部署简单、没有额外的网络跳数;代价是一台节点挂了,你同时损失一台 apiserver 和一个 etcd 成员,控制平面容量和 etcd 容量被绑定在一起。
External 拓扑里,etcd 是独立主机上的独立集群,控制平面节点只做无状态的那部分。好处是故障域解耦,控制平面可以扩到五台、etcd 保持三台不变;代价是你多了一套必须独立运维、独立备份、独立轮换证书的 etcd 集群,还多了一段网络延迟。官方文档把两种拓扑都列为可选,选择的依据是你的运维能力,不是性能数字。
第三件:奇数成员与 Quorum。
etcd 用 Raft,写入必须由多数派确认。N 个成员时多数派是 ⌊N/2⌋+1,能容忍的故障数是 ⌊(N-1)/2⌋:
- 1 个成员:多数派 1,容忍 0 台故障;
- 2 个成员:多数派 2,容忍 0 台故障;
- 3 个成员:多数派 2,容忍 1 台故障;
- 4 个成员:多数派 3,容忍 1 台故障;
- 5 个成员:多数派 3,容忍 2 台故障。
所以从 1 台加到 2 台,可用性没有任何提升,成本却翻倍。官方文档和 etcd 自己的文档都强调成员数取奇数、推荐 3 或 5,原因就在这里。
第四件:apiserver 可以水平扩,etcd 不能。
apiserver 本身几乎无状态:它的状态全在 etcd,自己只做认证、鉴权、admission、序列化和 watch 扇出。多起几个实例是自然的水平扩展,L4 做 TCP 轮询即可,客户端不需要 sticky session。但 apiserver 扩容解决的是连接数、watch 扇出、CPU 密集的序列化;它不解决 etcd 写入的串行化——所有写最终都排队进同一个 Raft 日志。
第五件:证书 SAN 与 controlPlaneEndpoint 是绑死的。
--control-plane-endpoint 的值会被写进 apiserver 证书的 SAN,也会写进 kubeadm 生成的各种 kubeconfig 的 server 字段。这是一个第一天就固化的决策:事后换域名或换 VIP 地址,要么重新签发 apiserver 证书,要么重建。如果 init 时漏了某个地址,可以用 --apiserver-cert-extra-sans 追加,或者走 kubeadm init phase certs apiserver 重新生成,但这些都不是「随手改一行配置」的操作。
反直觉判断:很多人以为 HA 的第一步是加节点,实际上第一步是给控制平面找一个稳定地址,并且这个地址本身还不能是新的单点。
奇数才成群
偶数白花钱
入口要稳定
证书先想好
04、架构
[输入] kubectl / kubelet / controller-manager / scheduler 的 API 请求
|
v
[模块] L4 Load Balancer(VIP:6443),把 TCP 连接分发到三台 kube-apiserver
|
v
[数据/状态] etcd 集群(3 成员,Raft 多数派 2),集群唯一的持久状态
|
v
[处理] kube-apiserver ×3 做认证/鉴权/admission/watch;controller-manager 与 scheduler 通过 Lease 选主
|
v
[输出] kubelet 拉取期望状态、controller 收敛、变更写回 etcd
边界。 LB 只转发 6443,不做 TLS 终止,也不做任何应用层判断,健康检查之外没有智能。客户端连接的是 VIP,不是任何一台具体节点。整个拓扑里,只有 controller-manager 和 scheduler 是选主(同一时刻一个 active),apiserver 和 etcd 才是真正的多副本。
收益。 任意一台控制平面节点下线,客户端连接由 LB 转移到其余节点,集群读写不中断;控制平面节点可以逐台 drain 维护;3 成员 etcd 容忍 1 台故障。
代价。 控制平面成本约等于乘以 3;etcd 写入延迟受最慢成员的 fsync 影响,跨可用区部署尤其明显;LB 或 VIP 成为一个新增的、必须自己也做冗余的组件;故障域变复杂——以前只有一个节点要盯,现在要盯一个 LB、三个节点、一个 etcd 集群。
适用条件。 控制平面中断的业务影响大于三台机器的成本;团队有能力维护 VIP 的高可用(keepalived 主备、云厂商 LB 或 kube-vip);能接受 etcd 成员故障后需要人工介入把它加回来,而不是自动愈合。
入口只一个
后端可三台
状态在 etcd
无状态可扩
05、实战一次
先跑通一个最小但完整的版本。下面的命令未在当前环境实测,以下为预期结果,具体版本号和 API 版本请按当前官方文档核验。
环境与依赖。 三台待做控制平面的节点,三台 worker,Ubuntu 22.04,容器运行时用 containerd,kubeadm、kubelet、kubectl 保持同一版本(例如 v1.30.0)。每台节点先做基础准备:
sudo swapoff -a
sudo modprobe overlay
sudo modprobe br_netfilter
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
先把 LB 放好。 三台控制平面分别是 10.0.0.11、10.0.0.12、10.0.0.13,LB 的 VIP 是 10.0.0.100。HAProxy 只做 L4:
# /etc/haproxy/haproxy.cfg 片段:只做 TCP 转发,不做 TLS 终止
frontend k8s-apiserver
bind *:6443
mode tcp
option tcplog
default_backend k8s-apiserver
backend k8s-apiserver
mode tcp
option tcp-check
balance roundrobin
server master-1 10.0.0.11:6443 check
server master-2 10.0.0.12:6443 check
server master-3 10.0.0.13:6443 check
VIP 本身可以用 keepalived 主备、云厂商的负载均衡服务,或者集群内的 kube-vip 来提供,具体形态取决于你的环境。这一层不解决,后面做的都不算 HA。
第一次 init,我犯了个错。 当时的心路是「先把集群跑起来,LB 后面再接」,于是把 controlPlaneEndpoint 填成了 master-1 的 IP:
# /root/kubeadm-config.yaml(V1,endpoint 用了单节点 IP)
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: 10.0.0.11
bindPort: 6443
nodeRegistration:
criSocket: unix:///run/containerd/containerd.sock
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.30.0
controlPlaneEndpoint: 10.0.0.11:6443
networking:
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
sudo kubeadm init --config /root/kubeadm-config.yaml --upload-certs
init 成功后会打印三类 join 命令:控制平面用的、带 --certificate-key 的、worker 用的。随后初始化本地 kubeconfig 并看节点:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION
master-1 Ready control-plane 12m v1.30.0
(示例输出)
加入第二台控制平面。 从 init 输出的控制平面 join 命令里取出 token、hash 和 certificate key:
sudo kubeadm join 10.0.0.11:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash> \
--control-plane \
--certificate-key <key>
验证 V1。 两个控制平面都 Ready 之后,看一眼 etcd 成员:
kubectl -n kube-system exec etcd-master-1 -- etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
member list
<id-1>, started, master-1, https://10.0.0.11:2380 , https://10.0.0.11:2379 , false
<id-2>, started, master-2, https://10.0.0.12:2380 , https://10.0.0.12:2379 , false
(示例输出)
到这里,V1 是能跑的:两台 apiserver、两台 etcd、两台 controller-manager 通过 Lease 选主,kubelet 都在正常汇报。LB 就在旁边待命,但 kubeadm 还不知道它的存在。这个版本看起来很「HA」,接下来两章会说清楚它到底差在哪。
先让集群能跑
再让它跑得对
两台看似冗余
实际一步就倒
06、排查
诊断链一:断电测试没通过。
- 现象:为了验证 HA,我把 master-2 关机。十几秒后
kubectl get nodes开始转圈,最终返回etcdserver: request timed out。master-1 上的 kube-apiserver 日志里出现 etcd 请求超时、leader 变更之类的信息。开机后恢复正常。 - 怀疑:第一反应是 apiserver 出了问题。毕竟「多了一个 apiserver 反而更不稳」这个现象很反常识。
- 检查:先确认 etcd 成员和健康状态。
kubectl -n kube-system exec etcd-master-1 -- etcdctl \
--endpoints= https://10.0.0.11:2379 ,https://10.0.0.12:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint status --cluster -w table
- 证据:输出里集群只有两个成员,其中一个 endpoint 显示不可达;
endpoint health --cluster报的是部分健康,整体不健康。 - 根因:etcd 用 Raft,写入必须由多数派确认。2 个成员时多数派是 2,也就是容忍 0 台故障;1 个成员时多数派是 1,同样是容忍 0 台故障。所以从 1 台加到 2 台,可用性没有任何提升,只是把成本翻了一倍,并且让故障时多了一层「leader 选举失败」的噪声。
- 修复:见 07,加入第三台控制平面,让多数派变成 2、容忍 1 台故障。
诊断链二:客户端连不上 VIP。
- 现象:LB 就绪后,我试着把 kubeconfig 的 server 改成 VIP:
kubectl --server=https://10.0.0.100:6443 get nodes
返回 Unable to connect to the server: x509: certificate is valid for 10.0.0.11, ... , not 10.0.0.100。
- 怀疑:先怀疑 HAProxy 配错了,或者 VIP 根本没起来。
- 检查:先用
nc -vz 10.0.0.100 6443确认四层连通,再用 openssl 看服务端到底出示了哪张证书:
openssl s_client -connect 10.0.0.100:6443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -text | grep -A1 'Subject Alternative Name'
- 证据:TCP 是通的,说明 LB 没问题;但证书 SAN 里明确的地址是 10.0.0.11 等,没有 10.0.0.100。
- 根因:init 时的
--control-plane-endpoint是 master-1 的 IP,kubeadm 只把那个值写进了 apiserver 证书 SAN 和生成的所有 kubeconfig。控制平面的接入地址是初始化时固化的,不是一个可以事后随手改的配置项。 - 修复:见 07 的两条路径。
顺带踩到的一个坑:certificate key 过期。 第三次 join 时我隔了一段时间才执行,join 报错提示无法下载证书。原因是 --upload-certs 上传到集群里的那份证书有有效期限制,超时后 Secret 会被清理(具体时长需按当前官方文档核验)。补救方式是重新上传并拿到新的 key:
sudo kubeadm init phase upload-certs --upload-certs
错误尝试: 我最初以为 SAN 问题只是客户端的事,于是把每台机器上 /etc/kubernetes/*.conf 里的 server 字段全部改成 VIP,然后重启 kubelet。结果控制平面节点直接 NotReady——kubelet 连不上 apiserver,因为握手阶段就因为证书里没有 VIP 而失败。为什么错:我把「客户端连哪儿」和「服务端证书声明自己是谁」当成了两件可以分开改的事。实际上它们必须匹配,而后者由 init 时的 controlPlaneEndpoint 决定。改客户端配置只会把问题从「连不上 VIP」变成「连谁都不对」。
现象先看清
证据再说话
猜错不丢人
改错才要命
07、优化
修改一:把 etcd 成员补成奇数。
- 根因:2 成员集群多数派为 2,容错为 0。
- 修改:把 master-3 以控制平面身份加入,成员数变 3。
- 原因:多数派变成 2,容忍 1 台故障;同时 apiserver 从两台变三台,LB 有更多可转移的后端。
- 新行为:任意一台控制平面节点不可用时,etcd 仍能选出 leader 并提交写入。
sudo kubeadm join 10.0.0.11:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash> \
--control-plane \
--certificate-key <key>
- 验证:先看成员数,再做一次断电演练。
kubectl get nodes -l node-role.kubernetes.io/control-plane
kubectl -n kube-system exec etcd-master-1 -- etcdctl \
--endpoints= https://10.0.0.11:2379,https://10.0.0.12:2379 ,https://10.0.0.13:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint health --cluster
预期是三个成员全部 healthy。然后对一台控制平面节点执行 drain、关机再开机,确认 kubectl get --raw='/readyz' 和一次 kubectl apply 都不受影响。注意这里只能验证「预期」,实际输出取决于你的环境和网络。
修改二:把控制平面接入地址换成真正的稳定入口。
- 根因:init 时的
controlPlaneEndpoint用了单节点 IP,SAN 与所有 kubeconfig 都被固化在那个地址上。 - 修改:分两条路径,按集群状态选。
路径 A,集群还没有承载生产负载时优先选它:用正确的配置重建。把 controlPlaneEndpoint 改成 VIP,三台控制平面按顺序 join,然后统一验证。这条路最干净,因为证书是一次性签对的。
# /root/kubeadm-config.yaml(V2,endpoint 用 VIP)
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.30.0
controlPlaneEndpoint: 10.0.0.100:6443
apiServer:
certSANs:
- k8s-api.internal.example.com
networking:
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
路径 B,集群已经在跑业务、不能重建时:用 kubeadm init phase certs apiserver 配合 --apiserver-cert-extra-sans 把 VIP 追加进 SAN,重新签发 apiserver 证书,让静态 Pod 重建 apiserver,再把所有 kubeconfig 的 server 改成 VIP。这条路径没有一步到位的官方命令,需要按 phase 分步执行并逐项核验,操作前请按当前官方文档核验每个 phase 的行为和证书分发方式。
- 原因:接入地址一旦进入证书 SAN,客户端与服务端的身份认定就绑在一起了;把地址和节点解耦,才能让任意一台节点被摘掉而不影响客户端。
- 新行为:kubeconfig 里只有一个地址,但它后面有三台 apiserver,任意一台下线对客户端透明。
- 验证:
kubectl get --raw='/readyz?verbose'逐项通过;停掉任意一台控制平面节点后,一次kubectl apply仍能成功;所有节点上的 kubeconfig server 字段一致。
修改三:给 LB 本身做冗余。 这一步不是可选项。LB 从「一个方便」变成了「一个新单点」,所以要给它做主备或者托管 LB,并确认 VIP 漂移后客户端不需要改配置。
补上第三台
多数派成立
摘掉任一台
集群仍然在
08、演进
[同一输入] kubectl apply,客户端连接的那个地址
|
+--[V1] endpoint=10.0.0.11:6443,2 控制平面 / 2 etcd 成员
| 代价:任一控制平面不可用即失去多数派,读写一起超时;接入地址绑在节点上
|
+--[V2] endpoint=10.0.0.100:6443(VIP),3 控制平面 / 3 etcd 成员
| 代价:控制平面成本约 ×3,etcd 写入受最慢成员 fsync 影响,VIP 本身需要冗余
|
[Trade-off]
得到:稳定的客户端入口、滚动维护窗口、单节点故障不中断
失去:三倍控制平面成本、更高的运维复杂度、一个必须自己冗余的新组件
适用边界:3 是稳态;要容忍 2 台同时故障才考虑 5;External etcd 只在需要解耦故障域或 etcd 规模更大时引入
正确性。 V1 的正确性建立在「控制平面节点一个都不坏」之上,这在生产里不是假设,是祈愿。V2 把正确性建立在多数派之上,1 台故障属于设计内事件。
稳定性。 V1 的故障表现是读写一起超时,而且因为 etcd 无法选出 leader,恢复要靠节点回来,没有中间状态。V2 在 1 台故障时仍然能提交写入,代价是写入延迟受剩余成员中最慢那个的影响——所以如果你的控制平面跨可用区,etcd 的 fsync 延迟会直接体现在 apiserver 的写延迟上。
复杂度。 V1 只有一台机器要盯。V2 要盯三台节点、一个 LB、一个 etcd 集群,还要理解 Lease 选主、证书分发、成员增删。这部分复杂度是被买来的,不是被消除的。
成本。 控制平面机器数量约 ×3。如果用的是堆叠 etcd,etcd 的成本包含在这三台里;如果走 External etcd,还要再加一套独立集群。
适用范围。 内部平台、发布流水线、CI 依赖的集群,控制平面中断会直接冻结所有人的变更,3 控制平面是合理起点。如果只是个人实验环境,单控制平面加一份定期快照更划算。
遗留问题。 LB 的冗余度、etcd 的快照与恢复演练、证书轮换的计划、节点故障后 etcd 成员的手工加回流程,这些都不会因为「上了 HA」而自动消失。
一台是运气
两台是错觉
三台是底线
五台是预算
09、洞见
9.1 反直觉判断:从 1 个 etcd 成员加到 2 个,可用性不是提升,而是白花钱
多数派公式 ⌊N/2⌋+1 让 1 成员和 2 成员的容错数都是 0。这个反直觉的地方在于,它违反了我们从「副本越多越安全」得来的直觉。更尴尬的是,2 成员集群在故障时还会多出一层 leader 选举失败的噪声,让排查更绕。所以控制平面数量要么是 1(接受停机),要么是 3(接受容忍 1 台),中间那个 2 是最差的选择。
9.2 反直觉判断:HA 改造完成后,最容易把集群打挂的不是那三台 master,而是那一个 VIP
我们花三倍成本消除节点单点,同时引入了一个新的、被所有客户端依赖的接入点。如果 VIP 只有一台机器在扛,那么可用性并没有提升,只是把「master 挂了」换成了「LB 挂了」。这就是为什么在规划 HA 时,LB 的冗余度要和控制平面节点数一起讨论,而不是留到最后补。
9.3 controlPlaneEndpoint 是架构决策,不是 init 参数
它同时决定了客户端连哪儿、证书签哪些地址、未来能不能平滑换入口。把它当成一个「先随便填,以后再改」的启动参数,会直接导致本文 06 章那条 x509 报错,以及一次本可以避免的重建。工程上的正确顺序是:先定入口,再定拓扑,最后才敲 init。
9.4 apiserver 可以横向扩,etcd 的写入不能
apiserver 是无状态的,扩它解决连接数和 watch 扇出;但所有写最终都要排队进同一个 Raft 日志。这意味着当你发现集群写入变慢时,加 apiserver 几乎不会改善,真正要看的是 etcd 成员的磁盘 fsync 延迟、网络 RTT 和成员地理分布。把「加机器」用在错误的那一层,是 HA 改造里最常见的资源浪费。
反直觉之处
才是真约束
加机器容易
加共识很难
10、系统落地
原来有什么。 一套单控制平面集群,kubeconfig 指向节点 IP,etcd 单成员,控制平面组件全在一台机器的静态 Pod 里。有集群,但没有冗余,也没有可验证的维护流程。
本篇新增什么。 一个稳定入口(L4 LB/VIP,需要它自己也冗余);三台控制平面节点,堆叠 etcd 三成员;两份 kubeadm 配置,V1 的单节点 endpoint 版和 V2 的 VIP 版,正好是本文两条排查链的证据;一套验收标准:控制平面节点数、etcd 成员数、证书 SAN 是否包含 VIP、关掉一台节点后的读写是否正常、单节点 drain 重启是否无感。这些内容可以直接沉淀到 cluster/kubeadm/ha/ 目录下,按「前置 LB」「init 配置」「join 命令」「验收脚本」四块归档,下次做同类改造时不用重新推导。
现在能做什么。 可以对任意一台控制平面节点执行 drain、内核升级、重启、uncordon,并在此过程中保持集群读写、调度与发布正常;可以在理解 quorum 的前提下判断「再加一台到底有没有用」;可以用 openssl 和 etcdctl 两条命令快速区分「是 LB 的问题」还是「是证书的问题」。
还缺什么。 一是 LB 的冗余还没做,它是目前最明确的单点;二是 etcd 的快照与恢复从来没有演练过,有冗余不等于有恢复能力;三是证书轮换没有排进计划,apiserver 证书到期那天同样是一次控制平面中断;四是节点故障后 etcd 成员加回目前还需要人工介入,没有跑通过一次。
下一步如何演进。 先把 LB 做成主备或换成托管 LB,让 VIP 本身不再是单点;再做一次 etcd 快照恢复演练,把恢复时间摸清楚;然后把三台控制平面节点的运维手册写下来,包含 drain 顺序、证书轮换、成员增删。等这些都跑顺了,再来讨论要不要上 External etcd。
有的先盘点
缺的先记账
能做的先做
该等的别急
11、小结
Q1 → 业务容器还活着是因为 kubelet 和容器不受影响,平台像死了是因为 apiserver、controller-manager、scheduler、etcd 全在那一台机器上,变更类操作整体失效
Q2 → 第一个该动的是接入地址:controlPlaneEndpoint 决定 SAN、kubeconfig 和客户端连哪儿,必须先于节点数量定下来
Q3 → VIP 不冗余的话,加 master 只买到了「能在节点之间转移流量」,没买到「入口不会消失」,单点只是换了位置
状态 → 可交付:一个 VIP + 3 控制平面 + 3 成员 etcd 的 HA 集群,外加可执行的验收标准与两条排查链
三个问题答完
一条链路走完
能力已经到手
边界还要记牢
12、作业
12.1 理解题:为什么 3 个 etcd 成员能容忍 1 台故障,而 2 个成员一台都不能容忍?
参考答案:etcd 基于 Raft,写入需要多数派确认。2 个成员的多数派是 2,也就是必须两台全部在线;3 个成员的多数派是 2,允许 1 台缺席。推广开来,容错数是 ⌊(N-1)/2⌋,所以成员数取奇数才不浪费成本,官方推荐 3 或 5。
12.2 实战题:手上有一套 3 控制平面的集群,如何确认它真的是 3 个 etcd 成员,并给出一条能证明「容忍一台故障」的演练步骤?
参考答案:先用 kubectl -n kube-system exec etcd-<node> -- etcdctl ... member list 确认成员数,再用 endpoint status --cluster 看集群整体状态。演练时对一台控制平面节点执行 drain,然后关机或停止 kubelet,在此状态下验证 kubectl get --raw='/readyz' 与一次 kubectl apply 是否正常,最后恢复节点并 uncordon。注意记录每一步的实际输出,而不是凭印象判断。
12.3 排障题:客户端通过 VIP 访问报 x509: certificate is valid for ..., not <VIP>,给出定位路径。
参考答案:第一步先排除四层问题,nc -vz <VIP> 6443 确认连通;第二步用 openssl s_client -connect <VIP>:6443 -showcerts 取出服务端证书,看 SAN 里到底有哪些地址;第三步回查 init 时的 controlPlaneEndpoint,确认它与当前使用的 VIP 是否一致。如果 SAN 里没有 VIP,说明问题出在初始化阶段的入口决策,改客户端配置没有用,要么重新签发 apiserver 证书并重启 apiserver,要么重建集群。
12.4 架构判断题:这套集群要不要上 External etcd?
参考答案:看故障域和运维能力,不要看性能宣传。如果团队无法独立运维一套 etcd 集群的备份、证书轮换与扩容,堆叠 etcd 是更现实的选择,它把控制平面的故障域耦合在一起,但少了一大块要维护的东西。只有当控制平面节点数需要继续扩张(比如 5 台以上)、或者 etcd 需要独立于控制平面做容量与磁盘规划时,External etcd 的复杂度才换得回收益。
会做不算会
会讲才算懂
四题做完后
再去看一遍
13、思考
这次改造真正教会我的,不是 kubeadm 的哪条命令,而是一个顺序问题:在分布式系统里,消除单点从来不是把「一个东西」变成「三个东西」那么简单,你要先想清楚那个「所有东西都指向它」的入口是什么、它自己会不会成为新的单点、以及它和证书、配置、客户端之间的绑定有多深。
我在 05 章犯的那个错,本质上是把顺序颠倒了:先跑通,再考虑入口。这在单体应用里通常没问题,但在控制平面这种「入口即身份」的系统里,顺序反了就要用重建来还债。类似的判断会反复出现:加不加第三台、要不要 External etcd、LB 用主备还是托管,每一条的答案都不取决于技术名词,而取决于你能接受多大的故障域、愿意为它付多少运维成本。
HA 不是一种状态,是一组可以被验证的行为:任意一台节点下线时,集群还能不能读写;任意一台节点需要维护时,能不能无感地摘出去再放回来。做到这两条,才叫 HA。
一念辨边界
一证定根因
一改知取舍
一役见真章

浙公网安备 33010602011771号