1. 为什么我会在实验环境里折腾这套组合
先说个背景。之前帮一个业务团队做内部系统的双机改造,他们当时的架构是一台 nginx 扛所有流量,业务方最怕的不是 nginx 挂了,而是"挂了没人知道、知道了切不过去"。改 DNS 要等 TTL 生效,改完还有一堆本地缓存,等解析刷新完,用户早就投诉了。所以当时调研了一圈高可用方案,最后选定 keepalived + nginx 做实验验证,核心诉求就一个:让 IP 自己会漂移,让切换对用户无感。
很多人容易把 keepalived 当成 nginx 专属组件,其实它是独立的开源项目,本身做的是 Linux 下的高可用管理,最拿手的是基于 VRRP 协议实现虚拟 IP(VIP)漂移。它不关心你的业务是 nginx、tomcat 还是 MySQL,只要服务监听在某个端口,keepalived 就能通过健康检查感知状态,在节点故障时自动把 VIP 切到备用节点。这种解耦设计非常实用,一台 keepalived 可以同时管理多个服务的漂移策略,也可以只用它做纯粹的 IP 高可用。
这套方案到底解决了什么问题?打个比方,你家里宽带配了一个固定公网 IP,你用一台路由器拨号,路由器坏了全家断网。如果你的宽带支持两台路由器做 VRRP 热备,那任意一台坏了,另一台会立刻接手同一个 IP,终端设备根本感知不到底层发生了什么。keepalived + nginx 就是把这个思路搬到服务器上:两台 nginx 共享一个 VIP,正常情况下流量走主节点,主节点出问题时 VIP 自动漂移到备节点,客户端连接的还是同一个地址。
这篇实验笔记适合谁看?如果你正在做 Linux 运维入门、准备面试环境演练、或者要给业务上一个低成本的同城双机高可用方案,可以直接照着搭。实验本身不复杂,但涉及网络协议、系统配置、服务切换等多个层面,我会把我踩过的坑、排查的思路一起写出来,而不是只给你一份能跑通的配置文件。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实验拓扑与前置准备
2.1 网络拓扑和角色分配
我的实验环境用的是两台 CentOS 7.9 虚拟机,示意图上没有花哨的架构,就是最经典的一主一备。主节点 hostname 设为 lb01,IP 为 192.168.80.131;备节点 lb02,IP 为 192.168.80.132。两者原本不在同一个广播域时需要先打通二层网络,这里我直接用同一台物理宿主机上的 VMware NAT 网段,天然满足 VRRP 报文传输的条件。
VIP 我规划的是 192.168.80.130,和两台节点同网段。有人会问,VIP 能不能和业务 IP 不同网段?理论上只要交换机路由可达就能生效,但实验环境里面越简单越好,同网段会让抓包、验证和排障都省很多事。
顺带把域名映射也做了,我在这套环境里配了一个实验域名 app.lab.local 指向 VIP,这样可以模拟真实场景:用户访问域名,DNS 解析到 VIP,流量进入 nginx 集群。当然域名不是必须的,直接 curl VIP 也可以,但有域名会让后续验证更接近生产。
2.2 软件版本选择和安装源
nginx 版本我选了 1.24.0 的官方稳定版,没有用操作系统自带的 1.20 老版本。原因很简单:实验的目的是贴近线上,而线上越来越多的模块要求新版本特性,比如 HTTP/2、Stream 模块增强。keepalived 用的是 2.0.20,这个版本对 VRRP v2/v3 的支持都比较成熟,状态机的日志也更清晰,排障时会友好很多。
安装源方面,nginx 我直接配了官方 yum 源:
bash复制rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm
keepalived 在 EPEL 源里就有,版本可能不是最新但足够稳定。装完第一件事是检查版本号,别小看这一步,很多配置写法在不同版本间有细微差异,比如 vrrp_script 的定义方式、某些参数的默认值,版本不对照着老教程写容易踩坑。
2.3 系统基础配置:关防火墙,改主机名,同步时间
实验环境我先临时关闭了 firewalld 和 selinux。生产环境我不会建议直接关防火墙,而是放行 VRRP 协议(协议号 112)和 80 端口。但实验阶段关掉能减少一个变量,等你把整个链路跑通以后,再逐步把防火墙规则加回来验证,这样排障范围更可控。
bash复制systemctl stop firewalld && systemctl disable firewalld
sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
setenforce 0
然后就是时间同步。keepalived 的日志里会带时间戳,如果两台机器时间偏差太大,分析主备切换时序时会非常痛苦。我用 ntpdate 手动同步到阿里云 NTP 服务器:
bash复制yum install -y ntpdate
ntpdate ntp.aliyun.com
主机名和 hosts 也顺手配好:
bash复制hostnamectl set-hostname lb01
echo "192.168.80.131 lb01" >> /etc/hosts
echo "192.168.80.132 lb02" >> /etc/hosts
准备工作到这里已经花掉大约 20 分钟,但它帮我省掉了很多后面排查的时间。我见过有人跳过这些"无关紧要"的配置直接跑 keepalived,最后日志里全是莫名其妙的切换记录,查了半天才发现是系统时间差了 10 分钟加上主机名解析失败导致的。
3. 两台机器上的 nginx 基础部署
3.1 安装与配置文件结构说明
在两台机器上执行同样的 nginx 安装步骤:
bash复制yum install -y nginx
systemctl enable nginx
装完之后,nginx 的主配置文件在 /etc/nginx/nginx.conf,默认通过 include 方式加载 /etc/nginx/conf.d/*.conf 下的所有子配置。这种按目录拆分配置的做法我很推荐,实验里我把站点配置独立成 /etc/nginx/conf.d/app.conf,而不是全部堆在主配置文件里,这样切换、修改、备份都清晰。
关于配置文件结构,我多说几句。新手最容易犯的错是改完主配置文件就四处找"重启"命令,但 nginx 推荐的是 nginx -t 先校验语法,然后用 systemctl reload nginx 平滑重载配置。如果直接 restart,建立过的连接全部断开,生产上会有一瞬间的抖动;reload 则是 master 进程不退出,重新加载配置后 worker 平滑替换,对用户的影响小很多。实验环境虽然没有那么讲究,但从一开始就养成正确习惯,后面上生产会少踩很多坑。
3.2 区分主备节点的标志页面
为了让故障切换的验证结果一目了然,我在两台机器的默认站点配置里放了不同的标识内容。lb01 返回的页面标题是 "APP Server - LB01",lb02 返回 "APP Server - LB02",正文里再附带各自的 hostname 和 IP 信息。
nginx复制server {
listen 80 default_server;
server_name _;
root /usr/share/nginx/html;
index index.html index.htm;
location / {
return 200 "LB01 - 192.168.80.131 - active";
}
}
lb02 上同样配置,只是返回内容换成 LB02 的信息。这样做的价值在验证阶段就会体现出来:当 VIP 迁移到备节点后,curl http://192.168.80.130 的返回内容发生改变,你一眼就能判断当前流量到底被谁处理了,不用登录机器去查状态。
我习惯把这种区分做得更彻底一点——在 /usr/share/nginx/html 下放一个 health.html 文件作为健康检查专用路径,内容固定,不随业务变更而变化。因为生产环境健康检查路径最怕的就是和业务页面混在一起,业务逻辑异常导致页面 500 时,健康检查跟着失败,keepalived 就会误判节点状态。
bash复制echo "health-ok" > /usr/share/nginx/html/health.html
3.3 启动并验证两个节点工作正常
启动之前,先做个基础验证。单独访问两个节点的业务 IP,确认它们各自都能正常返回网页:
bash复制curl http://192.168.80.131/health.html
curl http://192.168.80.132/health.html
这一步如果都不通,那问题大概率出在 nginx 本身,而不是 keepalived。很多时候大家在 keepalived 上排查半天,最后才发现备节点 nginx 根本没启动,这种低级错误完全可以通过前置验证避免。
确认两个节点都正常后,我手动停掉其中一台的 nginx,测试 keepalived 能否正确感知。但这里先按住不表,因为要让感知生效,需要在 keepalived 配置里写上健康检查脚本,否则 keepalived 默认只检查接口存活状态,对 nginx 进程是否存活是"睁眼瞎"的。这正是我下一节要展开讲的核心点。
4. keepalived 安装与核心配置拆解
4.1 VRRP 协议是怎么工作的
keepalived 的核心是 VRRP(Virtual Router Redundancy Protocol),虚拟路由冗余协议。它解决的典型问题是:一组路由器(或服务器)共享同一个虚拟 IP,平常只有 Master 响应 IP 的流量,Backup 处于待命状态。Master 通过周期性发送 VRRP 通告报文(Advertisement)宣告自己还活着,Backup 在一段时间内没收到通告,就认为自己应该接管 VIP,从而触发角色切换。
这里有几个概念必须先理清楚,否则后面配置看的一头雾水:
- 优先级(priority):范围 0-255,数值越大优先级越高。Master 的优先级通常高于 Backup,但这个数值不是静态唯一的选主依据,还会结合各节点的"宣告状态"综合判断。
- 虚拟路由 ID(virtual_router_id):同一个 VRRP 组里,主备节点必须配置一样的 ID,这样它们才会被视为同一个虚拟路由器。
- 通告间隔(advert_int):Master 发送 VRRP 报文的时间间隔,默认 1 秒。设置太短会增加网络开销,太长会拉长故障切换时间。
- 抢占模式(nopreempt):默认情况下,更高优先级的节点启动后会抢占 VIP;配置 nopreempt 后,只要当前 Master 还活着,即使高优先级节点恢复了也不立刻切换,这样能避免频繁的角色抖动。
VRRP 的"虚拟组"概念可以这样理解:它像一个班级选班长,全班只有一个班长(Master)对外负责,班长会定期喊一声"我还在"(发送通告),如果大家长时间没听到班长点名(超时),副班长就自动顶上,对外身份和联系方式(VIP)完全不变,同学们(客户端)感知不到任何变化。
4.2 keepalived.conf 核心字段逐行解读
keepalived 主配置在 /etc/keepalived/keepalived.conf,这是整个实验的灵魂文件。我先把 lb01 的完整配置贴出来,然后逐段解释:
code复制global_defs {
router_id lb01
enable_script_security
}
vrrp_script check_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
timeout 2
rise 2
fall 2
}
vrrp_instance VI_1 {
state MASTER
interface ens33
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 123456
}
virtual_ipaddress {
192.168.80.130/24 dev ens33 label ens33:0
}
track_script {
check_nginx
}
}
global_defs 里的 router_id 是 keepalived 进程自身的标识,用于日志记录和报警展示,不参与 VRRP 选举逻辑,所以不需要跟 hostname 完全一致,但要保证可读性。enable_script_security 表示让脚本以调用者身份运行而不是默认的 nobody,这样健康检查脚本可以顺利读取日志文件等资源,这个参数在新版本里很关键,旧教程里常常漏掉。
vrrp_script 模块定义了一个健康检查脚本 check_nginx。interval 2 是每 2 秒执行一次脚本;timeout 2 是脚本最多执行 2 秒,超时视为失败;rise 2 表示连续成功 2 次才判定服务为健康;fall 2 表示连续失败 2 次才判定服务故障。这里我要强调一下 rise 和 fall 的设计逻辑:为什么不设成 1?因为网络抖动、负载瞬时过高都可能导致一次检查失败,如果失败一次立刻切换 VIP,实际上会造成大量无意义的抖动。让脚本连续失判 2 次再切换,代价是切换时间多出约 4-6 秒,但换来的是稳定性,这个取舍在生产环境里非常值得。
vrrp_instance 是 VRRP 实例的核心配置。state MASTER 声明本节点初始角色,Backup 节点对应写 state BACKUP。有人会问:既然优先级决定角色,为什么还要写 state?因为这是 keepalived 启动时的初始角色设定,如果两台机器同时启动,先启动的会先成为 Master;而在运行过程中,priority 才决定最终谁能抢到 VIP。所以实验时不要纠结"我写 MASTER 是不是就一定是 Master",真正看的是优先级数值和实际运行状态。
interface ens33 指定 VRRP 报文从哪个网卡发送,必须和业务网卡保持一致。virtual_router_id 51 是虚拟路由 ID,主备必须相同,不同业务组要用不同的 ID 防止冲突。priority 100 表示该节点优先级,lb02 我设成 90,相差 10 可以保证抢占时的明确性。advert_int 1 是 1 秒发送一次通告。authentication 里的 auth_type 选 PASS 简单认证,auth_pass 是明文密码,主备必须一致,否则 VRRP 报文会被丢弃。生产环境建议用更强的方式或至少保证报文在可信网络内传输。
virtual_ipaddress 就是 VIP 的挂载位置。这里我用了 label ens33:0 的方式,表示 VIP 以子接口的形式绑定到 ens33 上。这样做的好处是:IP 漂移时,keepalived 会精确地添加/删除这个带标签的子接口,和业务主 IP 完全隔离,ip addr show 时一眼就能看到当前 VIP 状态。dev ens33 则是显式指定 VIP 绑定的物理接口。
4.3 健康检查脚本:keepalived 和 nginx 之间的"翻译官"
vrrp_script 里引用的 check_nginx.sh 脚本长这样:
bash复制#!/bin/bash
if [ "$(systemctl is-active nginx)" = "active" ]; then
exit 0
else
exit 1
fi
这段脚本的逻辑很简单:systemctl 认为 nginx 服务处于 active,就返回 0(健康),否则返回 1(不健康)。keepalived 拿到非零返回值后,结合 fall 参数判断,连续失败达到阈值就触发 VIP 漂移。
但我用 systemctl 检查只是图省事,实际生产里我推荐直接用 curl 探测业务端口或健康检查路径。原因在于 systemctl is-active 判断的是服务进程管理模式,它认为 active 不代表你的应用真的能正常响应请求。一个极端的例子:nginx master 进程还活着,但所有 worker 进程卡死无法 accept 新连接,systemctl 依然认为 active,keepalived 也不会发现问题。改用 curl 探测 HTTP 响应,才是真正贴近业务的健康检查。
code复制#!/bin/bash
curl -s -o /dev/null -w "%{http_code}" --connect-timeout 2 --max-time 3 http://127.0.0.1/health.html | grep -q 200
这里 curl 静默探测本机的 health.html 路径,返回 200 就认为健康。--connect-timeout 2 --max-time 3 是防止 nginx 假死时 curl 一直卡住拖垮脚本。脚本超时后返回非零状态,keepalived 会按 fall 参数决定是否切换。
一个容易踩的坑:脚本必须加执行权限,并且最好用绝对路径调用。如果 keepalived 是以 root 运行的,脚本路径的所属用户是 root,但脚本内执行 curl 时可能受到系统 PATH 影响找不到命令,建议脚本里直接用 /usr/bin/curl 的绝对路径。我就是在这里浪费过半小时,日志里显示脚本返回值异常,一查是 curl 命令路径缺失。
4.4 主备节点配置的差异点
lb02 的配置和 lb01 几乎一样,差别只有四处:
- router_id 改成 lb02
- state 改成 BACKUP
- priority 改成 90
- 健康检查脚本内容不变
其它如 virtual_router_id、认证密码、VIP 地址、interface 名称必须保持完全一致。我列个对比表方便参考:
| 配置项 | lb01 | lb02 | 一致性要求 |
|---|---|---|---|
| router_id | lb01 | lb02 | 可不同,建议不同 |
| state | MASTER | BACKUP | 不同 |
| priority | 100 | 90 | 必须不同 |
| virtual_router_id | 51 | 51 | 必须相同 |
| auth_pass | 123456 | 123456 | 必须相同 |
| virtual_ipaddress | 192.168.80.130 | 192.168.80.130 | 必须相同 |
| interface | ens33 | ens33 | 必须相同 |
| check_nginx.sh | 相同 | 相同 | 必须相同 |
有朋友把 priority 设成一模一样,然后想着靠 state 区分主备,结果两边经常出现频繁抢 VIP 的问题。VRRP 选举时如果遇到相同优先级,会按照 VRRP 报文中的 IP 大小决出优先级略高的一方,但这个结果不可控,很容易出现两个节点轮流做主的抖动。所以主备优先级一定要设出明确差值。
5. 实验验证:VIP 漂移与故障切换实测
5.1 正常状态:VIP 在主节点上
配置好两台节点后,先通过 systemctl start keepalived 依次启动。启动顺序有讲究:我建议先启动备节点,再启动主节点。虽然 keepalived 支持动态抢占,但按这个顺序可以避免不必要的角色切换日志,让初始状态更干净。
启动完成,先看主节点上 VIP 是否挂载成功:
bash复制ip addr show ens33
192.168.80.130 应该出现在 ens33 或者 ens33:0 的条目里。同时查看 keepalived 日志确认状态:
bash复制tail -f /var/log/messages | grep Keepalived
日志里会出现类似:
code复制Keepalived_vrrp[12345]: Sending gratuitous ARP on ens33 for 192.168.80.130
Keepalived_vrrp[12345]: VRRP_Instance(VI_1) Entering MASTER STATE
然后从第三台机器(或者宿主机)访问 VIP:
bash复制curl http://192.168.80.130/health.html
此时返回的应该是 lb01 的内容。再访问业务页面,返回 "LB01 - 192.168.80.131 - active",说明流量进入了主节点。
这一步的核心意义在于确认"VIP 对外可达",不只是 VIP 挂上去了,而是通过 VIP 访问服务链路完全通。如果这里不通,检查方向通常是:VIP 是否真的绑定成功、nginx 是否监听 80、系统防火墙和路由表是否拦截。
5.2 模拟故障一:停掉主节点的 nginx
现在做最关键的故障切换实验。在 lb01 上执行:
bash复制systemctl stop nginx
然后在第三台机器上持续观察:
bash复制while true; do curl -s http://192.168.80.130/health.html && echo; sleep 2; done
过大约 5-8 秒后,访问结果会从 "LB01 - ..." 变成 "LB02 - ...",说明流量已经切到备节点。整个过程没有人为干预,客户端访问的 IP 也没变,这就是 keepalived 的价值所在。
日志方面,lb01 上会出现:
code复制Keepalived_vrrp[12345]: VRRP_Script(check_nginx) failed
Keepalived_vrrp[12345]: VRRP_Instance(VI_1) Entering BACKUP STATE
Keepalived_vrrp[12345]: Stopping gratuitous ARP on ens33 for 192.168.80.130
lb02 上则出现对应的进入 MASTER 状态的日志。这里我建议两边日志对照着看,因为常见问题就是一边降级了一边没升级,光看单机日志很难定位。
这时候我通常会再抓一把包,确认 VRRP 报文交换情况。在备节点上用 tcpdump 抓 ens33 网卡的协议 112 报文:
bash复制tcpdump -i ens33 vrrp -n
正常情况下可以看到 Master 每 1 秒发送一次 VRRPv2 通告,报文中的 Priority 字段能直接看出当前谁是 Master。当 lb01 故障后,lb02 在 3 个通告周期内没收到报文,就会转入 Master 状态并发出自己的通告。抓包分析是排查 VRRP 问题的利器,比看日志更直观。
5.3 模拟故障二:直接把主节点整机断电
停掉 nginx 只是应用层故障,keepalived 还在长期存活,但生产环境最怕的其实是整机宕机(断电、硬件故障、内核 panic)。这时 keepalived 进程本身也没了,无法主动发送"我要退出 Master"的报文,所以切换完全依赖 Backup 侧的超时机制。
我在 lb01 上直接执行 poweroff 模拟整机宕机。观察备节点日志和 VIP 状态,会发现切换时间比停 nginx 时略长一点。原因很简单:停 nginx 场景下,健康检查脚本最快在 2 秒内就能感知并触发主动降级,备节点收到降级报文会立刻抢占;而整机宕机时,备节点必须等待 Master 的通告超时(通常是 3 个通告间隔),然后才能进入 Master 状态。
实验数据也验证了这一点:停 nginx 场景下切换耗时约 5 秒,整机宕机场景下约 6-7 秒。这个差异在生产上是可以接受的,前提是业务对切换超时的容忍度高于这个值。
这里要提一个重要的知识点:VIP 漂移只是切换的第一层,后面还有一件事就是ARP 缓存更新。当 VIP 从 lb01 漂移到 lb02 时,交换机和客户端原本学习到的"192.168.80.130 对应的 MAC 地址是 lb01 的网卡 MAC",如果这个缓存不更新,后续发往 VIP 的数据包依旧会送给已经宕机的 lb01。keepalived 的处理方式是在状态变化时主动发送 gratuitous ARP 广播,通知所有设备刷新缓存。我在实验里用抓包的方式确认过,进入 Master 状态时,新 Master 会立刻发送大量免费 ARP 报文,这也就是日志里 "Sending gratuitous ARP" 那一行的来源。
5.4 故障恢复:主节点回归的两种表现
把 lb01 恢复开机、启动 nginx 和 keepalived,观察 VIP 的归属变化,这里有两种情况值得记录。
第一种是默认抢占模式:lb01 恢复后,因为它 priority 100 高于 lb02 的 90,所以它会主动发通告请求重新成为 Master,VIP 会再次漂移回 lb01。这个过程中,业务会出现一次短暂的闪断,因为 VIP 先被摘除,再挂载到 lb01,然后再发免费 ARP 通知刷新缓存。
第二种是配置 nopreempt 的非抢占模式:只要当前 Master(lb02)还活着,lb01 恢复后即使优先级更高也不会立刻抢占 VIP,直到下次故障它才有机会成为 Master。这种模式特别适合不希望频繁切换的场景,比如数据库主从,频繁切换反而可能引发复制链路抖动。
实验里我验证了两种模式的表现差异,最终倾向于生产环境用非抢占模式。原因很现实:keepalived 切换的成本不仅仅是 VIP 漂移那几秒,更重要的是下游系统和中间件(比如 Redis、MySQL 连接池、注册中心)都需要重新建立连接,频繁的主备倒换对系统的稳定性影响远大于单次故障本身。当然,如果你的服务是纯无状态、连接可快速重建的,抢占模式也是可以接受的,它能让流量更均匀地分散。
6. 容易被忽略的坑:从"实验能跑"到"生产能扛"
6.1 ignore_uuid 和 VRRP 报文不一致问题
实验做到后面,我遇到过一个比较刁钻的状况:两台 keepalived 配置完全一致,但备节点始终收不到主节点的 VRRP 报文,VIP 一直不漂移。日志在备节点显示 "RX: Dropping VRRP packet because it is not from a router on a properly configured VRRP subnet"。
排查思路是逐步排除:先抓包确认主节点确实发出了报文,备节点也确实收到了报文,说明网络二层没问题;然后对比两个节点的 virtual_router_id、认证字段,完全一致;最终发现问题出在 keepalived 2.0 版本的 interface 参数上——我一开始在备节点写的 interface 名称是 ens33,但备节点上实际网卡名称叫 eno16777736,这导致 VRRP 报文虽然到了主机,但被 keepalived 自己的接口过滤逻辑丢弃了。
这类问题的教训很朴素:拿到任何一台机器,先 ip link 看清楚网卡真实名称,再写配置。虚拟机的网卡名经常和物理机不同,用 ip addr 确认远比凭记忆书写可靠。
6.2 脑裂问题的本质和预防
脑裂(split-brain)是高可用方案里绕不开的话题。在 keepalived 场景下,如果两台节点互相之间无法通信,但它们各自都认为对方已死,就都会尝试绑定 VIP 并对外提供服务。结果是同一 IP 被两台机器同时持有,流量可能被负载均衡器或者交换机随机分发到两台机器,业务表现就会出现随机的不一致。
在我的实验里,特意模拟过一次脑裂:用 iptables 把两台节点之间的 VRRP 报文全部 DROP,但保留业务流量。观察结果非常有意思:lb01 和 lb02 在几秒后都进入了 MASTER 状态,VIP 被绑定到两台机器上,但通过交换机从外部访问 VIP,流量只会到其中一台机器(具体到哪台取决于交换机的 MAC 表学习结果),因为同一网段内同一个 IP 只能有一个 MAC 地址能被交换机记住。
预防脑裂的标准手段是加第三路仲裁,比如引入独立的 ping 节点、或者用数据库锁、分布式协调服务(etcd、consul)做决策。keepalived 本身也提供了一个轻量级的 vmcast 机制,但它只能降低脑裂概率,无法彻底消除。所以在生产架构设计时,通常会把 keepalived 作为"快速切换层",真正的强一致性还是得靠业务层的幂等设计和数据同步机制来兜底。
6.3 健康检查脚本对切换效率的影响
我在实验里做过一组对比测试,改变健康检查脚本的 interval 和 fall 参数,观察切换耗时的变化:
| 配置组合 | 切换耗时 | 适用场景 |
|---|---|---|
| interval=1, fall=1 | 约 2 秒 | 对切换时间极其敏感,但容易误判抖动 |
| interval=2, fall=2 | 约 5 秒 | 实验默认组合,平衡稳定性与速度 |
| interval=3, fall=3 | 约 10 秒 | 对稳定性要求极高,切换太久会出现连接超时 |
数据规律很明显:切换时间和 interval × fall 基本成正比。如果你的业务能容忍 10 秒以上的中断,可以把参数调大换取更稳定的判断;如果业务要求秒级切换,就要接受脚本可能因为一次瞬时故障就误切换的风险。没有一组参数是绝对正确的,只有适合业务场景的。
实际生产我还遇到过一个问题:健康检查脚本用了 curl 127.0.0.1,而 nginx 的 access log 里全是 127.0.0.1 的访问记录,导致真实用户访问日志和健康检查日志混在一起,分析访问量时数据严重失真。后来我把健康检查改成访问独立端口或独立路径,并在 nginx 配置里单独配置 access_log off,彻底把健康检查流量从业务日志中剥离。
6.4 实验到生产:还有哪些配置要改
实验环境跑通不等于生产可以直接用,中间还差几步额外加固。
第一,防火墙规则必须精确放行。VRRP 报文不用 TCP/UDP 端口,而是直接基于 IP 协议号 112。firewalld 的放行写法是:
bash复制firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 --protocol 112 --in-interface ens33 --action ACCEPT
同时还要放行 80 端口以及 keepalived 所用的 VRRP 组播地址 224.0.0.18。如果用 iptables 的话要注意,VRRP 报文目标地址是组播地址,如果规则里写了 -d 192.168.80.130 这种具体地址,VRRP 报文根本不会被匹配。
第二,keepalived 日志默认写到 /var/log/messages,和生产上的日志采集工具(如 rsyslog、filebeat)集成时要指定好过滤规则。我见过因为 keepalived 日志量太大把 rsyslog 队列打满的案例,所以建议单独配置 keepalived 的日志输出:
bash复制/etc/rsyslog.d/keepalived.conf:
:programname, isequal, "Keepalived" /var/log/keepalived.log
& stop
第三,生产环境要对 keepalived 进程本身做守护。很多教程只在 systemd 里 enable 了 keepalived,但如果 keepalived 进程因为段错误等原因退出了,systemd 又不自动拉起,那高可用就变成没可用。可以在 systemd 服务文件里加上 Restart=always,并配合 StartLimitInterval 参数防止无限重启。
第四,VIP 的上游链路检查很重要。我这里说的是更细的一层:如果 nginx 本身健康,但它的上游应用(比如后端的 Tomcat、网关)已经全部不可用,nginx 返回的是 502/504,那健康检查脚本用 curl 127.0.0.1/health.html 就永远会返回 200,keepalived 不会切换,用户访问时看到的全是 502。所以更健壮的配置是健康检查脚本同时探测后端关键接口的响应码,或者探测一个会触发完整调用链的页面,确保"nginx 活着"和"nginx 能代理到后端"是两个都需要被验证的层次。
6.5 keepalived 和 haproxy 到底有什么区别
这个热词我在调试过程中也反复被同事问到,因为很多人会把 keepalived + nginx 的组合和 keepalived + haproxy 的组合搞混。我在实验环境里其实两套方案都搭过,简单梳理一下区别:
| 对比维度 | keepalived + nginx | keepalived + haproxy |
|---|---|---|
| 核心作用 | 提供四层到七层的 Web 服务承载 + 高可用 | 提供四层/七层负载均衡 + 高可用 |
| 流量调度能力 | nginx 通过 upstream 做负载均衡,支持七层 HTTP 精细化路由 | haproxy 支持更灵活的四层 TCP 和七层 HTTP 转发、ACL 规则 |
| 健康检查深度 | keepalived 的脚本检查的是节点自身状态 | haproxy 自身就带强大的后端健康检查(HTTP/TCP/脚本),keepalived 只管它的整体存活 |
| 适用场景 | Web 服务 + 静态资源、需要做反向代理、缓存、TLS 终结 | 需要四层端口转发、数据库读写分离中间层、大量 TCP/UDP 代理 |
| 配置复杂度 | 中低,nginx 配置直观,keepalived 配置纯粹 | 中高,haproxy 配置能力更强但学习曲线更陡 |
简单总结我个人的选型经验:如果你的核心诉求是高可用 nginx Web 服务,keepalived + nginx 足够、直接、好维护;如果你需要一个更专业的负载均衡器来承担四层/七层流量分发、对后端做精细健康检查,但又不希望引入 LVS 之类的内核依赖,那我建议 keepalived + haproxy。我曾经在一个内部工具平台把 nginx 换成 haproxy 做 TCP 流量转发层,就是因为 nginx 的 stream 模块在大量并发短连接场景下表现不如 haproxy 稳定。但这是"负载均衡能力"层面的问题,和 keepalived 解决的高可用问题是两回事,不要混为一谈。
7. 最后的实验心得与几点运维习惯
整套实验做完,我把两台虚拟机分别重启了一遍,重新验证自动拉起的整体链路。最终确认:系统开机后两台 keepalived 会自动启动,VIP 会先出现在 priority 更高的 lb01 上,通过 VIP 能正常访问服务;停掉 lb01 的 keepalived 后 VIP 自动漂移到 lb02;恢复 lb01 后 VIP 自动回归(或等待下次切换,取决于是否配置 nopreempt)。这个闭环实验证明这套方案已经具备基本的高可用能力。
说几个我在反复搭建这套环境后形成的运维习惯,算是对整个实验的总结性补充。
第一,keepalived 的配置变更一定要备份。每次改动前执行 cp /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf.bak.$(date +%F_%T)。这个习惯成本极低,但在某次手误把 virtual_router_id 改成重复值导致两个业务组互相干扰时,我靠备份在 2 分钟内完成了回滚,没有影响到其它正在跑的实验内容。
第二,验证高可用方案一定要做"三层验证"。第一层是配置验证,用 keepalived --config-test 检查配置文件语法;第二层是状态验证,用 ip addr 和日志确认 VIP 归属;第三层才是业务验证,从外部持续访问 VIP 观察切换过程是否存在可感知的抖动。很多人在第一层过了就直接宣布方案成功,往往会漏掉第二、三层里面才能暴露的问题,比如 ARP 缓存不刷新、健康检查脚本权限错误等。
第三,所有切换测试都要记录时间基线。我在实验里做了一个简单的测试记录表,记录故障注入时间、切换完成时间、业务恢复时间、日志关键行。几次测试下来,这些数据就成了优化参数的重要依据,比如调整 advert_int 后效果如何,看时间基线比凭感觉判断靠谱得多。生产环境的故障演练也应该这样,每个环节的可观测数据要留痕,否则"高可用"就是一个没有验证过的纸上概念。
keepalived + nginx 是我接触过性价比最高的高可用组合之一。它不像专业负载均衡设备那样昂贵,配置维护也不像分布式协调系统那样复杂,对大部分中型 Web 服务来说足够扛住绝大多数单点故障场景。当然它也不是银弹,脑裂、网络分区、健康检查盲区这些老问题至今没有完美的通用解法,需要在架构设计的更高维度去配合处理。但这些都不妨碍你从一个简单的双机热备实验开始,去理解高可用系统的核心思想——鸡蛋不要放在同一个篮子里,以及,篮子坏了要能让整个货架毫无感知地完成替换。
