LVS + Keepalived 四层负载均衡实战:DR 模式部署、VIP 漂移与 ARP 参数陷阱全流程

为什么四层负载均衡仍然值得学

很多站长一提到负载均衡,脑子里蹦出来的第一反应是 Nginx 的 upstream。Nginx 确实好用,做七层反向代理、按 URL 分流、按 Host 分流都游刃有余。但 Nginx 本质上是一个应用层软件,它必须把整个 TCP 连接收下来、解析 HTTP 请求,再转发给后端。当你的业务是 TCP 长连接、数据库代理、游戏服务、或者单纯就是需要扛住几十万并发连接时,七层方案会先被自己拖死。

LVS(Linux Virtual Server)走的是另一条路。它是内核态的四层负载均衡,工作在 netfilter 框架里,靠 IPVS 模块直接改写数据包的目的地址,然后交给内核转发。它不解析应用层协议,不建立完整的用户态连接,因此吞吐量是 Nginx 的好几倍,单机扛住几十万并发连接是家常便饭。而 Keepalived 则负责给 LVS 提供高可用——两台调度器互相盯着,主挂了备自动接管那个对外的虚拟 IP。

本文从零讲清楚 LVS + Keepalived 的 DR 模式部署:为什么要用 DR 而不是 NAT、VIP 是怎么在两台机器间漂移的、后端为什么必须把 VIP 绑到 lo 接口、以及一个几乎人人都会踩的坑——ARP 广播导致整个集群流量错乱。

LVS 的三种工作模式

在动手之前必须先搞清楚 LVS 的三种转发模式,因为它们决定了网络拓扑和后端配置,选错了后面全是坑。

  • NAT 模式:调度器改写请求包的目的 IP,转发给后端;后端处理完把响应交回调度器,调度器再改写源 IP 发回客户端。问题是所有响应流量都要经过调度器,调度器立刻成为新的瓶颈,而且后端必须把网关指向调度器。现在基本没人用了。
  • TUN 模式:调度器把请求包用 IP 隧道封装后发给后端,后端解封装处理,响应直接回给客户端,不再经过调度器。解决了响应流量瓶颈,但要求后端内核支持 IPIP 隧道,且不支持端口映射,跨机房时也容易出现 MTU 问题。
  • DR 模式(Direct Routing,直接路由):调度器只改写请求包的 MAC 地址,把包转发给后端;后端处理完后,用自己 lo 接口上绑定的 VIP 作为源地址,直接把响应发给客户端。响应流量完全绕开调度器,性能最好,是生产环境的主流选择。

本文的一切配置都基于 DR 模式,因为它是最常用也最能体现 LVS 价值的方案。

拓扑规划与 IP 分配

先把机器和地址定下来,后面所有配置都围绕这张表展开。假设你有两台调度器和两台后端:

角色        主机名      内网 IP          说明
调度器主    lb-master   10.0.0.11        绑定 VIP,对外服务
调度器备    lb-backup   10.0.0.12        待命,主挂了接管 VIP
后端 1      web-1       10.0.0.21        真实服务器 RS
后端 2      web-2       10.0.0.22        真实服务器 RS
虚拟 IP     VIP         10.0.0.100       客户端访问的地址

关键约束:两台后端必须和调度器在同一个二层网络里(同一个交换机、同一个网段)。因为 DR 模式靠改 MAC 地址转发,二层到不了就没法玩。这是 DR 模式最硬的限制,跨网段、跨机房就不要用 DR,改用 TUN 或干脆上七层。

第一步:给调度器安装 IPVS 和 Keepalived

IPVS 早就合并进内核主线了,现代发行版的内核都自带这个模块,你只需要装管理工具和 Keepalived:

apt-get update
apt-get install -y ipvsadm keepalived

装完先确认内核模块可用:

ipvsadm --version
lsmod | grep ip_vs

如果 lsmod 没有输出也不用慌,模块是加载时才用的,Keepalived 启动时它会自动 modprobe。手动验证一下:

modprobe ip_vs
lsmod | grep ip_vs

第二步:配置 Keepalived 的 VRRP 高可用

Keepalived 的核心是 VRRP 协议。两台机器组成一个虚拟路由器组(VRID 相同),周期性互相发送心跳通告。主的优先级高,持有 VIP;主的挂了,几秒内备的通告超时,备立刻抢占 VIP。客户端对此毫无感知。

主调度器 /etc/keepalived/keepalived.conf:

global_defs {
    router_id lb-master
    enable_script_security
}

vrrp_script chk_haproxy {
    script "/usr/bin/kill -0 $(cat /var/run/keepalived.pid)"
    interval 2
    weight -20
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass zz1984vip
    }
    virtual_ipaddress {
        10.0.0.100/24 dev eth0 label eth0:vip
    }
    track_script {
        chk_haproxy
    }
}

备调度器的配置几乎一样,只改三处:router_id 改成 lb-backup、state 改成 BACKUP、priority 改成 90(必须比主低)。其它像 virtual_router_id、auth_pass、virtual_ipaddress 必须和主一模一样,否则组不起来。

几个容易忽略的点:

  • advert_int 1 表示每秒发一次心跳,配合默认 3 倍超时,故障切换大概在 3 秒左右。对绝大多数业务够用,再快就要牺牲网络抖动下的稳定性。
  • track_script 做健康检查,如果本机的服务异常,weight -20 会让本机优先级降下来,主动让出 VIP。这是避免"接管了 VIP 但服务是坏的"的关键。
  • label eth0:vip 给 VIP 起个别名,方便 ip addr 里一眼认出来。

启动并验证 VIP 是否落在主上面:

systemctl enable --now keepalived
ip addr show eth0 | grep 10.0.0.100

主上应该能看到 VIP 那个地址。此时把主停掉,几秒后再看备,VIP 应该漂移过去了。

第三步:配置 IPVS 负载均衡规则

Keepalived 不光管 VIP,它还能直接下发 IPVS 规则。在 keepalived.conf 末尾追加一个 virtual_server 块,Keepalived 会在本机自动维护 ipvsadm 规则:

virtual_server 10.0.0.100 80 {
    delay_loop 6
    lb_algo wrr
    lb_kind DR
    persistence_timeout 50
    protocol TCP

    real_server 10.0.0.21 80 {
        weight 1
        TCP_CHECK {
            connect_timeout 3
            nb_get_retry 3
            delay_before_retry 3
            connect_port 80
        }
    }
    real_server 10.0.0.22 80 {
        weight 1
        TCP_CHECK {
            connect_timeout 3
            nb_get_retry 3
            delay_before_retry 3
            connect_port 80
        }
    }
}

逐项解释:

  • lb_algo wrr:加权轮询(Weighted Round Robin),按后端权重分配流量。权重高的机器接更多请求,适合新旧机器混布的过渡期。
  • lb_kind DR:指定 DR 模式,这是和前面拓扑对应的关键参数。
  • persistence_timeout 50:会话保持 50 秒。同一个客户端 IP 在 50 秒内的请求都发往同一台后端,对没有做会话共享的应用很重要。但如果后端已经用 Redis 共享了 session,这个值可以设成 0 关闭,让流量分配更均匀。
  • TCP_CHECK:健康检查,连不上后端的 80 端口就把它踢出集群;恢复了自动加回来。

加载后查看规则是否生效:

ipvsadm -Ln

应该看到 VIP 下面挂着两个 real server,前面标着 Route(DR 模式)。

第四步:后端为什么要绑定 VIP 到 lo

这是 DR 模式最反直觉、也是新手最容易漏掉的一步。DR 模式下后端收到的是目的 MAC 是本机、但目的 IP 仍然是 VIP 的数据包。如果后端内核发现 VIP 不在自己的网卡上,它会直接丢掉这个包——因为 Linux 默认丢弃"目的地址不是本机"的包(这种行为由 arp_ignore 和 rp_filter 控制)。

所以后端必须把 VIP 绑到本地回环接口上,但——又不能真的对外宣告这个 IP,否则 ARP 广播会冲突。

正确的做法是在后端执行:

# 抑制 ARP 响应和宣告
echo 1 > /proc/sys/net/ipv4/conf/lo/arp_ignore
echo 2 > /proc/sys/net/ipv4/conf/lo/arp_announce
echo 1 > /proc/sys/net/ipv4/conf/all/arp_ignore
echo 2 > /proc/sys/net/ipv4/conf/all/arp_announce

# 把 VIP 绑到 lo,注意掩码必须写 32 位,避免产生广播路由
ip addr add 10.0.0.100/32 dev lo label lo:vip

解释这两个内核参数:

  • arp_ignore=1:只有当 ARP 请求的目标 IP 正好落在收到请求的那个网卡的地址上时才回应。这样 lo 上的 VIP 不会响应来自 eth0 的 ARP 询问。
  • arp_announce=2:发送 ARP 请求时,一律使用最合适的本地地址作为源地址,绝不拿 lo 上的 VIP 去宣告。

这两个参数是 DR 模式的命根子。如果漏了,两台后端的 VIP 会互相抢答 ARP,导致交换机 MAC 表被刷乱,客户端流量直接跑到后端去了,调度器形同虚设,现象就是"配置看起来全对,但流量分配完全乱套"。

把这些写进开机脚本,避免重启后失效。可以放进 /etc/rc.local,或者写一个 systemd oneshot 服务。这里给一个简单的 systemd 单元 /etc/systemd/system/lvs-rs.service:

[Unit]
Description=Configure LVS Real Server VIP on lo
After=network.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/lvs-rs-up.sh

[Install]
WantedBy=multi-user.target

对应的 /usr/local/bin/lvs-rs-up.sh 就是把上面那几行 echo 和 ip addr add 包起来,加个幂等判断:

#!/bin/bash
VIP=10.0.0.100
if ! ip addr show lo | grep -q "$VIP"; then
    echo 1 > /proc/sys/net/ipv4/conf/lo/arp_ignore
    echo 2 > /proc/sys/net/ipv4/conf/lo/arp_announce
    echo 1 > /proc/sys/net/ipv4/conf/all/arp_ignore
    echo 2 > /proc/sys/net/ipv4/conf/all/arp_announce
    ip addr add $VIP/32 dev lo label lo:vip
fi

五、验证与常见的三个坑

配置完成后按顺序验证:

  • 客户端连续请求 VIP,观察后端访问日志是否两台都有记录,且比例接近权重。
  • 把其中一台后端停掉,观察是否在健康检查周期内被踢出,请求全部转到另一台。
  • 把主调度器停掉,观察 VIP 是否在 3 秒内漂移到备,且服务不中断。

三个高频坑位:

  1. 忘了改 arp_ignore / arp_announce,导致后端抢答 ARP。现象是流量乱窜,排查手段是 arping 看谁在应答 VIP,或者抓包看 ARP 回复来自哪台。
  2. 防火墙拦掉了 VRRP 协议。Keepalived 之间用的是 IP 协议号 112(不是 TCP/UDP 端口),iptables/nftables 里如果做了严格的协议白名单,必须放行 ip protocol vrrp,否则心跳收不到,直接双主(两边都持有 VIP,灾难现场)。
  3. 后端还把 VIP 的网关指向了别处。DR 模式下后端处理完响应直接用 lo 上的 VIP 作源地址回给客户端,不需要经过调度器,所以后端的默认网关保持正常网络配置即可,千万不要为了"对称"去改网关。

真实场景复盘:一次 ARP 引发的流量错乱

我曾经在一个客户的电商项目上遇到过典型案例。他们上线了 LVS + Keepalived,配置表看起来完全正确,ipvsadm 规则也在,但监控显示流量几乎全压在一台后端上,另一台几乎空转,而调度器的转发计数增长得又特别慢。

排查路径是这样的:先确认 ipvsadm 规则,正常;再抓调度器出口流量,发现转发量确实很小,说明大量请求根本没走调度器;最后用 arping -I eth0 10.0.0.100 探测 VIP,发现应答的 MAC 竟然是两台后端的网卡,而调度器的 MAC 根本不在应答列表里。根因就是后端漏配了 arp_announce,两台后端各自在宣告 VIP,交换机的 MAC 地址表被反复刷新,客户端 ARP 缓存里 VIP 直接映射到了后端的 MAC。

修复动作很简单:在后端补上那四条 arp 参数并重启网络,问题立刻消失。但代价是这次故障已经持续了三天,期间流量分配严重不均,其中一台后端长期过载。教训很明确——DR 模式里,ARP 参数的优先级高于一切应用层配置,它不出现在任何一行 load balancer 配置里,却是整个方案能否工作的地基。上线前的验收清单里,一定要有一项是"从客户端 arping VIP,确认只有调度器单方面应答"。

总结

LVS + Keepalived 的组合看起来比 Nginx 复杂,但它解决的是 Nginx 解决不了的问题:内核态四层转发带来的极致吞吐、几十万并发连接的承载力、以及不依赖应用层解析的极致效率。DR 模式是其中最值得掌握的方案——响应流量绕开调度器、性能损耗最小。

记住四个要点:调度器和后端必须同网段(二层可达)、VIP 只由 Keepalived 在调度器上宣告、后端把 VIP 绑到 lo 且必须收口 ARP、健康检查要覆盖到后端真实服务而不只是端口存活。把这四点做对,你的服务就能在单台后端故障时继续服务,在单台调度器宕机时几秒内自动接管——这才是真正意义上的高可用。

Last modification:October 9th, 2026 at 01:24 pm

Leave a Comment