单台 HAProxy 做负载均衡,等于把单点从应用层挪到了流量入口:它一停,整站立刻 502。本文用 Keepalived 的 VRRP 协议配合 HAProxy,在两台服务器上实现 VIP 双机热备——主节点故障后备节点在秒级接管同一个虚拟 IP,业务方只改一次 DNS 就能长期不用再动。内容包括完整可用配置、基于 stats socket 的健康检查脚本、真实故障演练,以及公有云上最容易踩的四个坑。
一、为什么”上了负载均衡”反而更脆
很多团队的做法是:两台应用服务器 → 一台 HAProxy → 对外提供服务。应用层做了冗余,但 HAProxy 只有一台,它本身就是新的单点。更麻烦的是这台机器的位置很尴尬——它在所有流量必经之路上,一旦宕机,用户看到的是浏览器直接超时,比应用层某个接口报错严重得多。
正确姿势是在 HAProxy 前面再加一层”入口冗余”。这一层不需要复杂的编排系统,两台普通云服务器 + 一个能在机器之间漂移的虚拟 IP(VIP)就够了。这就是经典的双机热备:同一时刻只有一台机器持有 VIP 并对外服务,另一台待命;持有者心跳消失后,VIP 连同它的 ARP 应答一起转移到备用机。
二、原理:VRRP 如何让 IP 在机器间”漂移”
Keepalived 实现 VIP 漂移靠的是 VRRP(Virtual Router Redundancy Protocol,虚拟路由冗余协议)。工作方式很朴素:一组机器构成一个虚拟路由器,共享一个 virtual_router_id,各自按 priority 排座次,优先级最高的成为 MASTER 并持有 VIP;其余为 BACKUP。
MASTER 每隔 advert_int 秒发一次 VRRP 通告(默认 1 秒,组播地址 224.0.0.18,IP 协议号 112)。BACKUP 只要在 3 个通告周期内没收到通告,就判定对端失联,自己接管 VIP,并立刻发送免费 ARP(gratuitous ARP)刷新交换机、路由器和同网段主机的 MAC 表。整个过程不需要任何中心化协调组件,也不依赖 DNS 生效时间。
理解了这一点,后面所有配置和排错就有了统一解释:VIP 没漂过去,无非是”通告没到”或”ARP 没刷”两类原因。网络层排查思路可参考站内的网络丢包排查实战。
三、架构选型:主备、双主还是云负载均衡
双机热备只是高可用的一种形态。落地前先确认它是不是你要的那一种,别为了”看起来专业”引入不必要的运维负担。
| 方案 | 故障切换 | 资源利用率 | 适用场景 | 主要短板 |
|---|---|---|---|---|
| Keepalived + HAProxy 主备 | 秒级(通常 1–6 秒) | 50%(备机空转) | 自建机房、少量云主机、需固定入口 IP | 备机闲置;云上需处理组播与 ARP 限制 |
| Keepalived 双主(两个 VIP 互为主备) | 秒级 | 接近 100% | 流量较大、希望两台都干活 | 配置更绕,故障时容量腰斩需预案 |
| 云厂商 SLB / CLB | 秒级,由云平台保证 | 按需弹性 | 已有云资源、追求少运维 | 按量计费;入口 IP 与策略受平台约束 |
| DNS 轮询 / 多 A 记录 | 分钟级(受 TTL 与本地缓存影响) | 100% | 跨地域容灾的补充手段 | 切换慢,无法做真正的健康检查 |
结论很直接:如果你的入口必须是一个固定公网 IP,又不打算买云负载均衡,主备模式是性价比最高的选择;流量大到一台 HAProxy 扛不住时再考虑双主。
四、环境准备
示例环境为两台同网段主机,系统 Ubuntu 22.04,Keepalived 2.2.x、HAProxy 2.4+:
- lb-node-01:192.168.1.11,priority 100(初始 MASTER)
- lb-node-02:192.168.1.12,priority 90(初始 BACKUP)
- VIP:192.168.1.100,必须与两台机器同网段且未被占用
- 后端应用:192.168.1.21:8080、192.168.1.22:8080
# 两台机器都执行
sudo apt update
sudo apt install -y keepalived haproxy socat curl
# 允许进程绑定非本机地址(VIP 漂移后 HAProxy 需要绑它)
echo 'net.ipv4.ip_nonlocal_bind = 1' | sudo tee /etc/sysctl.d/99-vip.conf
sudo sysctl --system
ip_nonlocal_bind 这一项是新手最常漏的:VIP 还没漂到本机时,HAProxy 尝试 bind 会直接失败,进程起不来,于是健康检查判定异常,形成”永远起不来”的死循环。
五、配置 HAProxy
两台机器的 HAProxy 配置完全一致,包括 stats socket——健康检查脚本会通过它判断进程是否真的在服务,而不只是进程存在。
# /etc/haproxy/haproxy.cfg
global
log /dev/log local0
maxconn 20000
user haproxy
group haproxy
daemon
# 关键:暴露 admin socket 给健康检查脚本使用
stats socket /var/run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
defaults
mode http
log global
option httplog
option dontlognull
option redispatch
retries 3
timeout connect 5s
timeout client 30s
timeout server 30s
frontend fe_web
bind *:80
# VIP 漂移期间仍监听所有地址,避免绑定失败
default_backend be_app
backend be_app
balance leastconn
option httpchk GET /healthz
http-check expect status 200
server app01 192.168.1.21:8080 check inter 2s rise 2 fall 3
server app02 192.168.1.22:8080 check inter 2s rise 2 fall 3
listen stats
bind *:8404
stats enable
stats uri /haproxy?stats
stats refresh 10s
stats auth admin:ChangeMe2026
几个参数值得单独说明。option httpchk 用的是应用层探测,能识别”端口开着但业务已经卡死”的情况,比 TCP 探测可靠得多;leastconn 在长连接场景下比轮询更均衡;timeout server 一定要小于上游 Nginx 或网关的超时,否则会出现”HAProxy 先断开、上游还在算”的浪费,反向代理的详细调优见Nginx 反向代理实战。
六、配置 Keepalived
主备两台机器只有 state、priority、unicast_src_ip 三处不同。注意:即使写了 state MASTER,最终角色仍由优先级和通告情况决定,state 只影响启动初期的默认行为。
# /etc/keepalived/keepalived.conf (lb-node-01,备机把 01/12 对调、priority 改 90)
global_defs {
router_id lb-node-01
script_user root
enable_script_security
# 加快 VIP 漂移后的 ARP 刷新
vrrp_garp_interval 0.001
vrrp_gna_interval 0.000001
}
vrrp_script chk_haproxy {
script "/etc/keepalived/check_haproxy.sh"
interval 2
weight -30
fall 3
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 100
advert_int 1
# 云上必须用单播,VPC 默认不转发组播
unicast_src_ip 192.168.1.11
unicast_peer {
192.168.1.12
}
authentication {
auth_type PASS
auth_pass FsdataVrrp2026
}
virtual_ipaddress {
192.168.1.100/24 dev eth0 label eth0:vip
}
track_script {
chk_haproxy
}
notify_master "/etc/keepalived/notify.sh master"
notify_backup "/etc/keepalived/notify.sh backup"
notify_fault "/etc/keepalived/notify.sh fault"
}
weight -30 与 priority 100/90 的组合含义是:主机健康检查失败后有效优先级降到 70,低于备机的 90,于是主动让位。这个差值必须大于两台机器的 priority 差,否则备机抢不过来。
七、健康检查脚本:别只看进程在不在
为什么不用 killall -0
网上大量教程直接写 killall -0 haproxy,只能判断进程存在。进程活着但后端全部 DOWN、或 stats socket 已卡死时,VIP 依然不会漂移,流量照样打空。更靠谱的做法是查 stats socket 的真实状态。
#!/bin/bash
# /etc/keepalived/check_haproxy.sh
SOCK=/var/run/haproxy/admin.sock
# 1) socket 可读且能返回运行时信息
if ! echo "show info" | socat - UNIX-CONNECT:$SOCK 2>/dev/null | grep -q "Uptime"; then
logger -t keepalived "HAProxy stats socket 不可用,判定故障"
exit 1
fi
# 2) 统计 BACKEND 里 DOWN 的数量(CSV 第 18 列为 status)
down=$(echo "show stat" | socat - UNIX-CONNECT:$SOCK \
| awk -F, 'NR>1 && $2=="BACKEND" && $18=="DOWN"' | wc -l)
if [ "$down" -ne 0 ]; then
logger -t keepalived "检测到 $down 个后端 DOWN,触发 VIP 漂移"
exit 1
fi
exit 0
#!/bin/bash
# /etc/keepalived/notify.sh 状态切换时推送告警
STATE=$1
HOST=$(hostname)
curl -s -X POST "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[VIP 切换] $HOST 当前角色: $STATE\"}}" \
>/dev/null 2>&1
logger -t keepalived "角色切换为 $STATE"
别忘了 chmod 755 /etc/keepalived/*.sh。脚本不可执行时 Keepalived 会静默跳过检查,日志里只有一行难以察觉的警告,这是排查”为什么没切换”时最容易忽略的地方。
八、验证与故障演练
配置完成后先做静态验证,再做破坏性演练。演练必须在低峰期进行,并且提前把 DNS TTL 调低。
# 1) 语法与启动
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl restart haproxy keepalived
sudo systemctl enable haproxy keepalived
# 2) 确认 VIP 在主节点
ip -4 addr show eth0 | grep 192.168.1.100
# 3) 抓 VRRP 通告,确认单播地址正确(协议号 112)
sudo tcpdump -i eth0 -nn proto 112 -c 5
# 4) 演练:主节点停掉 HAProxy,观察切机时间
date +%T; sudo systemctl stop haproxy
watch -n1 'ip -4 addr show eth0 | grep 192.168.1.100'
切机时间为什么是 6 秒左右
切机耗时的理论值是 interval × fall,即 2 秒 × 3 次 ≈ 6 秒,加上 ARP 刷新通常在 6–8 秒内完成。如果你观察到 VIP 漂移成功但访问仍然不通,八成是交换机或云平台的 MAC 表没刷新——此时可从客户端 arp -d 验证,或调整 vrrp_garp_interval。
九、公有云上最容易踩的四个坑
| 现象 | 根因 | 修复动作 |
|---|---|---|
| 两台机器都认为自己是 MASTER(双主脑裂) | VPC 不转发 224.0.0.18 组播,双方都收不到通告 | 改用 unicast_peer 单播心跳,并确认安全组放通协议号 112 |
| VIP 加不上 / 加了但外网不通 | VIP 不在实例网段,或云平台”源/目的地址检查”未关闭 | VIP 选择同 vSwitch 内闲置 IP;在控制台关闭源/目的地址检查 |
| 备机接管后 HAProxy 起不来 | 未设置 net.ipv4.ip_nonlocal_bind=1 | 写入 sysctl 并立即生效,再重启 HAProxy |
| EIP 绑不到 VIP | EIP 只能绑定实例或弹性网卡,不能绑虚拟 IP | 对外入口改用云 SLB,或在实例上做端口转发 |
脑裂的后果比单点宕机更糟:两台机器同时持有 VIP、同时响应 ARP,客户端会话会被分到不同节点,表现为随机掉登录态、写请求偶发失败。发现双主时不要急着重启,先抓一轮 tcpdump proto 112 确认心跳到底走没走通。另外,切换瞬间大量连接会重连,TIME_WAIT 可能短暂冲高,属于正常现象,可参考TIME_WAIT 过多排查处理。
十、它不适合什么场景
技术选型同样要看边界。以下三种情况,双机热备并不划算:
第一,纯云环境且预算允许。云 SLB 自带健康检查、证书卸载、弹性带宽和 DDoS 基础防护,把 VIP 漂移的自建成本换算成工时,往往比按量付费贵。它的价值在”入口 IP 固定且必须自控”。
第二,已经在用 Kubernetes。Ingress Controller 加 Service 的多副本本身就有冗余,再套一层 Keepalived 只是把复杂度叠高。除非你需要一个集群外的固定入口,那也应该优先考虑 MetalLB 之类的方案。
第三,服务已经是多活架构。当流量按地域分发、后端无状态且前面有 DNS 或 Anycast 时,单点 VIP 的意义就不大了,反而成为跨区故障的传播路径。
十一、上线检查清单
- VIP 与两台节点同网段,且通过 ping 确认未被占用
ip_nonlocal_bind=1已生效(sysctl net.ipv4.ip_nonlocal_bind)- 两台的
virtual_router_id一致且不与同网段其他集群冲突 - 心跳走单播,安全组/防火墙放通协议号 112
- 健康检查脚本可执行,且能区分”进程在”与”真在服务”
- 切换告警已接入群机器人,演练时收到通知
- HAProxy 配置纳入版本管理与自动化部署,见 GitHub Actions 实战
- 演练记录留档:切机时间、丢包情况、回切是否正常
十二、小结
双机热备的核心只有三件事:心跳能送达(云上走单播)、VIP 能绑上(nonlocal_bind 与同网段)、流量只在健康节点上(stats socket 级健康检查)。把这三条做扎实,入口层的可用性就从”一台机器”变成了”两台机器加一个会自己搬家的 IP”。如果你的数据库也在自建,主库的高可用是同一套思路的延伸,可对照PostgreSQL 流复制与高可用切换一起规划,避免只做了入口冗余、数据层仍是单点。




