keepalived与VRRP高可用:从配置到切换演练

一、先想清楚:高可用要解决的是哪一种中断

很多站长第一次接触 keepalived,是因为某天凌晨服务器重启了半小时,网站打不开,第二天被用户投诉。于是跑来问:装个 keepalived 能不能让网站不中断?

答案要先分清中断的类型。高可用(High Availability)这个词在运维里指的是「减少单点故障导致的不可用时间」,它不解决所有问题。具体来说,keepalived 这类工具能处理的是:

  • 单机故障:一台机器断电、网卡坏、内核崩、磁盘挂死。这是它能解决的问题,也是它存在的唯一理由。
  • 软件故障:Nginx 配置写错 reload 失败、PHP-FPM 进程耗尽。这类问题 keepalived 配合健康检查也能兜住,但前提是健康检查真的能发现问题。
  • 数据层故障:MySQL 主库挂了、磁盘数据损坏。这类问题需要数据库自己的主从切换方案,keepalived 只能做「把 VIP 挪走」这件事,挪走之后如果从库数据是旧的,业务照样出问题。
  • 它解决不了的:机房整体断电、CDN 回源被墙、域名解析被污染、代码逻辑写错导致的全站 500。这些跟 IP 高可用没有任何关系。

所以正确的预期是:keepalived 把「一台机器坏了网站打不开」压缩成「几秒内切换到备机,用户最多看到一次刷新」。它给你的是秒级到十秒级的恢复能力,不是「永远不宕机」。把这句话先讲明白,后面所有的参数取舍才有依据。

二、VRRP 到底在做什么:一个虚拟 IP 的争夺

keepalived 的核心协议是 VRRP(Virtual Router Redundancy Protocol,虚拟路由冗余协议),原本是给路由器做网关冗余用的,后来被 Linux 上的 keepalived 借来实现服务器高可用。理解它的机制,比记住配置模板重要得多。

它的工作原理可以用一句话概括:一组机器共同持有一个虚拟 IP(VIP),任何时刻只有一台机器真正拥有它,其余的机器在旁边盯着。

具体过程是这样的:

  • 每台机器上都跑一个 keepalived 进程,配置里写着同一个 virtual_router_id 和同一组 virtual_ipaddress。
  • 拥有 VIP 的那台(MASTER)每隔 advert_int 秒(默认 1 秒)向组播地址 224.0.0.18 发一个 VRRP 通告包,说「我还活着,VIP 在我这」。
  • 其他机器(BACKUP)收到通告就知道主还活着,自己继续待命。如果在 advert_int × 3(也就是 3 秒左右)内没收到通告,就认为主挂了,按优先级重新选举,优先级最高的那台把 VIP 加到自己网卡上。

这里有几个关键点,是新手最容易忽略的:

第一,VIP 不是通过 DNS 切换的。VIP 是直接加在网卡上的一个附加地址,用的是 ARP 广播告诉交换机「这个 IP 现在在我这」。所以切换速度取决于 ARP 缓存刷新的速度,通常是秒级。DNS 方案的 TTL 再短也要几分钟,这就是必须用 VIP 的原因。

第二,优先级(priority)决定谁是主。同一个 virtual_router_id 组里,priority 数字最大的那台会成为 MASTER。数字范围是 1 到 255,但要留出余量给 track_script 扣分用(后面细讲)。

第三,virtual_router_id 必须全局唯一。如果同一个二层网络里两个不同的 keepalived 组用了相同的 ID,双方会互相抢通告,导致 VIP 在两台机器之间反复跳。这是最经典的踩坑,症状是「VIP 每隔几秒换一次位置」,用 tcpdump -i any vrrp 就能看到两个不同 MAC 在发同 ID 的通告。

三、最小可用配置:两台机器加一个 VIP

假设我们有两台 Nginx 前端,IP 分别是 10.0.0.11 和 10.0.0.12,希望对外提供 10.0.0.100 这个 VIP,让用户访问 VIP 就能拿到服务。

先在两台机器上都安装:

# Debian / Ubuntu
apt-get install -y keepalived

# CentOS / Rocky
yum install -y keepalived

systemctl enable keepalived

主机的配置 /etc/keepalived/keepalived.conf:

global_defs {
    router_id node_a
    script_user root
    enable_script_security
}

vrrp_script chk_nginx {
    script "/etc/keepalived/check_nginx.sh"
    interval 2
    weight -30
    fall 3
    rise 2
}

vrrp_instance VI_WEB {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1
    unicast_src_ip 10.0.0.11
    unicast_peer {
        10.0.0.12
    }
    authentication {
        auth_type PASS
        auth_pass zz1984ha
    }
    virtual_ipaddress {
        10.0.0.100/24 dev eth0 label eth0:vip
    }
    track_script {
        chk_nginx
    }
}

备机的配置几乎一样,只改三处:router_id 改成 node_b,state BACKUP,priority 90,unicast_src_ip 改成自己的 IP。

健康检查脚本 /etc/keepalived/check_nginx.sh:

#!/bin/bash
# 不只是看进程在不在,还要看它能不能真的响应
if ! ss -lnt | grep -q ':80 '; then
    exit 1
fi
if ! curl -s -m 2 -o /dev/null -w '%{http_code}' http://127.0.0.1/healthz | grep -q '^200$'; then
    exit 1
fi
exit 0

注意脚本必须可执行:chmod 755 /etc/keepalived/check_nginx.sh。这一点看着废话,但 script_user root 加 enable_script_security 之后,权限不对会直接被拒绝执行,而 keepalived 只在日志里说一句,不报错退出,非常隐蔽。

四、别用组播,改单播:云环境的第一条铁律

上面配置里我直接写的是单播(unicast),没用默认的组播。这是有原因的。

经典教程里的 keepalived 都用组播,因为它在同一个物理二层网络里配置最简单,什么地址都不用填。但现在大部分站长的服务器在云上或者用了 VPC,云厂商普遍禁止或限制组播,结果就是两台机器都在正常跑,通告却谁也收不到,双双认为对方死了,于是双方都把自己选成 MASTER,VIP 同时在两台机器上出现。

这个故障的名字叫「双主」(split-brain),破坏力极大:两个机器同时持有 VIP,交换机上的 ARP 表会被来回覆盖,流量忽左忽右,用户看到的是页面时好时坏、登录状态莫名丢失。因为两台机器都没有报错,日志看起来一切正常,排查往往要花很久。

unicast_src_ip 和 unicast_peer 就是解决这个问题的:不再往组播地址发,而是直接点对点把通告发给对端。即使网络屏蔽组播,单播也能通。

判断是否已经出现双主,三条命令:

# 两台机器都执行,看 VIP 在哪
ip -4 addr show | grep 10.0.0.100

# 看 VRRP 通告有几个来源
tcpdump -i eth0 -nn vrrp -c 20

# 看状态切换记录
journalctl -u keepalived --since "-30min" | grep -i "state\|transition"

如果两台机器上 ip addr 都能看到 VIP,或者 tcpdump 里出现两个源 IP 在发同一 ID 的通告,就是双主无疑。修法是补上单播配置,然后两台机器依次重启 keepalived。

五、weight 与 track_script:扣分机制怎么设计

vrrp_script 里的三个参数是配置里最难凭直觉理解的部分,值得单独说清楚。

  • interval:多久跑一次检查脚本。设太小会频繁 fork 进程,设太大会延迟发现故障。2 秒是个常见的平衡点。
  • fall:连续失败几次才判定为真的故障。设成 3 意味着连续 3 次失败(约 6 秒)才动手。这个参数是抗抖动的,避免 Nginx 只是短暂卡顿 200 毫秒就被切走。
  • rise:连续成功几次才算恢复。设成 2 是防止刚恢复就又被判定为坏、来回震荡。
  • weight:这个最关键。脚本失败时,本机优先级会被减去(负数)或加上(正数)这个值。负数是主流用法:检查失败时自己降级,让对端成为 MASTER。

这里有一个必须注意的设计原则:weight 的绝对值必须大于「两台机器的初始优先级差」。假设主机 100、备机 90,差值是 10;如果 weight -5,主机掉到 95,仍然高于 90,备机根本不会接管,健康检查形同虚设。所以要么用 -30 这种明显大于差值的数,要么干脆把两台机器的 priority 设成一样的值(比如都是 100),让 weight 成为唯一的裁决依据。

我个人的习惯是后者:两台 priority 都写 100,weight 写 -30。这样语义最清晰——谁健康谁拿着,两台都健康时靠 keepalived 的「先启动者优先」或 nopreempt 决定。

nopreempt 值得单独提。默认情况下,当原来那台恢复健康、优先级重新变高时,它会把 VIP 抢回去。这意味着每次故障恢复都会再闪一次。对于网页服务,一次多余的切换就是一次新的连接中断,完全没必要。建议在备机上开启 nopreempt,让 VIP 留在当前这台直到它真的坏了为止。代价是两台机器的负载可能长期不均,需要你自己权衡。

六、VIP 漂过去了,会话为什么还在:有状态与无状态的差别

这是很多站长上线 keepalived 之后踩的第二个坑:VIP 切换成功了,curl 访问返回 200,但用户反馈「登录被踢了」「购物车空了」「后台跳回登录页」。

原因是会话(session)存在了本地。VIP 只是把流量导到另一台机器,它不会搬数据。原来那台机器上的 PHP session 文件、上传的临时文件、本地缓存,新机器一个都没有。

三条解决路径,按推荐度排序:

  • 会话外置到 Redis 或数据库。这是唯一正确的长期方案。PHP 改 session.save_handler = redis 并配好 session.save_path,WordPress 用 redis-object-cache,自研程序改 session 驱动。
  • cookie 签名共享。如果会话数据本身就放在加密的 cookie 里(无状态会话),两台机器只要 APP_KEY 一致就能互认。前提是签名密钥写在配置文件里且两台完全一致,不要各自生成。
  • 会话粘滞。在负载均衡层按 IP 哈希做粘滞。这只是把问题推后,一旦某台机器挂了,粘在它上面的用户照样掉线,只能作为临时妥协。

另外,无状态化的范围不只是会话。文件上传目录、动态生成的缩略图、日志、计划任务也都得处理:上传目录用对象存储或共享盘,缩略图能重生成就重生成,cron 任务要么加锁要么只在一台上跑。

有一个很实用的检验方法:上线前把主机直接关机,然后用浏览器完整走一遍业务流程(登录、提交表单、上传文件、翻页、退出)。哪一步卡住,哪里就还有本地状态。这个测试比任何配置检查都有效,而且只要十分钟。

七、切换要多少秒:把感知延迟拆开算

老板问「切换要多久」,别答「秒级」,要给出能对得上的数字。故障被用户感知到的总延迟,是四段之和:

  • 检查发现:interval × fall。2 秒 × 3 次 = 6 秒。这段是大头,也最容易调。
  • 通告超时:如果直接靠 VRRP 超时,是 advert_int × 3,默认 3 秒。如果健康检查先触发扣分,这段可以和上一段重叠,不必重复计算。
  • ARP 刷新:VIP 在新机器上生效后,交换机、网关、客户端都要更新 ARP 缓存。通常 1 秒内,但如果中间有老式交换机或客户端缓存很重,可能是几秒到几十秒。主动发免费 ARP(gratuitous ARP)能大幅缩短这一步,keepalived 默认会发,配置里的 garp_master_delay 可以调,默认 5 秒,建议改成 1。
  • 客户端重试:浏览器和移动端对失败的请求有自己的重试策略,这段时间不受你控制。所以让超时短一点、让前端有重试,比纠结切换快 1 秒更有价值。

把这些加起来,实际可做到 5 到 10 秒内恢复。想更快就把 interval 降到 1 秒、fall 降到 2,但要注意误判率会上升——健康检查脚本本身也会因为系统负载短暂抖动而超时。

八、上线前的验收清单

配置写完不算完,必须做一轮主动演练,否则你永远不知道自己配置的是不是真能用。清单如下:

  1. 确认初始状态:ip addr 看 VIP 只在主机上;journalctl -u keepalived -n 30 看状态是 MASTER。
  2. 停服务触发切换:在主机上 systemctl stop nginx,等 8 秒,检查备机是否拿到 VIP,前端访问是否恢复。
  3. 恢复服务确认不抖动:在主机上 systemctl start nginx,确认 VIP 没有立刻被抢回(开了 nopreempt 的话)。
  4. 整机断电演练:直接 poweroff 主机,检查备机接管时间。这是唯一能验证「真宕机」场景的方法。
  5. 双主排除:确认任何时刻只有一台机器持有 VIP。
  6. 恢复后的一致性:主机重新开机后,确认它没有带着旧的 VIP 上线打架。

每一项都记录时间点,形成一份可复查的演练记录。下次真出事的时候,这份记录能帮你判断「是不是切换慢了」还是「压根没切」。

九、常见故障与三个误区

故障一:VIP 加上了但外网访问不通。先确认前端防火墙放行了 VIP 和对应的端口。有些防火墙规则绑定在具体 IP 上,VIP 是新地址,规则不匹配就会被丢。另外云厂商的安全组也要加 VIP。

故障二:日志里反复出现「IP address ... is duplicate」。这是双主的直接证据,回到单播配置那一步排查。

故障三:keepalived 启动正常但 VIP 一直不出现。多半是 interface 写错了网卡名,或者 virtual_router_id 超过 255、或者 VIP 的掩码与网段不一致导致添加失败。用 journalctl -u keepalived -f 实时看最直接。

三个常见误区:

  • 误区一:以为有了 keepalived 就不需要备份。高可用只防「机器坏」,不防「误删数据」。主从两台机器上一次 rm -rf 会同步到两边,照样全丢。
  • 误区二:把 MySQL 也直接套用这一套。VIP 挪到从库上之后,从库默认是只读的,主从复制关系也没反转,业务立刻报错。数据库切换要用专门的方案(MHA、Orchestrator、云数据库自带的主备切换),别指望 keepalived 一把梭。
  • 误区三:只做两台机器就以为万无一失。两台机器同时重启(比如升级内核后都重启)照样全站不可用。真正的可用性来自多个维度:多机房、CDN、可快速重建的镜像、以及能在一小时内恢复的备份。

十、小结

keepalived 本身不复杂,配置文件不到三十行,真正难的是三件事:想清楚它解决的是哪一类中断;在云环境里用单播避免双主;以及把会话和文件这类「本地状态」搬到外部去。这三件事做到位,VIP 切换才会真的对用户表现为一次短暂刷新,而不是一次需要重新登录的事故。剩下的就是定期演练——高可用方案不演练,等于没有。

Last modification:November 2nd, 2026 at 10:27 pm

Leave a Comment