1. 扩容前先想清楚:单master集群的真实风险与HA边界
做了几年k8s运维的人应该都有一种感觉:单master集群在测试环境里怎么折腾都没事,一旦上了生产,那个唯一的control-plane节点就像悬在头顶的定时炸弹。我接手过的某模拟项目里,最初就是一台裸金属服务器同时跑着etcd、apiserver、scheduler、controller-manager,外加容器运行时和一堆业务Pod。刚开始两个月风平浪静,直到有一次机房整机重启,整个集群控制面完全失联,worker节点上的Pod虽然还在跑,但kubectl完全没反应,也没法优雅扩缩容,那会儿才真正理解"控制面不可用"和"业务挂掉"之间隔着多远的距离。
1.1 单master宕机的连锁反应
单master集群的脆弱点,其实不在于硬件本身,而在于所有控制面组件共享一台机器的生命周期。etcd一旦不可用,apiserver首先失去后端存储,scheduler和controller-manager拿不到数据,整个k8s集群变成一个"只能看不能碰"的状态。更隐蔽的是,worker节点上的kubelet会反复重试连接apiserver,重试期间节点状态会被打上NotReady,如果恰好有Deployment在做滚动更新,ReplicaSet控制器无法工作,新Pod没人调度,旧Pod可能已经被Terminating,业务直接处于"半残废"状态。
有人会说,那我给单master做物理备份、快照,挂了再拉起来不就完了?这个问题我在实际维护中试过:快照恢复etcd数据确实可以把集群拉回来,但耗时少说也要十几分钟,这期间所有需要读写API的运维操作全部停摆。如果需要变更业务镜像版本、紧急扩容Pod副本数、调整Ingress规则,都得等。生产环境真正需要的,是控制面组件在单一节点失效时能够无缝接管,而不是"修复后恢复"。这就引出了多master扩容的第一个核心理由:把单个故障点的恢复时间从"小时级"降为"秒级"。
1.2 多master解决什么、不解决什么
把单机k8s服务扩容到多个master节点,本质上是做控制平面的高可用,这跟业务侧高可用是两码事。多master集群会让apiserver存在多个副本,由一个负载均衡入口统一分发请求,etcd也变成多节点集群,scheduler和controller-manager通过选主机制保证同一时刻只有一个实例在工作。这套架构解决的是"控制面组件单点故障"问题,它不解决的,是worker节点上的Pod调度问题——如果你的业务副本数只有1,某个worker宕了,控制器虽然感知得到,但没有可用节点承接Pod,业务照样中断。
另外一个必须明确的事实是:多master架构不会让集群"更快"。在一次master宕机切换的瞬间,正在进行的API请求会收到连接超时或拒绝,需要客户端重试。也就是说,高可用解决的是可持续性,不是无感切换。这一点建议在扩容前就和业务方讲清楚,否则实操后他们会拿着监控日志问你:"为什么切换瞬间有请求超时?"
1.3 一次扩容的目标数量:为什么至少3个
我先给个结论:如果当前是单master,别只加1个变成2个,直接往3个走。原因是k8s的etcd天然依赖多数派仲裁,3节点etcd允许挂1个仍然能正常写入,2节点etcd反而变成"只能容忍0个故障"——任何一个节点挂掉,etcd都无法形成多数派,集群直接只读甚至不可用。所以在同一个维护窗口内把master数量扩到3,比先扩2个再去加第3个要安全得多。
这个建议我在真实项目里踩过一次:某环境一开始是1个master,后来想着"先加1个成本低",结果第二个master加入后,有一次第二台机器内核升级触发了重启,恰好在同一时间段第一台master也在发布补丁,两节点etcd各挂一台,整个集群直接失去控制。后面学乖了,所有生产级集群至少3个master节点。扩容动作本身不复杂,真正决定成败的是目标架构和数量规划。
2. 架构方案与前置准备:别急着kubeadm join
很多教程一上来就是"在第二台机器上执行kubeadm join --control-plane",但如果没有想清楚架构,这个join大概率会在某个环节卡住。实操前先把三件事定下来:etcd的部署模式、负载均衡器的形态、以及版本与网络互通清单。这三件事决定了整个扩容过程是顺利还是踩坑。
2.1 堆叠etcd还是外部etcd:怎么选
从k8s高可用架构来看,etcd有两种部署方式:堆叠式(stacked)和外部etcd。堆叠式意味着etcd跟apiserver、scheduler、controller-manager等控制面组件跑在同一个master节点上,kubeadm默认就是这种方式。外部etcd则是把etcd单独部署在独立节点上,不由kubeadm直接管理。
我的建议是:中小规模集群、机器数量可控的情况下,直接选堆叠式。理由很实在——外部etcd集群虽然隔离性更好,但会拉长组件链路,所有控制面节点都要访问一组独立etcd服务器,多了一个层的维护成本。而且kubeadm对堆叠式的自动化处理很完善,扩容时能自动把新节点的etcd加入已有集群,外部etcd则需要自己跟etcd集群打交道,复杂度高不少。只有那种需要极致性能隔离、etcd写入要求严格IO独占的大规模环境,才有必要考虑外部etcd。
2.2 LB放在哪个层级:VIP、四层LB还是SLB
多master集群正常工作的前提是,所有访问apiserver的客户端——包括worker上的kubelet、kube-proxy、集群外使用kubectl的运维人员——都能通过一个稳定入口到达任意一个master节点。现实中我见过直接拿某台master的IP当所有客户端入口的,这种集群即使有三个master,也只是"伪高可用",因为入口绑定的那台机器挂了,客户端不会自动切到其他master。
负载均衡放哪个层级,取决于你的环境:如果在云环境,优先用云厂商提供的托管四层负载均衡器或内网SLB,把三个master的6443端口挂到同一个LB后端,LB地址作为集群入口,天然免运维;如果自建机房裸金属,就在三个master节点前面部署一个VIP方案,配合Keepalived+Nginx/HAProxy把流量分发到多台apiserver上。LB数量上至少得是两个节点配对,否则LB本身会成为新的单点。
2.3 版本一致性、端口清单和证书计划
扩容前最容易被忽略的是版本一致性。新master节点的kubelet、kubeadm、kubelet版本必须与已有集群保持大版本一致,最好是完全一致,否则可能出现API兼容异常或证书签发失败。我建议在已有master上执行kubeadm version,然后在新节点上装同一个版本号的二进制包,不要想当然"装最新版"。
端口清单方面,至少要确认以下端口在三个master之间互通,以及新master能被计划中的候选负载均衡器访问:
| 用途 | 端口 | 协议 | 说明 |
|---|---|---|---|
| apiserver访问 | 6443 | TCP | 负载均衡器、worker节点均需可达 |
| etcd peer通信 | 2380 | TCP | master节点之间etcd互相通信 |
| etcd client通信 | 2379 | TCP | apiserver访问etcd时需要 |
| kubelet心跳 | 10250 | TCP | master节点间需要互相访问kubelet |
| 调度器等选主 | 10259 / 10257 | TCP | controller-manager和scheduler端口 |
证书计划上,如果集群是用kubeadm初始化的,多master扩容依赖的CA证书会自动通过kubeadm join下发,不需要手工拷贝。但有个细节:如果你之前对apiserver的证书做过自定义(比如签发了额外的SAN域名),那么新master join后生成的apiserver证书大概率不会自动带上这些自定义SAN,需要后续统一补签。这类证书类问题往往不立刻暴露,等哪天从外部用域名访问API时才抓狂。
3. 实操扩容主流程:从新节点初始化到Control Plane加入
架构定了之后,真正的实操流程就比较清晰了。kubeadm对控制面扩容的支持已经相当成熟,但每个环节都有一些隐藏细节,如果不理解在做什么,出了问题很难定位。
3.1 新节点基础准备(OS、容器运行时、kubeadm版本)
新master节点上来第一件事不是执行kubeadm join,而是先把基础环境调到跟已有master一致。操作系统尽量选同一发行版,避免systemd模块差异影响kubelet启动;容器运行时(比如containerd或CRI-O)必须装好并配置正确,因为控制面组件都是以Pod形式运行在静态清单里的,没有容器运行时等于白搭。
之后安装kubeadm和kubelet二进制。装完后先不要启动kubelet,因为此时没有集群配置,它只是空转。可以用如下命令检查版本是否匹配:
kubeadm version kubelet --version另外在基础准备阶段就把IPv4转发和iptables参数配好,这一步直接决定后续Pod是否能正常通信。不少扩容失败案例的共性就是:新节点网络参数没配,kubelet起来了但节点状态诡异。
3.2 生成join所需关键信息:token与证书密钥
kubeadm把控制面扩容设计成一个高度自动化的流程,核心思路是"已有集群生成凭证,新节点拿凭证加入"。要在第一台master上生成新的discovery token和证书密钥,命令如下:
kubeadm token create --print-join-command这条命令会输出类似下面的join命令样例,其中包含token、apiserver地址以及discovery hash:
kubeadm join 192.168.10.11:6443 \ --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:e1a0... \ --control-plane \ --certificate-key 512f...这里的--certificate-key是控制面扩容专用的,用来解密已上传到集群的CA等证书数据。如果这条join命令里没有这个参数,说明集群在init时没有上传证书或者证书已经过期,需要到已有master节点上执行:
kubeadm init phase upload-certs --upload-certs它会重新生成一个证书密钥并输出到终端。记住这个密钥只有两个小时的有效期,实操时最好在维护窗口内一气呵成,不要临时有事出去吃个饭回来再执行join。
3.3 执行Control Plane join及常见失败点
准备工作做好后,在新机器上执行完整命令。这里需要特别指定advertise地址,也就是新master对外通告的IP,如果不指定,kubeadm会默认取默认网卡地址,在许多多网卡环境会选错:
sudo kubeadm join 192.168.10.11:6443 \ --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:e1a0... \ --control-plane \ --certificate-key 512f... \ --apiserver-advertise-address 192.168.10.12执行过程中kubeadm会自动完成几件关键事情:分发生成控制面组件所需的证书、在本地创建etcd配置并把它静态加入已有etcd集群、生成kubelet配置并启动控制面组件Pod。等待几分钟后,新节点上的静态Pod列表会陆续出现:
kubectl -n kube-system get pods -o wide | grep -E 'master-2|etcd-master-2'常见失败点主要有几个:一个是token过期或hash不匹配,提示校验失败,重新生成token即可;另一个是etcd加入失败,日志里会出现连接192.168.10.11:2380超时之类的错误,这时要回到前置准备检查2380端口互通;还有一个比较隐晦,新节点上残留了之前试验过的/etc/kubernetes目录,kubeadm会拒绝覆盖,需要提前清理。
3.4 同步自定义apiserver参数与组件配置
kubeadm跑完join只是"完成80%",剩下20%才是最容易埋雷的地方。已有单master集群在初始化时,apiserver很可能加了自定义参数,比如:
# /etc/kubernetes/manifests/kube-apiserver.yaml spec: containers: - command: - kube-apiserver - --service-node-port-range=30000-40000 - --enable-aggregator-routing=true - --feature-gates=APIPriorityAndFairness=true新join的master节点默认只会带kubeadm生成的默认参数,不会自动同步你手动加的那些配置。于是就会出现一种很奇怪的现场:新master看起来是Ready状态,但某些功能跟旧master不一致,比如访问一个自定义端口范围的服务时,新master上报错而旧master完全正常。
解决办法是把已有master上的/etc/kubernetes/manifests/kube-apiserver.yaml、kube-controller-manager.yaml、kube-scheduler.yaml与现有新节点的对应文件做一次diff,找出不一致的参数并手动同步到新master上,然后重启kubelet使其重建静态Pod:
sudo systemctl restart kubelet这类操作建议三个master一起对齐,否则以后排查问题时每个master行为都不一样,很难相信监控数据。
4. 负载均衡层落地:让worker和kubectl不再绑定单masterIP
多master节点都Ready之后,还差最后一块拼图:把集群入口从"某台master的固定IP"迁移到负载均衡器上。这一步不做,高可用就是纸面空谈。
4.1 为什么必须把入口前置到LB
看一个具体场景:当前有3个master节点,但worker节点上的kubelet仍然通过https://master1:6443连接apiserver(这是kubeadm init时写入kubelet的kubeconfig里的地址)。如果master1故障,即使master2、master3都健康,worker也找不到入口,集群依然不可用。同理,我们自己用的kubeconfig文件里的server地址如果还是master1,集群控制面在高可用也没意义。所以负载均衡器是给所有"apiserver客户端"提供统一入口,让它们不再关心背后哪台master存活。
如果是自建机房环境,比较轻量的方案是Keepalived+HAProxy组合:Keepalived对外提供一个虚拟IP(VIP),HAProxy负责把转发到后端的三个6443端口上。云环境则直接用托管的四层LB,免去Keepalived本身的维护。
4.2 直接可用的HAProxy+Keepalived配置示例
这里给出一个典型的HAProxy配置,监听6443端口,后端是三个master节点:
frontend k8s_apiserver bind *:6443 mode tcp default_backend k8s_apiserver_backend backend k8s_apiserver_backend mode tcp balance roundrobin option tcp-check server master-1 192.168.10.11:6443 check fall 3 rise 2 server master-2 192.168.10.12:6443 check fall 3 rise 2 server master-3 192.168.10.13:6443 check fall 3 rise 2这里选择mode tcp而不是mode http,后面细讲。Keepalived配置则要定义instance和VIP,建议配对部署在至少两台LB节点上,避免LB节点单点:
vrrp_instance kubernetes { state MASTER interface eth0 virtual_router_id 64 priority 100 virtual_ipaddress { 192.168.10.50 } }两台LB的配置除了priority有差异之外基本相同,VIP为192.168.10.50。这样LB节点挂一台,另一台可以接管VIP,kubectl和worker用的地址依然是同一个VIP,不会感知后端变化。
4.3 切换kubeconfig的步骤与回滚预案
负载均衡器就绪后,开始切换客户端入口。如果是从单master迁移过来的,建议分成两个阶段。第一阶段把/etc/kubernetes/admin.conf和其他在已有master上的组件kubeconfig里的server地址改为https://192.168.10.50:6443,同时保持旧地址也还能访问,先让所有master节点上的控制面组件都通过LB连接apiserver。第二阶段再把worker节点上的/etc/kubernetes/kubelet.conf和kube-proxy相关配置也切换到LB地址,并逐个重启kubelet:
sudo systemctl restart kubelet这里建议一台一台地改,每次改完观察节点心跳是否正常,不要一次性全改完再重启,否则万一VIP没起来,整个集群控制面全断。回滚预案也要提前想好:如果切换后节点状态异常,只需把kubeconfig里的server地址改回某个健康master的IP,重启kubelet就能恢复。
我自己在做这类切换时还会顺手把员工本地kubectl的kubeconfig同步换到VIP地址,并把旧master IP从DNS和配置里清干净,避免"有的客户端走VIP、有的直连master"这种混乱状态。
5. 扩容后的高可用验证:别只看kubectl get nodes
看到三个master节点都处于Ready状态时,很多人会觉得任务完成。但我的经验是,扩容完成之后的验证环节恰恰是最容易暴露问题的,特别是那些只有在故障切换时才会出现的隐患。
5.1 控制面组件与etcd健康检查
首先确认控制面Pod分布是否均匀。多master架构下,apiserver会在每个master节点都运行一个,scheduler和controller-manager则只会有一个处于leader状态,但Pod也是每个master节点都有。用下面的命令查看:
kubectl -n kube-system get pods -o wide | grep -E 'kube-apiserver|kube-scheduler|kube-controller-manager|etcd'接着检查etcd集群成员。直接启动一个临时的etcdctl容器,指向任意一个etcd端点,查看成员列表和健康状况:
kubectl -n kube-system exec -it etcd-master-1 -- etcdctl member list kubectl -n kube-system exec -it etcd-master-1 -- etcdctl endpoint health正常的输出应该包含三个member,且状态都是healthy。如果只有一个member,说明扩容时新节点的etcd没有真正加入集群,只是"假装"成功了,这个问题非常隐蔽,后续一旦写流量激增就会在etcd日志里看到大量apply错误。
5.2 故障演练:单master宕机后的真实表现
验证高可用最有效的手段是破坏性测试。选一个维护窗口,直接关闭或重启一台master节点(比如master-2),然后观察集群行为:
- kubectl命令是否仍然能正常执行,且可以分配到剩余master节点;
- worker节点是否仍然能正常上报心跳;
- 已有业务的Pod是否保持Running状态;
- 如果可以,尝试临时创建一个Deployment,看能否正常调度。
正常情况下,即使一台master挂了,三节点etcd和剩余的两个apiserver仍能组成可用仲裁,kubectl可以使用VIP地址正常访问,worker节点也不会出现大面积的NotReady状态。我在实际演练中还会去检查宕机master上的Pod被调度到其他节点的行为是否正常,以及它恢复重启后能否自动回流到Ready状态。
这里有一个容易被忽略的细节:故障演练之后,原节点重新启动并恢复Ready,并不代表它自动重新加入了etcd集群。如果etcd因网络分区而把故障成员踢出,需要你手动用etcdctl member add/remove重新调整集群成员。不过kubeadm管理的etcd配置一般会自动尝试重新加入,但为了稳妥,恢复后仍建议执行一次etcdctl member list确认成员数量。
5.3 证书生命周期与后续升级节奏
很多集群扩容完之后就忘了证书的事。kubeadm创建的控制面证书默认有效期是1年,多master集群下的每个master节点都有一份对应的kubelet客户端证书和控制面证书。规划维护节奏时,建议在每半年就检查一次证书剩余时间:
kubeadm certs check-expiration如果在某台master上发现证书快过期,需要在这台机器上执行:
sudo kubeadm certs renew all sudo systemctl restart kubelet多master集群的证书更新有个坑:直接在单节点上renew证书,会导致该节点重新加载apiserver服务,但它持有的etcd客户端证书如果不同步更新,可能出现apiserver连接etcd失败的情况。所以在扩容后维护文档里一定要把"证书更新顺序"写清楚:先从etcd集群层面确认多数派在线,再逐台renew并重启,最后通过etcd endpoint health验证回归正常。
6. 扩容过程中必须避开的坑(实际运维总结)
这部分沉淀自多个真实项目中的踩坑记录。每个坑单独看都不复杂,但叠加在一起时,常常会让一次扩容变成通宵排障。
6.1 第一坑:etcd 2节点伪HA
前面已经提过,但不建议被忽略:从1个master扩到2个master,etcd会变成2节点集群,而2节点的仲裁阈值是2——也就是说,任何一台master故障,etcd都无法工作。很多人在这一步误以为"已经高可用了",实际上比单点更糟,因为原有单点模式至少不会出现etcd成员间日志分裂的问题。所以从单master扩容时,务必一次性加到3个master节点,不要在2节点状态长期逗留。
如果因为硬件预算限制只能先加到2个,至少要明确这是过渡态,并且维护周期里忍住所有"第二个master宕机"的意外风险。我甚至见过有团队拿2节点master跑了三个月,期间每次升级都很怕,这种状态真不建议长期存在。
6.2 第二坑:忘同步自定义APIServer参数
再强调一次:kubeadm join --control-plane只保证"新master加入集群",不保证"新master配置和旧master一致"。已有的自定义参数需要手动同步。实操中可以写一个脚本对比三个master节点上的静态清单文件:
for f in kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml; do diff /etc/kubernetes/manifests/$f master-2:/etc/kubernetes/manifests/$f done发现差异后按需修改,不用全量镜像。比较常被自定义的参数包括--service-node-port-range、--service-cluster-ip-range、--bind-address、--feature-gates等,把这些参数保持三节点一致,可以大幅减少后续维护时的认知负担。
6.3 第三坑:LB健康检查用了HTTP而apiserver带证书
HAProxy如果配置为mode http,会向apiserver发送HTTP HEAD请求做健康检查。但apiserver的6443端口默认是TLS端口,普通HTTP探测会直接得到TLS握手失败,健康检查会一直报down,后端流量得不到分发。所以我们配置四层负载均时,统一用mode tcp,并通过tcp-check做检测,或者在需要更精细检查时可用以下方式:
option tcp-check tcp-check connect tcp-check expect string "HTTP/1.1 200 OK" rstatus如果后面改用云负载均衡器,健康检查协议同样建议直接选TCP,不要用HTTP,因为默认TLS端口对HTTP探测不友好,容易误判后端不健康。这个坑我自己就踩过,当时把所有精力花在排查为什么三个master都没流量,最后发现LB后台显示后端全红,原因只是健康检查类型选错。
6.4 回滚与恢复思路
扩容过程中如果遇到无法解决的故障,最稳当的兜底手段是回到扩容前状态。因此动手前一定要保留好关键数据:至少在已有master节点上执行一次etcd快照备份:
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snapshot.db备份文件不要只存在本机,同步到其他机器或对象存储里。回滚时假设新master节点已经损坏或数据不可信,可以在保留旧master运行的情况下,清掉新master上的kubeadm相关数据:
sudo kubeadm reset -f这样新节点会脱离集群,旧master维持原样。但要注意:如果新节点加入过程中已经对etcd集群做了成员变更(比如主动remove或add),回滚还需要用etcdctl把对应member移除,确保etcd成员数量恢复原状。这部分最好提前写在运维SOP里,压测前先演练一遍,不然真到故障现场再翻命令临时试,压力会大得多。
扩容单master到多master,核心思路其实不复杂:选对etcd模式、做好负载均衡、认真执行join、验证故障转移。真正的难点在于每一个容易被忽略的细节——自定义参数的同步、健康检测协议的选择、证书的生命周期管理——这些才是决定一个集群扩容完能不能真正扛住故障的关键。希望这些实操经验能让你少走一段弯路。