HAProxy+Keepalived构建Kubernetes高可用APIServer入口
去年有一天半夜运维群突然弹出一条消息kubectl get nodes 卡住了所有命令都超时。第一反应是 apiserver 挂了结果 ssh 上去一看进程明明活着是那台 master 节点整个失去了网络。集群明明有 3 个 master可大家手里的 kubeconfig 写死的是其中一台的 IP其他组件也一样谁也不认识剩下两台。那一晚我们最后是靠批量修改所有人的 kubeconfig 才恢复的。从那以后我再没让自建集群“裸奔”过——要么买云负载均衡器要么自己搭一套 haproxy keepalived 作为入口。这套组合解决的问题非常具体给 Kubernetes 控制面的 APIServer 提供一个统一的浮动入口。haproxy 负责把 VIP:6443 的流量分发到多台 master 的 apiserverkeepalived 负责让 VIP 在负载均衡节点之间漂移。适合不想依赖云 LB、想彻底掌控流量入口的团队也适合在物理机、VM、混合网络里自建 k8s 集群的人。本文我会从原理到配置、再到故障切换实测把整个搭建过程完整过一遍连试错经验也一并写出来。1. 高可用集群的真正单点APIServer 入口1.1 APIServer 是控制面的“总机”它挂了谁也进不来很多人一说 k8s 高可用第一反应是 etcd 做三副本、master 节点多部署几台。这个方向没错但漏了最要命的一层APIServer 是整个集群唯一的入口。kubectl、kubelet、scheduler、controller-manager、各类 Operator 全部要连接它。你可以把 APIServer 想象成公司的总机etcd 是档案室总机挂了所有人连门都进不去档案室再稳也没用。更麻烦的是即使你有三台 master如果客户端 kubeconfig 里写死的是其中一台的 IP那这台物理机宕机后所有依赖 kubectl 的自动化脚本、日志采集、CI/CD 都会瞬间瘫痪。kubelet 的心跳上报也会受影响虽然 work 节点上的 Pod 还能跑但整个集群已经处于“只读死锁”状态——调度、扩缩容、改配置全部失效。所以高可用要解决的核心问题不是“数据不丢”而是“入口永远可达”。这就需要一个位于 APIServer 前面的流量入口层它必须满足两个条件第一多个后端之间自动负载均衡某个 apiserver 出问题就摘掉它第二这个入口本身不能是单点必须有一个虚拟 IP 在多个节点之间漂移。1.2 为什么偏偏是 haproxy keepalived 组合市面上的方案不少云厂商的 SLB、自建 nginx、MetalLB、甚至直接在 kube-proxy 层做文章。但中小规模自建集群里haproxy keepalived 的组合依然是最稳、最容易维护的。云 SLB 的问题在于依赖环境你如果是在物理机房、VMware 虚拟化环境或者某些混合网络里根本没有可用的云 LB。自建 nginx 当然也能做四层转发但我实际对比过nginx 的 stream 模块在处理高并发长连接时表现很好可配置复杂度和排错成本比 haproxy 要高。haproxy 本身就是为 TCP/HTTP 负载均衡设计的配置直观、健康检查灵活、状态页面方便作为 k8s 入口非常合适。keepalived 在这个组合里只干一件事通过 VRRP 协议维护一个 VIP并决定这个 VIP 当前该落在哪台机器上。它不负责转发流量只负责“IP 漂移”。很多人问为什么不直接把 VIP 绑在 apiserver 上答案很简单——多个 master 同时绑定同一个 VIP 会引发地址冲突而用 VRRP 选主则可以避免“多人抢同一总机”的问题本质上是一种分布式仲裁。1.3 一条完整流量路径整个架构的流量路径非常清晰客户端请求 → VIP:6443 → 当前持有 VIP 的节点上的 haproxy → 任一台 master 的 apiserver:6443每个 master 节点的 kube-apiserver 监听在 0.0.0.0:6443haproxy 负载均衡层则部署在一组专门的 LB 节点上。生产环境建议 LB 节点独立部署最少两台小规模或测试环境可以跟 master 复用节点但要注意资源竞争。keepalived 会确保同一时刻只有一台 LB 节点持有 VIP这台节点的 haproxy 一旦异常VIP 自动漂移到其他 LB 节点客户端无感知。2. 节点规划与网络前提几台机器、哪些端口要通2.1 最小可行拓扑与角色分布这套架构最少需要两台 LB 节点才能谈得上“高可用”但为了避免 split-brain两个节点都认为自己是主的极端情况我建议至少用三台。master 节点同样建议三台与 etcd 副本数对齐。一个典型的中小规模生产集群拓扑如下角色数量建议配置说明master34C8G运行 kube-apiserver、kube-scheduler、kube-controller-manager、etcdLB32C4G运行 haproxy keepalived可复用 master 但对生产不推荐workerN按业务运行实际工作负载LB 节点和 master 节点理论上可以在同一批机器上混布小规模集群这样能省机器。但生产环境我强烈建议独立出来原因很简单haproxy 本身要消耗少量 CPU 处理长连接keepalived 的 VRRP 心跳也要吃一点网络如果 master 负载高时把 LB 也拖死反而失去了高可用的意义。2.2 网络要求与端口清单规划网络时先把 IP 想清楚避免后期返工。核心要求就一句话所有客户端、worker、LB、master 之间必须三层互通。VLAN 或防火墙隔离时至少保证以下端口可达6443kube-apiserver所有组件都要访问2379/2380etcd 客户端与集群通信仅在 master 之间10250kubelet 指标与 exec 端口master 要能访问 worker179Calico BGP 端口如果用 BGP 模式8472/4789VXLAN 覆盖网络端口IP 协议 112VRRPkeepalived 用这就是很多集群建到一半才发现网络不通的坑尤其 VRRP 的协议号 112防火墙默认不放行。如果用了 keepalived 的单播模式实际上还是要放行协议 112 的单播报文后面我会细说。2.3 系统准备与版本建议操作系统建议用 CentOS 7.9、Rocky 8/9 或 Ubuntu 22.04 LTS内核版本不要太旧。haproxy 用 2.x 以上别再用 1.5 时代的版本了2.x 的性能和对 HTTP 健康检查的支持都好很多。keepalived 用 1.3.x 或 2.x 都行功能差异不大但 2.x 的配置语法更严谨。系统层面只需要两件必做的事第一所有节点时间同步这是个常被忽视的细节时间不同步会导致 VRRP 优先级比较出错还会影响 k8s 证书校验第二关闭防火墙或者精确放行上面的端口和协议。如果不想关闭 firewalld可以这样放行 VRRPfirewall-cmd --permanent --add-rich-rulerule protocol value112 accept firewall-cmd --reload另外建议把 keepalived 和 haproxy 都设置开机自启systemctl enable --now haproxy systemctl enable --now keepalived我见过不少搭建好的集群一旦机房断电重启LB 节点上的组件没有自启VIP 根本起不来整个集群直接失联。自启这种事越早配置越好。3. HAProxy 配置细节转发、健康检查与配置生效3.1 安装与核心配置段在三个 LB 节点上分别安装yum install -y haproxy然后修改 /etc/haproxy/haproxy.cfg。完整配置如下这是经过生产验证的版本global log /dev/log local0 log /dev/log local1 notice chroot /var/lib/haproxy stats socket /var/lib/haproxy/stats mode 600 level admin stats timeout 30s user haproxy group haproxy daemon maxconn 4096 defaults log global mode tcp option dontlognull timeout connect 5s timeout client 50s timeout server 50s frontend k8s-api bind *:6443 mode tcp default_backend k8s-api-backend backend k8s-api-backend mode tcp balance roundrobin option tcp-check server master01 192.168.1.11:6443 check inter 3s fall 3 rise 2 server master02 192.168.1.12:6443 check inter 3s fall 3 rise 2 server master03 192.168.1.13:6443 check inter 3s fall 3 rise 2这里最关键的几个点mode tcp 表示四层转发不做 HTTP 解析对 k8s APIServer 来说没必要做七层性能更好。balance roundrobin 轮流把连接分发到三个 apiserver简单公平。server 行最后的 check inter 3s fall 3 rise 2 表示每 3 秒探测一次连续失败 3 次标记为 down连续成功 2 次标记为 up。option tcp-check 表示用 TCP 探测这也是下面要说的一个重要选型点。3.2 健康检查别抄错 option httpchk很多网上的教程会在 backend 里写option httpchk GET /healthz然后信心满满地说“这样 apiserver 不健康就会自动摘除”。但实际跑起来你会发现健康检查永远失败后端全部被标记为 down。原因很简单kube-apiserver 的 6443 端口是 TLS 加密端口而 haproxy 的 httpchk 默认发送的是明文 HTTP 请求。APIServer 收到的是一堆无法完成 TLS 握手的垃圾字节直接扔掉了。这个问题的正确解法有两种。如果只关心进程活着、端口能连上那 TCP 检查就已经足够了也就是我上面配置里写的 option tcp-check。APIServer 如果出现“进程活着但端口不通”的场景逻辑上已经算故障了TCP 检查完全可以兜住。如果你确实希望做到 HTTP 级别的健康检查验证 /healthz 返回 200那就要借助外部脚本backend k8s-api-backend mode tcp balance roundrobin option external-check external-check command /etc/haproxy/check_apiserver.sh server master01 192.168.1.11:6443 check inter 3s fall 3 rise 2 ...脚本 check_apiserver.sh 内容如下#!/bin/bash VIP192.168.1.100 PORT6443 timeout 2 bash -c echo -n | openssl s_client -connect ${VIP}:${PORT} -servername ${VIP} 2/dev/null | grep -q BEGIN CERTIFICATE exit $?这个脚本比明文 httpchk 可靠得多它完成了一次真实的 TLS 握手确认 apiserver 真的能提供 HTTPS 服务而不是仅仅“端口通”。但注意它会消耗一点额外的握手开销而且要保证 VIP 当前在这台机器上或者直接探测各 master 的 IP。大多数场景我更推荐前面的 TCP 检查版本简单、可靠、无依赖。3.3 配置生效与本地验证改完配置后先校验语法haproxy -c -f /etc/haproxy/haproxy.cfg校验通过后 reloadsystemctl reload haproxy注意不要用 systemctl restart haproxyreload 是平滑重载不会断开现有 TCP 连接restart 会把所有长连接全部断开。kubectl 客户端到 apiserver 的长连接很敏感restart 会导致一波连接中断报错。然后确认监听状态ss -lntp | grep 6443每个 LB 节点上都能看到 haproxy 监听在 6443 端口并且此时如果 VIP 恰好在这台机器上外部访问 VIP:6443 也就通了。先别急着继续可以小测一下curl -k https://192.168.1.100:6443/healthz正常会返回 ok。如果你看到的是一堆证书报错说明 curl 没带 -k或者网络根本不通。4. Keepalived 配置细节VRRP 漂移与故障转移脚本4.1 VRRP 到底在做什么Keepalived 的核心是 VRRP虚拟路由冗余协议。简单说一组机器维护同一个虚拟 IP其中一台是 MASTER其余是 BACKUP。MASTER 周期性地发送广播/单播通告告诉其他成员“我活着”。如果 BACKUP 在指定时间内收不到 MASTER 的通告就认为 MASTER 挂了其中优先级最高的 BACKUP 自动接管 VIP。这个过程非常像开会的“主持人失联副手自动接替”。关键参数有两个priority优先级数字越大越可能成为 MASTER和 advert_int通告间隔单位秒。MASTER 挂了之后BACKUP 通常需要 3 个间隔周期才能确认并接管所以生产环境会把间隔调小一点我这里用的是 1 秒。4.2 完整配置示例MASTER 与 BACKUP 的区别在第一个 LB 节点上/etc/keepalived/keepalived.conf 内容如下global_defs { router_id lb01 script_user root enable_script_security } vrrp_script check_haproxy { script /etc/keepalived/check_haproxy.sh interval 2 weight -20 fall 2 rise 2 } vrrp_instance VI_1 { state MASTER interface ens192 virtual_router_id 51 priority 120 advert_int 1 authentication { auth_type PASS auth_pass k8s_lb_51 } virtual_ipaddress { 192.168.1.100/24 dev ens192 label ens192:0 } track_script { check_haproxy } unicast_peer { 192.168.1.22 192.168.1.23 } }第二个节点把 state 改成 BACKUPpriority 改成 110router_id 改成 lb02unicast_peer 改成另外两台的 IP。第三个节点同样处理。role 是 BACKUP 的节点同样可以接管 VIP只是优先级低而已集群不要求所有节点都是 MASTER。virtual_router_id 51 是 VRRP 的虚拟路由标识同一组节点必须一致用于区分多组 VRRP。authentication 里的密码只是一个简单的明文校验别当安全机制用vrrp 报文本身没有加密。unicast_peer 是我强烈建议加的。VRRP 默认走组播但在很多云环境、VLAN 隔离网络里组播会被限制甚至丢弃导致 keepalived 完全发现不了对方VIP 双主一起出现。用单播模式可以绕开大部分这类问题但同样要记得放行协议号 112。4.3 健康检查脚本检查 haproxy还是检查 APIServer这里有个值得深入思考的点。keepalived 的 vrrp_script 到底该检查什么很多人只检查 haproxy 进程比如#!/bin/bash if pidof haproxy /dev/null; then exit 0 else exit 1 fi这能处理“haproxy 崩了”的场景但处理不了另一种很尴尬的情况haproxy 还活着后面所有 apiserver 全部挂了。这时 VIP 依然在这一台 LB 节点上外面连 VIP 的请求到达 haproxy 后全部转发失败keepalived 却认为一切正常VIP 不会漂移。所以我的建议是同时检查两侧haproxy 进程状态以及至少一个 apiserver 的健康状态。一个生产实用的脚本可以长这样#!/bin/bash # 检查 haproxy 进程是否存在 if ! pidof haproxy /dev/null; then echo haproxy is down /var/log/keepalived-check.log exit 1 fi # 再探测后端的 apiserver能连上任意一个就算健康 for master in 192.168.1.11 192.168.1.12 192.168.1.13; do if timeout 2 bash -c echo -n | openssl s_client -connect ${master}:6443 2/dev/null | grep -q BEGIN CERTIFICATE; then exit 0 fi done echo all apiservers are down /var/log/keepalived-check.log exit 1这个脚本的逻辑是haproxy 必须活着三个 apiserver 至少有一个能完成 TLS 握手才算健康。任何一个条件不满足keepalived 就会给当前节点扣 weight优先级掉下去VIP 漂移到备用节点。注意脚本必须给执行权限否则 keepalived 调不起来chmod x /etc/keepalived/check_haproxy.shweight -20 的含义要理解清楚脚本检查成功节点优先级不变脚本失败当前节点优先级减 20。当它的优先级低于另一个 BACKUP 时备节点就会接管。这是“软性降级”不是直接剥夺 VIP触发条件是反复失败 fall 2 次防止一次网络抖动就导致频繁漂移。5. 对接 K8scontrolPlaneEndpoint、证书与故障切换实测5.1 新建集群时把 controlPlaneEndpoint 指向 VIP如果你是从零搭建集群在 kubeadm 的配置文件里直接指定控制面端点为 VIPapiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 controlPlaneEndpoint: 192.168.1.100:6443 apiServer: certSANs: - 192.168.1.100 - 127.0.0.1 - 192.168.1.11 - 192.168.1.12 - 192.168.1.13 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12controlPlaneEndpoint 是这套架构和 K8s 对接的关键。配置了这个字段后kubeadm 生成的 kubeconfig、kubelet 的引导配置都会指向 VIP而不是某个具体 master 的 IP。以后无论 VIP 漂移到哪个 LB 节点集群组件都能找到控制面。certSANs 里必须包含 VIP因为 apiserver 的证书需要允许客户端通过 VIP 访问。如果不加kubectl 通过 VIP 连接时会报证书校验失败“x509: cannot validate certificate for 192.168.1.100 because it doesnt contain any IP SANs”。已有集群想改成这套方案也不是不行但需要所有节点的 /etc/kubernetes/*.conf 全换成 VIP 地址apiserver 的证书要重新签并滚重启过程比较折腾。如果还在规划阶段强烈建议一开始就写成 VIP。5.2 内部组件如何获取正确的 kubeconfig很多人会问scheduler 和 controller-manager 还需要单独连 VIP 吗不需要。kubeadm 生成的 /etc/kubernetes/scheduler.conf、/etc/kubernetes/controller-manager.conf 默认都指向 controlPlaneEndpoint 即 VIP。kubelet 的心跳上报也是通过 apiserver 地址同样指向 VIP。这里会出现一个小问题所有内部组件都在集群网络内它们访问 VIP:6443 时这个 VIP 当前在哪台 LB 上请求就会先到那台的 haproxy。如果那台 LB 正好出故障VIP 漂移到了另一台组件仍能继续工作这正是我们想要的效果。需要打交道的只有两个文件管理员的 kubeconfig 和 kubelet 的 bootstrap 配置。用 kubeadm 初始化的集群默认生成的 admin.conf 里 server 就是 controlPlaneEndpoint直接复制到 ~/.kube/config 使用即可。5.3 故障切换实测过程搭建完成后必须做一轮故障注入验证口头上的“高可用”不算数。我的标准验证流程如下第一步确认基线状态。所有机器上执行ip addr show | grep 192.168.1.100正常情况下只有某一台 LB 节点能看到 VIP。再执行 kubectl get nodes 确认集群正常。第二步模拟 haproxy 故障。在持有 VIP 的 LB 节点上执行systemctl stop haproxy等大约 5 秒后再在其他 LB 节点上看 VIP应该已经漂移过来。然后立刻执行 kubectl get nodes整个过程不应该有任何报错只是底层连接重连了一下。第三步模拟 apiserver 节点宕机。在某一台 master 上执行systemctl stop kube-apiserverhaproxy 的健康检查会在 9 秒内3 秒一次连续失败 3 次把这个后端标记为 down。此时 kubectl get nodes 依然正常因为另外两台 apiserver 在正常工作。把 apiserver 重新拉起后haproxy 又会把它加回后端池这个过程不需要人为干预。第四步恢复故障。把 haproxy 从停用状态恢复systemctl start haproxy观察 keepalived 日志确认 VIP 是否回到原节点还是继续留在备用节点。这取决于配置了 non-preempt 还是默认的抢占模式默认情况下原 MASTER 恢复后优先级最高会抢回 VIP。这个行为没有对错取决于你的业务更喜欢稳定还是更喜欢就近生产环境建议先想清楚。6. 实战踩坑记录从脚本退出码到健康检查的 TLS 陷阱6.1 keepalived 脚本退出的“隐形 0”keepalived 的 vrrp_script 判断成功与否看的是脚本进程的退出码。但很多人会写出这样的脚本#!/bin/bash pidof haproxy实际上 pidof 找不到进程时会返回非 0 退出码。这本身没问题可一旦脚本最后一行是某个永远成功的命令比如 echo那退出码就被覆盖成 0 了keepalived 永远认为 haproxy 活着VIP 永远不漂移。排查这类问题时先手动跑一遍脚本看退出码bash /etc/keepalived/check_haproxy.sh echo $?这里输出 0 和 1 是有讲究的0 代表健康非 0 代表不健康。我见过太多“脚本明明没报错但 VIP 不切”的故障最后都是退出码被最后一个命令污染了。6.2 组播失效导致的“双 VIP”分裂keepalived 默认使用组播方式宣告自身状态这在传统交换机环境通常没问题但一旦网络里禁用了组播或者 LB 节点跨了 VLAN/云网络组播包就传不过去了。此时所有 BACKUP 都认为 MASTER 死了会各自抢占 VIP出现“双主分裂”。排查方法很简单在节点上执行 tcpdump 看有没有 VRRP 报文tcpdump -i ens192 vrrp如果什么都抓不到基本可以断定组播被隔离了。解决方案就是我前面写过的 unicast_peer。单播模式下每个节点主动向其他节点的 IP 发送 VRRP 通告不依赖组播环境。改完后 tcpdump 应该能看到源地址为对端 IP、协议号为 112 的报文。6.3 haproxy reload 与 kubectl 长连接冲突第一次给 haproxy 做配置变更时我习惯性用了 systemctl restart haproxy结果立刻收到一堆报错kubectl 命令全部 connect: connection refused。因为 restart 会把所有 TCP 连接都断开kubectl 和 apiserver 之间建立的长连接被迫全部重建无可避免地出现几十秒的抖动。haproxy 本身支持优雅 reloadsystemctl reload 就是先起一个新进程加载新配置再把老进程的连接慢慢迁移过去。这条经验价值很大日常改 haproxy 配置一律 reload不要 restart。如果某次改了关键配置必须 restart也要选在业务低峰期并提前告知相关人员。6.4 证书 SAN 缺失与实际报错形态有一种特别容易误判的场景VIP 全部配置正确haproxy 也把流量转到了后端kubectl 却报error: x509: cannot validate certificate for 192.168.1.100 because it doesnt contain any IP SANs这不是网络问题也不是 haproxy 配置问题是 apiserver 的证书里没有包含 VIP 地址。Kubernetes 对证书 SAN 有严格校验双向都会验证。这里有一个冷门但真实的情况如果 kubeadm 初始化时 controlPlaneEndpoint 写了 VIP但 apiServer.certSANs 里漏了 VIP某些版本的 kubeadm 依然能成功初始化只是之后所有通过 VIP 访问的客户端都会证书校验失败。处理方法是修改 apiServer.certSANs 重新生成证书kubeadm init phase certs apiserver --configconfig.yaml然后逐个节点滚动重启 kube-apiserver。这个坑一旦踩到修复成本比预防高得多所以 config 文件里的 certSANs 一定要在初始化时认真核对。6.5 网络抖动与频繁切换给脚本加“防抖”健康检查脚本写得过于敏感也会出问题。比如只检查 apiserver 的 TCP 端口但 port 探测本身非常容易被网络抖动影响。一旦某个瞬间丢包脚本返回失败keepalived 扣 20 分VIP 切到另一台过几秒又切回来。VIP 在 LB 节点之间来回弹不仅影响正在进行的 kubectl 长连接还会影响集群内所有访问 apiserver 的组件。解决方法是利用 keepalived 的 fall/rise 参数给脚本加“防抖”。fall 2 表示连续失败 2 次才认为节点不健康rise 2 表示连续成功 2 次才认为恢复。同时脚本内部的 timeout 不要给太短2 到 3 秒比较合理。另外如果脚本里用 openssl 探测还可以把探测对象设为负载均衡算法的后端集合任意一个成功即为健康这样比单点探测稳定得多。一点点个人体会这套架构我在不下十个集群里落地过从几十节点的生产环境到几台机器的小型测试环境都有。最大的感受是高可用不是搭出来就完了真正考验人的是你有没有持续做故障切换演练。按我的经验每三个月停一次 haproxy 或拔一次 LB 节点网线让团队所有人都熟悉 VIP 漂移过程和检查命令比任何自动化工具都靠谱。最后一个建议是给 haproxy 的 stats socket 开着方便出问题时快速查看后端状态echo show stat | socat stdio /var/lib/haproxy/stats通过 stat 输出可以看到每个 apiserver 后端的当前状态、健康检查次数、会话数定位“为什么流量没过去”这类问题时效率高很多。这套组合不需要任何花哨的工具把每个环节理解透发生故障时就不会手忙脚乱。