1. 高可用负载均衡集群的架构设计与选型思考
1.1 为什么单台负载均衡器会成为整个系统的命门
很多团队在搭建服务集群时,第一反应是买一台性能不错的服务器装上负载均衡软件,把流量分发到后端几台应用服务器上,觉得这就万事大吉了。我早期也这么干过,结果有一次那台负载均衡器所在机房的一台交换机出了故障,整个业务停了将近四十分钟,后端十几台应用服务器全都好好的,但用户就是访问不了。这件事让我彻底明白了一个道理:负载均衡层本身如果只有一台,那它就从“提升可用性的组件”变成了“整个系统最大的单点故障”。
负载均衡器的核心职责是把用户请求按照某种策略分发到后端多台服务器上,同时屏蔽后端服务器的故障。但问题在于,如果分发请求的这个“调度员”自己倒下了,那后面所有服务器再健康也没有意义。这就好比一个车站只有一个检票口,检票员请假了,后面开了再多通道也没用。所以高可用负载均衡集群要解决的核心问题就是:让调度层本身也具备冗余能力,任何一台调度器出问题,另一台能在极短时间内接管,用户几乎无感知。
1.2 HAProxy 与 Keepalived 的分工与配合逻辑
在这个组合里,HAProxy 负责的是“活”——也就是实际的流量分发工作。它是一款高性能的 TCP/HTTP 负载均衡器,支持四层和七层代理,能根据后端服务器的健康检查结果动态调整转发策略。HAProxy 本身非常稳定,单进程就能轻松处理数万并发连接,但它有一个前提:这台机器得活着,而且网络得通。
Keepalived 负责的是“命”——也就是可用性保障。它最初是为 LVS 负载均衡设计的辅助工具,核心功能有两个:一是通过 VRRP 协议实现 IP 地址的漂移,二是对后端服务做健康检查。VRRP 协议允许多台机器组成一个虚拟路由器组,共享一个虚拟 IP(VIP),组内通过优先级选举出一台主节点持有 VIP,其余为备节点。主节点定时发送心跳通告,备节点监听;一旦备节点在设定时间内收不到心跳,就认为主节点故障,触发新一轮选举,由优先级最高的备节点接管 VIP。
把这两个工具组合起来,架构就清晰了:两台(或多台)服务器上都运行 HAProxy 和 Keepalived,同一时刻只有主节点持有 VIP 并对外提供服务,HAProxy 把流量转发到后端真实服务器。主节点故障时,VIP 漂移到备节点,备节点上的 HAProxy 接管流量分发。整个过程对用户来说,只是 VIP 换了一台物理机承载,IP 地址没变,连接重建后就能继续访问。
1.3 主备模式与主主模式的取舍分析
这套架构有两种常见变体:主备模式和主主模式。主备模式下,备节点平时不处理流量,只待命,资源利用率低但逻辑简单、故障切换明确。主主模式下,两台机器同时持有 VIP 并各自处理一部分流量,资源利用率高,但需要 DNS 轮询或上层调度配合,且故障时流量会全部压到一台机器上,对单机性能要求更高。
我个人的选择倾向是:如果后端服务对延迟敏感、且预算允许一定冗余,主备模式更稳妥。因为主备切换时,备节点上的 HAProxy 连接数是零,接管后从零开始建立连接,不会出现瞬间过载。而主主模式下,一台挂了之后,另一台要瞬间承接双倍流量,如果没做好容量规划,很可能引发雪崩。当然,如果业务本身流量不大,主主模式能充分利用硬件资源,也是合理选择。下面的实操以主备模式为主线展开,主主模式的差异点我会在对应位置单独说明。
2. 环境准备与核心组件安装配置
2.1 服务器规划与网络参数设定
假设我们有两台调度服务器,分别命名为lb-master和lb-backup,后端有三台真实应用服务器app-01、app-02、app-03。网络规划如下:
| 角色 | 主机名 | 物理 IP | 虚拟 IP(VIP) |
|---|---|---|---|
| 主调度器 | lb-master | 192.168.10.11 | 192.168.10.100 |
| 备调度器 | lb-backup | 192.168.10.12 | 192.168.10.100 |
| 应用服务器1 | app-01 | 192.168.10.21 | 无 |
| 应用服务器2 | app-02 | 192.168.10.22 | 无 |
| 应用服务器3 | app-03 | 192.168.10.23 | 无 |
VIP 的选择要注意:必须和物理 IP 在同一网段,且未被其他设备占用。我习惯在规划阶段先用arping命令探测一下目标 VIP 是否已被占用,避免后续冲突。命令是arping -I eth0 192.168.10.100,如果有回应就说明该 IP 已被使用,需要换一个。
注意:所有服务器的防火墙规则需要放行 VRRP 协议(协议号 112)和 HAProxy 监听的端口。如果用的是 iptables,需要添加
-A INPUT -p vrrp -j ACCEPT;如果用 firewalld,则需要firewall-cmd --add-protocol=vrrp --permanent后重载。
2.2 HAProxy 安装与基础配置模板
在 CentOS 或 RHEL 系上,HAProxy 可以通过包管理器直接安装:yum install -y haproxy。Ubuntu/Debian 系则是apt install -y haproxy。安装完成后,主配置文件位于/etc/haproxy/haproxy.cfg。
下面是一个经过生产验证的配置模板,我把它拆成几个逻辑块来说明:
global log 127.0.0.1 local2 chroot /var/lib/haproxy pidfile /var/run/haproxy.pid maxconn 40000 user haproxy group haproxy daemon stats socket /var/lib/haproxy/stats defaults mode http log global option httplog option dontlognull option http-server-close option forwardfor except 127.0.0.0/8 option redispatch retries 3 timeout http-request 10s timeout queue 1m timeout connect 10s timeout client 1m timeout server 1m timeout http-keep-alive 10s timeout check 10s maxconn 30000 frontend main_frontend bind *:80 default_backend app_servers backend app_servers balance roundrobin option httpchk GET /health server app-01 192.168.10.21:8080 check inter 2000 rise 2 fall 3 server app-02 192.168.10.22:8080 check inter 2000 rise 2 fall 3 server app-03 192.168.10.23:8080 check inter 2000 rise 2 fall 3global段里maxconn 40000是全局最大连接数,这个值要根据服务器内存来算。HAProxy 每个连接大约消耗 20KB 左右内存,40000 连接大约需要 800MB 内存用于连接跟踪,加上其他开销,建议机器至少有 2GB 空闲内存再设这个值。stats socket是开启管理套接字,后续可以用echo "show stat" | socat stdio /var/lib/haproxy/stats来查看实时状态。
defaults段里的option redispatch很关键:当某台后端服务器挂了,HAProxy 会把原本发往它的请求重新分发到其他健康节点。retries 3配合这个选项,能有效减少后端故障对用户的影响。超时参数我一般设得比较保守,timeout connect 10s意味着如果后端服务器 10 秒内没响应 TCP 握手,就认为它不可用。
backend段里的健康检查配置check inter 2000 rise 2 fall 3需要解释一下:inter 2000是每 2 秒检查一次,rise 2是连续 2 次成功才标记为健康,fall 3是连续 3 次失败才标记为故障。这个组合意味着最坏情况下,一台后端服务器从故障到被摘除需要 6 秒左右。如果业务对故障感知要求更高,可以把inter调小到 1000,fall调到 2,但会增加健康检查的频次和资源消耗。
2.3 Keepalived 安装与 VRRP 实例配置
Keepalived 同样通过包管理器安装:yum install -y keepalived。主配置文件在/etc/keepalived/keepalived.conf。主备模式的配置差异主要在state、priority和nopreempt这几个参数上。
主节点lb-master的配置:
global_defs { router_id lb-master enable_script_security script_user root } vrrp_script chk_haproxy { script "/etc/keepalived/check_haproxy.sh" interval 2 weight -20 fall 2 rise 2 } vrrp_instance VI_1 { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass Haproxy@2024 } virtual_ipaddress { 192.168.10.100/24 dev eth0 label eth0:1 } track_script { chk_haproxy } }备节点lb-backup的配置只有三处不同:router_id改为lb-backup,state改为BACKUP,priority改为 90。其余保持一致。
virtual_router_id 51是 VRRP 组的标识,同一组内所有节点必须一致,不同组之间不能冲突。advert_int 1表示主节点每秒发送一次心跳通告。authentication段是简单的密码认证,防止其他机器误加入组内。
track_script引用了chk_haproxy脚本,这个脚本的作用是检测本机的 HAProxy 进程是否存活。如果 HAProxy 挂了,脚本返回非零值,Keepalived 会根据weight -20把本机优先级降低 20。主节点优先级从 100 降到 80,低于备节点的 90,就会触发切换。这个机制比单纯检测网络心跳更可靠,因为有时候机器活着但 HAProxy 进程已经崩溃了,VIP 漂移过去也没用。
健康检查脚本/etc/keepalived/check_haproxy.sh的内容:
#!/bin/bash if killall -0 haproxy 2>/dev/null; then exit 0 else exit 1 fikillall -0只发送信号 0,不实际杀死进程,仅检测进程是否存在。这个脚本要赋予执行权限:chmod +x /etc/keepalived/check_haproxy.sh。
注意:
script_user root和enable_script_security是 Keepalived 2.0 之后版本的安全要求,如果不加这两行,脚本可能因为权限问题无法执行。我在这上面踩过坑,脚本手动执行没问题,但 Keepalived 调用就是不生效,排查了半天才发现是权限限制。
3. 集群搭建实操与核心环节验证
3.1 启动顺序与初始状态检查
配置写完后,启动顺序有讲究。先启动备节点的 Keepalived,再启动主节点的 Keepalived。这样做的原因是:如果先启动主节点,它会立即抢占 VIP 并开始发送心跳;备节点后启动时,会收到主节点的心跳,自动进入 BACKUP 状态。反过来如果先启动备节点,它会短暂认为自己是主节点并持有 VIP,等主节点启动后再切换,中间会有一次不必要的漂移。
启动命令:systemctl start keepalived,HAProxy 则通过systemctl start haproxy启动。两个服务都设置开机自启:systemctl enable keepalived haproxy。
启动完成后,在主节点上执行ip addr show eth0,应该能看到eth0:1这个子接口上绑定了192.168.10.100。在备节点上执行同样的命令,则看不到这个 VIP。这是验证主备状态最直接的方法。
再用systemctl status keepalived查看服务状态,主节点会显示VRRP_Instance(VI_1) Transition to MASTER STATE,备节点显示VRRP_Instance(VI_1) Entering BACKUP STATE。如果状态不对,优先检查virtual_router_id是否一致、auth_pass是否相同、防火墙是否放行了 VRRP 协议。
3.2 模拟故障切换与切换时间测量
验证高可用是否真正生效,必须做故障切换测试。我常用的方法是:在主节点上直接systemctl stop keepalived,然后立刻在备节点上循环执行ip addr show eth0 | grep 192.168.10.100,观察 VIP 何时出现。
实测下来,从主节点 Keepalived 停止到备节点接管 VIP,时间大约在 1 到 3 秒之间。这个时间主要由advert_int和fall决定:主节点停止后,备节点最多等advert_int × fall秒(这里是 1×2=2 秒)就判定主节点故障,然后立即接管。如果对切换时间有更高要求,可以把advert_int调到 0.5 秒,但会增加网络心跳包的数量。
切换完成后,在备节点上执行ip addr show eth0确认 VIP 已经漂移过来,同时systemctl status keepalived会显示Transition to MASTER STATE。此时从客户端访问http://192.168.10.100,应该能正常得到后端应用的响应。
再把原主节点的 Keepalived 启动起来,由于它的优先级是 100,高于备节点的 90,它会重新抢占 VIP。这个过程叫“抢占模式”。如果不希望它抢回来(比如主节点刚恢复还不稳定),可以在备节点配置里加nopreempt,但注意nopreempt只在state BACKUP下生效,且需要所有节点都配置才有效果。
3.3 后端服务器健康检查与流量摘除验证
HAProxy 对后端服务器的健康检查同样需要验证。我通常在后端某台服务器上临时停掉应用进程,然后观察 HAProxy 的统计页面或日志。
开启统计页面的方法是在 HAProxy 配置里加一段:
listen stats bind *:9000 mode http stats enable stats uri /haproxy-status stats auth admin:Admin@2024 stats refresh 5s重启 HAProxy 后,访问http://192.168.10.100:9000/haproxy-status,输入用户名密码,就能看到所有后端服务器的状态。健康节点显示绿色,故障节点显示红色。停掉 app-01 上的应用后,大约 6 秒内(inter 2000 × fall 3),该节点会变成红色,状态显示DOWN。此时再访问业务地址,请求不会再被转发到 app-01。
这个验证很重要,因为有些团队只配了 Keepalived 的高可用,却忽略了 HAProxy 对后端的健康检查,结果后端某台机器挂了,HAProxy 还在往它转发请求,用户看到的是间歇性的 502 错误。两者配合才能实现完整的故障隔离。
4. 常见问题排查与生产环境避坑经验
4.1 脑裂问题的成因与预防措施
脑裂是高可用集群最危险的问题:主备节点都认为自己是主节点,同时持有 VIP,导致 IP 冲突和流量混乱。成因通常是心跳网络中断,备节点收不到主节点的心跳,误判主节点故障而接管 VIP;但主节点其实还活着,也在使用 VIP。
预防脑裂有几个层面。第一,心跳网络要独立。如果条件允许,用单独的网卡和交换机专门跑 VRRP 心跳,不要和业务流量混在一起。第二,配置unicast_peer指定单播心跳目标,避免组播被网络设备过滤。第三,在 Keepalived 里加notify脚本,在状态切换时发送告警,让运维人员第一时间知道发生了切换。
如果已经发生脑裂,最快的恢复方式是:先停掉其中一台的 Keepalived,确认 VIP 只在一台机器上,再排查心跳网络问题。千万不要两台都开着 Keepalived 去调试,那样只会让情况更糟。
4.2 VIP 漂移后连接不恢复的排查思路
有时候 VIP 漂移成功了,ip addr也能看到 VIP 在备节点上,但客户端访问就是不通。这个问题我遇到过几次,排查下来通常是以下几个原因:
第一,ARP 缓存问题。VIP 漂移后,同一网段内的其他设备(如网关、客户端)的 ARP 缓存里还记录着旧主节点的 MAC 地址,导致数据包发到了错误的机器上。解决方法是让 Keepalived 在切换时发送免费 ARP(gratuitous ARP)。Keepalived 默认会发送,但如果被防火墙拦截了就不行。检查方法是tcpdump -i eth0 arp看切换时有没有 ARP 广播发出。
第二,HAProxy 没有监听 VIP。HAProxy 的bind如果是*:80,会监听所有地址包括 VIP;但如果写的是具体的物理 IP,VIP 漂移过来后 HAProxy 并不会监听它。所以bind一定要用*或0.0.0.0。
第三,后端服务器没有配置 VIP 的回程路由。如果后端服务器和调度器不在同一网段,后端返回响应时可能不走 VIP 所在的网关,导致响应包丢失。这种情况下需要在后端服务器上添加一条路由,把 VIP 网段指向调度器的物理 IP。
4.3 生产环境参数调优与监控建议
生产环境的参数调优,我总结了几条经验。HAProxy 的maxconn不要设得太大,超过系统文件描述符限制会报错。需要同步调整ulimit -n和fs.file-max。timeout client和timeout server要根据业务特点来定:如果是长连接业务(如 WebSocket),这两个值要设得很大甚至设为0(不超时);如果是短请求业务,设 30 秒到 1 分钟比较合理。
Keepalived 的advert_int不建议低于 0.5 秒,否则心跳包太频繁,在网络抖动时容易误判。priority的差值建议至少 10,太小了容易因为weight调整而意外切换。
监控方面,至少要采集这几个指标:VIP 当前在哪个节点、HAProxy 的活跃连接数、后端服务器的健康状态、Keepalived 的状态切换次数。我习惯用socat定期抓取 HAProxy 的统计信息,配合简单的脚本写入时序数据库,再配置告警规则。Keepalived 的状态切换则通过notify脚本发送到告警通道,每次切换都记录一条日志,方便事后复盘。
提示:
notify脚本的写法是在vrrp_instance段里加notify_master /path/to/script.sh、notify_backup /path/to/script.sh、notify_fault /path/to/script.sh。脚本里可以通过$1、$2、$3获取实例名、状态和优先级,方便做差异化处理。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 备节点看不到 VIP | 主节点正常持有,备节点处于 BACKUP | ip addr检查主节点 | 正常现象,无需处理 |
| 主备都持有 VIP | 脑裂 | 两台都执行ip addr | 停掉一台 Keepalived,检查心跳网络 |
| VIP 漂移后访问不通 | ARP 缓存未更新 | tcpdump -i eth0 arp | 确认免费 ARP 已发送,检查防火墙 |
| HAProxy 不监听 VIP | bind绑定了具体 IP | 查看haproxy.cfg的bind行 | 改为bind *:80 |
| 后端故障未摘除 | 健康检查未配置或参数不当 | 查看 HAProxy 统计页面 | 添加check参数,调整inter/fall |
| Keepalived 脚本不执行 | 权限或安全限制 | 查看/var/log/messages | 加script_user root和enable_script_security |
| 切换时间过长 | advert_int和fall过大 | 计算advert_int × fall | 适当调小,但注意网络抖动误判 |
这套组合我用了好几年,从最初的测试环境到后来的生产环境,踩过的坑基本都在这了。最深的体会是:高可用不是配完就完事,必须定期做故障演练。我现在的习惯是每个月至少做一次切换测试,确保备节点上的 HAProxy 配置和后端列表跟主节点保持同步。因为很多时候主节点改了配置,备节点忘了改,真到切换的时候才发现备节点上的配置是旧的,那高可用就成了摆设。