K8s 1.2x多Master高可用集群部署:kubeadm + keepalived + haproxy实战
kubeadm 部署 k8s 1.2x 多 master 高可用集群这个需求我在不同项目里落地过好几回。一开始玩 K8s 都用单 master后来业务稍微上点规模就发现单点根本扛不住控制面一挂整集群瘫痪。这套方案就是把 kubeadm 和 keepalived haproxy 组合起来让 1.20 到 1.27 之间的 1.2x 版本跑出三节点控制面高可用任意一个 master 宕机kubectl、调度器、业务 Pod 都不受影响。本文适合运维、DevOps以及准备把 K8s 从测试环境往生产级别过渡的同学读完后可以直接照着操作搭出一套完整的多 master 高可用集群。1. 方案选型与整体架构拆解1.1 为什么选择 kubeadm 而不是二进制部署聊到装 K8s主流无非两条路二进制手工部署或者 kubeadm。二进制部署的逻辑是把 kube-apiserver、kube-controller-manager、kube-scheduler、etcd、kubelet 一个个下载下来自己写 systemd 单位文件自己签证书自己配置启动参数。这个过程对理解 K8s 内部原理有好处但不适合追求可维护、可复制的场景尤其多 master 环境下三台控制面的证书同步、配置一致性很容易出问题。kubeadm 的价值在于把社区沉淀的最佳实践固化成了命令证书生成、静态 Pod 编排、控制面组件启动参数、token 发放这些事情都不用自己手搓。它也不是黑盒所有配置都能用配置文件显式声明。我一般跟别人说二进制部署像从零组车kubeadm 像厂商给的原厂组装件该有的都有自己要做的是校准和微调。到 1.2x 这个版本区间kubeadm 已经非常稳定中小规模生产环境完全够用。如果你追求的是纯云原生托管方案可以用发行版自带工具但只要你是自己管理服务器kubeadm 就是最合理的选择。1.2 高可用负载均衡层的方案取舍多 master 集群的核心难点不在 init而在如何让多个 apiserver 对外呈现一个稳定入口。解决方案大致两类一类依赖云平台负载均衡另一类是自己搭建 VIP 负载均衡层。云平台负载均衡的典型做法是创建一个内网 SLB后端挂三台 master 的 6443 端口。优点是免维护、自带健康检查缺点是绑定云厂商你在裸金属或者私有云环境没法用。keepalived haproxy 的组合则完全不依赖外部设施三台控制面节点各自跑一个 haproxy 做四层负载均衡keepalived 负责虚拟 IP 漂移。这台组合我跑过多个环境稳定性完全够而且故障切换路径清晰、可控。为什么用 haproxy 而不是 Nginx 或其他工具haproxy 对 TCP 四层的转发和健康检查支持更顺手配置语义直观统计页还能直接看后端 apiserver 的健康状态Nginx 当然也能用 stream 模块做但健康检查能力相对弱一些。至于 keepalived它就是专门做 VRRP 虚拟 IP 漂移的核心职责只有一个保证 VIP 始终落在健康的节点上。1.3 多 master 架构下的组件分工与流量走向这套架构的控制面是三个 master 节点每个节点上同时运行 kube-apiserver、kube-controller-manager、kube-scheduler 和 etcd。对这就是 etcd 的 stacked 模式etcd 与控制面组件混部适合三到五节点的中小规模集群。如果追求更强的故障隔离可以单独部署三台 etcd 做 external etcd 模式但对绝大多数场景stacked 模式够用而且运维成本低很多。流量走向大概是这样的管理员或业务组件访问https://VIP:6443这个 VIP 由 keepalived 管理当前持有 VIP 的节点上的 haproxy 负责接收连接然后按轮询策略转发到三台 apiserver 的 6443 端口。apiserver 处理完请求后通过本机 kubelet 访问 etcd 和调度器不需要额外网络跳转。kube-controller-manager 和 kube-scheduler 默认只和本机 apiserver 通信kubelet 则通过 VIP 上报状态。这里有几个关键点。一是三个 master 必须能互相访问 6443 和 etcd 端口二是 haproxy 要部署在三台 master 上不是单独两台机器这样 VIP 不管漂到哪台流量都能无损接住。master 数量最低三台因为两台节点在脑裂场景下无法形成多数派etcd 选举会出问题反而更危险。2. 环境准备与基础配置清单2.1 硬件规划与系统版本选择先给出一个实际可用的机器规划本文示例按三台 master 加两台 worker 来设计。节点角色主机名示例数量最低配置部署组件Master-1k8s-master114C8Gapiserver / controller-manager / scheduler / etcd / haproxy / keepalivedMaster-2k8s-master214C8G同上Master-3k8s-master314C8G同上Workerk8s-worker1 / k8s-worker22按业务定kubelet / kube-proxy / 业务Pod系统我推荐 Ubuntu 22.04 LTS内核 5.15网络栈和 cgroup v2 支持都很好。CentOS 7 也可以用但如果跑 1.24 以上的版本内核 3.10 虽然能跑网络转发和 conntrack 上容易折腾最好先升内核到 5.4。这里不展开系统迁移的话题反正核心思想是用新不用旧内核别太老。主机名规划越早越好。kubeadm 生成的证书里有节点名和 IP 的 SAN后续很难改。把三台 master 和两台 worker 的 hostname 提前定好然后写进每台机器的/etc/hosts注意 IP 和 hostname 要一一对应。2.2 系统参数、内核模块与防火墙放通基础系统参数这一步是很多初装问题的大头。先说 swapkubeadm 初始化前必须关掉。K8s 的 Pod 内存隔离依赖 cgroup如果系统开着 swapkubelet 对内存的判断会混乱直接导致调度和驱逐逻辑错乱。swapoff -a sed -i /swap/s/^/#/ /etc/fstab然后加载内核模块并设置转发参数cat EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilter cat EOF | 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 sysctl --systemnet.bridge.bridge-nf-call-iptables这个参数的作用是让桥接的流量也经过 iptables 处理。K8s 的 Service 和 NetworkPolicy 依赖这条链路不设置会导致 Pod 之间通、Service 访问不通的诡异现象。net.ipv4.ip_forward则是 Pod 间和跨节点转发的基础。时间同步也要提前配好三台 master 的 etcd 对时钟偏移非常敏感。用 chrony 和 NTP 服务器对齐配置完执行chronyc sources确认偏移量。防火墙这块要注意。桌面环境图省事可以直接关掉 firewalld但生产环境必须按端口清单放行不要一刀切关防火墙。放行规则整理如下方向端口用途所有节点6443apiserver 访问含 VIPmaster 间2379/2380etcd 通信master 间10250kubelet 交互worker 间10250kubelet 交互外部30000-32767NodePort 服务master 间10257 / 10259controller-manager / scheduler 指标端口SELinux 在 CentOS 系列上如果不知道怎么配服务上下文直接临时设置为 permissive 然后把/etc/selinux/config改成 disabled避免规则冲突。Ubuntu 没有 SELinux 默认问题跳过。2.3 containerd 安装与 cgroup 驱动对齐1.24 版本之后 K8s 移除了 dockershim容器运行时直接用 containerd 最稳妥。安装时注意版本对应1.2x 集群建议 containerd 1.6.x 或 1.7.x通过系统包管理器安装也行但无论怎么装都要改/etc/containerd/config.toml里的两个关键配置。一个配置是 sandbox 镜像把sandbox_image改成当前 K8s 版本对应的 pause 镜像版本比如registry.k8s.io/pause:3.9。另一个是 cgroup 驱动必须和服务端对齐[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup truekubelet 默认的 cgroup 驱动是systemd如果 containerd 保持默认的cgroupfskubelet 启动后日志里会出现cgroup driver mismatch节点永远 Ready 不了。改完配置执行systemctl restart containerd然后crictl info确认输出里的systemdCgroup是 true。kubelet、kubeadm、kubectl 三个二进制版本必须一致本文以 1.26.x 为例1.25/1.27 也完全适用。安装时注意给软件包做 holdapt或 excludeyum防止后续意外升级。配置软件源时选一个离你网络环境最近的镜像地址能明显加速拉包。3. 负载均衡层haproxy keepalived 部署细节3.1 haproxy 安装与核心配置解析三台 master 都要装 haproxy因为 VIP 可能漂移到任意一台持有 VIP 的那台必须能接收 6443 流量。安装很简单CentOS 用yum install haproxyUbuntu 用apt install haproxy。haproxy 的配置我直接给出一个经过生产验证的版本global maxconn 50000 log /dev/log local0 info defaults mode tcp timeout connect 10s timeout client 30s timeout server 30s frontend k8s-api bind *:6443 default_backend k8s-masters backend k8s-masters balance roundrobin option httpchk GET /healthz http-check expect status 200 server master1 172.16.10.11:6443 check inter 3s fall 2 rise 2 server master2 172.16.10.12:6443 check inter 3s fall 2 rise 2 server master3 172.16.10.13:6443 check inter 3s fall 2 rise 2这里有两个设计细节值得说。第一模式必须是mode tcp因为 apiserver 是 HTTPS 的 TCP 流量四层转发最直接不要做 TLS 终止。第二健康检查用 HTTP 探活而不是纯 TCP connectoption httpchk GET /healthz请求的是 apiserver 的/healthz接口这能识别出 TCP 通但 apiserver 逻辑异常的中间状态。inter 3s fall 2 rise 2的意思是每 3 秒探测一次连续 2 次失败标记为不可用连续 2 次成功恢复。这个参数决定了故障发现时间太大会导致 VIP 切换后请求还在往坏节点上打。配置文件写好后在每台 master 上启动 haproxy 并设为开机自启systemctl enable --now haproxy systemctl status haproxy3.2 keepalived 配置与健康探活脚本keepalived 的核心逻辑是 VRRP一组节点里优先级最高的当 MASTER持有 VIP其他节点当 BACKUP 监听 MASTER 状态。MASTER 挂了BACKUP 自动升级接管 VIP。配置如下global_defs { router_id K8S_HA } vrrp_script check_haproxy { script /etc/keepalived/check_haproxy.sh interval 2 weight -20 } vrrp_instance K8S_VIP { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass K8s_HA } unicast_peer { 172.16.10.12 172.16.10.13 } virtual_ipaddress { 172.16.10.100/24 dev eth0 label eth0:ha } track_script { check_haproxy } }先说优先级设计。Master-1 上state MASTER、priority 100Master-2 和 Master-3 用state BACKUP、priority 分别 90 和 80。记住 BACKUP 节点的 priority 必须不一样否则平级抢 VIP 会出现脑裂。virtual_router_id 51在同一二层网络内要唯一多个 keepalived 集群别共用同一个 ID。接下来是极其容易漏掉的一步track_script check_haproxy。如果不检查 haproxy 进程会出现一种危险场景——keepalived 认为节点活着但 apiserver 流量已经到了 haproxy 而 haproxy 已经挂了VIP 并不会切换整个集群的 API 不可用。这个脚本是保证VIP 永远只在 haproxy 健康的节点上的关键。脚本内容很简单#!/bin/bash if systemctl status haproxy --no-pager /dev/null 21; then exit 0 else exit 1 fi保存到/etc/keepalived/check_haproxy.shchmod x加上执行权限。weight -20表示脚本探测失败时当前节点优先级降 20MASTER 的 100 降到 80低于 BACKUP 的 90VIP 就会漂走BACKUP 节点如果探测失败优先级降到 70 也不会接管。另外我在配置里用了unicast_peer而不是默认的组播 VRRP。默认组播在部分网络或云平台里会被过滤器挡掉导致 keepalived 互相感知不到对方直接在两个节点上都升成 MASTER。单播方式把对端 IP 明确定死更可靠。三台节点都要在unicast_peer里写另外两台 IP并且对称配置不能只在一台写。3.3 验证 VIP 漂移行为先在三台 master 上启动 keepalived然后检查 VIP 是否落在 Master-1systemctl enable --now keepalived ip addr show eth0预期能看到172.16.10.100/24绑定在eth0:ha上。接着测试一下 VIP 上的 apiserver 入口是否可用curl -sk https://172.16.10.100:6443/healthz正常会返回ok。然后做故障模拟在 Master-1 上systemctl stop haproxy隔 3 秒再看 VIP 是否漂到了 Master-2ip addr show eth0 | grep 172.16.10.100 curl -sk https://172.16.10.100:6443/healthz如果配置正确第二次 curl 依然返回 ok说明 VIP 已经切换并且在新的节点上正常提供流量入口。这一步验证必须在初始化 kubeadm 之前做因为后续所有控制面节点都要通过这个入口连接。4. kubeadm 初始化与多控制面节点接入4.1 第一个 master 的初始化配置在 Master-1 上初始化前先确认 VIP 确实在本机。kubeadm 对controlPlaneEndpoint要求很严格如果配置的 VIP 不在本机网卡上报错controlPlaneEndpoint is not local。初始化配置我建议写文件不要用一长串命令行参数后面维护和升级都能复用。创建kubeadm-config.yamlapiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.26.4 controlPlaneEndpoint: 172.16.10.100:6443 networking: podSubnet: 10.244.0.0/16controlPlaneEndpoint直接写 VIP 加端口这样生成的 admin.conf、kubelet 的连接地址全部指向 VIP后续任何节点 join 都走 VIP 而不是某个 master 的 IP。podSubnet选了10.244.0.0/16这是 Flannel 的默认网段如果你后面用 Calico 可以改成192.168.0.0/16但建议一开始就统一避免 CNI 和 kube-controller-manager 的 CIDR 对不上。初始化前先把镜像拉下来避免初始化超时kubeadm config images pull --config kubeadm-config.yaml然后执行初始化kubeadm init --config kubeadm-config.yaml --upload-certs这里加--upload-certs是为了把控制面证书加密上传到集群 secret这样后面两个 master join 时可以用--certificate-key参数自动拉取证书否则还得手动拷贝一套 pki 目录麻烦且容易漏文件。初始化成功后会输出一大段内容重点保存两段命令一段是kubeadm join ... --token ... --discovery-token-ca-cert-hash ...另一段是带--control-plane --certificate-key的控制面 join 命令。建议直接重定向保存kubeadm init ... /root/kubeadm-init.log 21 grep -A2 control-plane /root/kubeadm-init.log然后配置 kubectlmkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config4.2 控制面节点追加第二个和第三个 master在 Master-2 和 Master-3 上先做同样的系统参数和 containerd 配置然后把上一节保存的 control-plane join 命令原样执行。典型命令如下kubeadm join 172.16.10.100:6443 \ --token xxxx.yyyyy \ --discovery-token-ca-cert-hash sha256:xxxxxx \ --control-plane \ --certificate-key xxxxxxxxxxxxxxxxxxxxx执行完大概等两三分钟这个过程中 kubeadm 会自动把 apiserver、controller-manager、scheduler、etcd 的静态 Pod 拉到这台机器上并把证书注册进集群。回到 Master-1 上执行kubectl get nodes应该能看到三个 master 都处于 NotReady 状态这是正常的因为还没装网络插件。但它们的角色显示为control-plane,master说明控制面已经成功接入。如果--certificate-key当初没保存也不用慌。在 Master-1 上重新生成并获取证书 key然后重新获取 join 命令即可kubeadm init phase upload-certs --upload-certs kubeadm token create --print-join-command注意整个集群里控制面证书只有一份所有 master 共用--upload-certs生成的 key 每次都会变化但集群本身不受影响。4.3 worker 节点接入与 CNI 网络插件部署worker 的 join 更简单去掉--control-plane那一段即可kubeadm join 172.16.10.100:6443 \ --token xxxx.yyyyy \ --discovery-token-ca-cert-hash sha256:xxxxxxjoin 完成后 master 上kubectl get nodes能看到两个 worker状态同样是 NotReady。网络插件选择上我推荐 Calico。功能覆盖全面NetworkPolicy、IPAM、BGP 都有文档和社区都很活跃。安装之前先下载对应版本的 manifestcurl -O https://raw.githubusercontent.com/projectcalico/calico/v3.25.1/manifests/calico.yaml打开calico.yaml找到环境变量CALICO_IPV4POOL_CIDR如果初始化时 podSubnet 用的不是 Calico 默认的192.168.0.0/16手动改成一致我这里改成10.244.0.0/16。然后应用kubectl apply -f calico.yaml等待 Calico 相关 Pod 跑起来kubectl get pods -n calico-system kubectl get nodes等所有节点变成 Readykube-system命名空间下的 coredns 也会从 Pending 变成 Running。coredns 一直 Pending 的经典原因就是网络插件没装或者没生效优先排查 Calico 的 Pod。4.4 集群自检与 kube-proxy 模式调优集群搭完不要急着上线业务先做一轮自检我习惯用下面这套检查单检查项命令期望结果节点状态kubectl get nodes全部 Ready角色正确核心组件 Podkubectl get pods -A全部 Running 或 Completedetcd 健康在任一 master 执行 etcdctl 成员检查all endpoints healthy控制面静态容器crictl ps能看到 apiserver、etcd 等容器join tokenkubeadm token list非空且未过期还想提一个体验提升项kube-proxy 的转发模式。默认是 iptablesPod 和 Service 数量多起来之后 iptables 规则会线性膨胀更新规则时有明显的延迟抖动。改为 IPVS 模式能改善这个问题。做法是修改 kube-system 下的 kube-proxy 配置kubectl edit cm -n kube-system kube-proxy把mode字段从空字符串改成ipvs然后删掉 kube-proxy 的 Pod 让它重新调度kubectl delete pod -n kube-system -l k8s-appkube-proxy所有节点需要提前加载 IPVS 内核模块modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrrIPVS 模式下 kube-proxy 用哈希表管理转发规则性能和稳定性都更好。除非你集群里 Service 数量极少否则我建议默认就换。5. 高可用测试与日常维护排查5.1 故障演练一台 master 当机实测高可用不是部署完就完事得实测。我的习惯是在维护窗口里做一次真实的控制面故障演练别怕出事先把服务和业务 Pod 的预期说好就行。演练方式很简单模拟最坏情况在 Master-1 上停掉 kubelet 和 haproxysystemctl stop kubelet systemctl stop haproxy然后在任意一台机器上循环探测 VIP 的 apiserver 健康状态while true; do echo $(date) $(curl -sk -o /dev/null -w %{http_code} --connect-timeout 2 https://172.16.10.100:6443/healthz) sleep 2 done正常情况你会看到输出短暂的000haproxy 停掉到 keepalived 完成切换的空窗期然后恢复为200。这个空窗期取决于 keepalived 的检查间隔和 fall 次数我配置的inter 2加脚本检查实测空窗在 3 到 6 秒。切换期间kubectl get nodes正常执行已有的业务 Pod 完全不受影响因为控制面所有组件都通过 VIP 访问并不会感知到单个 master 宕机。演练完恢复 Master-1systemctl start kubelet systemctl start haproxykubelet 起来后会自动重新加入集群节点状态从 NotReady 变回 Ready。VIP 因为 Master-1 优先级更高keepalived 会自动把 VIP 抢回来整个流程不需要手工干预。5.2 证书过期与定期续期策略kubeadm 集群有个常被忽略的问题控制面证书默认一年有效期。到时间不续apiserver 和 kubelet 之间的 TLS 握手会直接失败集群表现为节点反复 NotReady、调度不了 Pod。不管哪个节点执行kubeadm certs check-expiration会看到一张证书有效期表里面admin.conf证书的剩余时间往往最短。手动续期流程如下在每台 master 上依次执行kubeadm certs renew all --configkubeadm-config.yaml systemctl restart kubelet针对 kubectl 的管理员 kubeconfig还需要用 init 命令重新生成 admin.confkubeadm init phase kubeconfig admin --configkubeadm-config.yaml生成的新文件覆盖到$HOME/.kube/config。操作之前一定先备份/etc/kubernetes整个目录续期过程虽然官方支持但在多 master 环境里单台续期再重启即可不需要同时重启所有节点。我建议把检查证书剩余时间加到月度巡检清单里别等到集群告警再救火。5.3 常见问题速查表与避坑经验多 master 部署中踩过的坑不少把高频问题整理成速查表基本覆盖了 80% 的故障场景。症状可能原因排查与解决init 报controlPlaneEndpoint is not localVIP 不在本机网卡先用ip addr add VIP/32 dev lo临时挂上或确认 keepalived 已接管节点一直 NotReadyCNI 没装 / containerd 和 kubelet cgroup 驱动不一致crictl ps看容器数量journalctl -u kubelet查具体报错coredns 一直 Pending没装网络插件或 IP 池冲突确认 Calico Pod 状态检查CALICO_IPV4POOL_CIDRjoin 卡住并提示访问 VIP 失败防火墙拦截 6443或 haproxy 配置错误在目标节点curl VIP:6443逐层排查token 过期导致 join 失败token 默认 24 小时有效kubeadm token create --print-join-command重新生成etcd leader 频繁切换时间不同步 / 网络抖动chronyc sources检查时间偏移确认 master 间网络延迟VIP 切换后请求依然失败apiserver 健康检查间隔过长故障节点未被摘除缩短 haproxy 的inter间隔并确认 keepalived 的探活脚本有效kubeadm join 时 certificate key 报错签发信息不存在在已有 master 执行kubeadm init phase upload-certs --upload-certs重新获取还有几个一次性经验写在这里供参考。keepalived 的auth_pass建议控制在 8 位以内的字符串超过指标在部分版本上会莫名报错haproxy 后端 server 里填写的 IP 一定要写三台 master 真实的 IP别偷懒填 127.0.0.1。另外给 haproxy 开一个监听在 8080 端口的统计页配置stats enable系列指令日常排查时能看到后端 apiserver 的实时状态和连接数非常有帮助。还有一个小技巧在每台 master 上把 admin.conf 复制到/root/.kube/config这样任何一台 master 上执行kubectl都不依赖跳板机多控制面排查起来顺手很多。最后说点个人体会。多 master 高可用部署真正难的不是 init 那个动作而是后面的维护意识和故障演练习惯。有一回我在一个环境里把三台 master 搭完组网、CNI、业务都跑顺了结果上线第二天发现 coredns 在 VIP 切换时偶尔超时。排查半天才发现是那台节点的 haproxy 健康检查间隔太长apiserver 在升级重启瞬间刚好处于 fall 阈值边缘流量打到了正在恢复中的节点。后来我把健康检查间隔缩短到 3 秒、keepalived 探活脚本间隔 2 秒再配上告警这个问题才算闭环。所以我建议所有看完这篇的人部署完一定要亲手做一次故障演练把某台 master 的 kubelet 停掉盯着日志看 VIP 怎么切、apiserver 请求什么时候恢复。只有亲手演练过、亲眼见过切换过程才敢拍着胸脯跟业务说这套集群足够高可用。对 1.2x 的多个小版本这套部署方式通用度很高后续如果升级到更高的 K8s 版本核心的设计思路和排障路径也不会变。