大概在两三年前,我帮一个朋友排查线上网关的问题:后端几十台实例几乎全部处于忙碌状态,可前置的负载均衡层CPU负载却始终占不满。讨论一圈才确认,朋友用的是一台跑着LVS类方案的机器,流量入口走的是Linux内核里的IPVS转发框架。那时我才意识到,今天还能在各种架构图里看到“LVS负载均衡”这个名词,不是因为它老,而是因为它在四层转发这件事上的效率,确实没有几个方案能比得上。这篇文章我会系统拆解LVS的核心原理:三种工作模式到底差在哪里、调度算法各自适合什么场景、keepalived怎么配合它做高可用集群,以及我在生产环境里反复踩过、也帮别人处理过的那些坑。对正在做集群架构选型、或者已经被LVS绕晕的研发和运维同学,这篇应该能直接拿来当参考。
1. 先把位置摆正:LVS是内核里的转发框架,不是跑在用户态的代理程序
1.1 一个最常见的误解:为什么找不到LVS进程
接触LVS后最容易被绕晕的地方,就是“进程去哪了”。按常识,运行一个负载均衡软件,至少会有一个进程挂在那里,比如开源七层代理会有自己的工作进程,监听端口、接受连接,一眼就能看到。但LVS完全不一样:转发逻辑在Linux内核的IPVS模块里,它平时不占用固定端口,也不会常驻一个用户态进程。你执行ps -ef看不到它,执行netstat -lntp也看不到它,于是很多刚上手的人会以为自己没装好。
实际上,工作正常的LVS集群,你唯一能通过交互看到的东西,是用ipvsadm命令刷出来的规则和统计信息。ipvsadm是用户态管理工具,它通过netlink接口把配置下发到内核,内核里的IPVS框架真正负责转发。理解了这个分层关系,后面所有排查思路才能建立起来。LVS不是“一款软件”,更像“内核里的一个功能开关”,配合外部工具去驱动它。
1.2 一条数据包在IPVS内部是怎么走的
当客户端请求到达LVS服务器的网卡,经过链路层接收后会进入netfilter框架的钩子点,IPVS模块就在这些钩子点注册了自己的处理逻辑。处理流程可以简化成三步:先看数据包的目的IP和端口是否匹配某个虚拟服务;匹配成功后就查连接跟踪表,如果这条连接已经存在,就按表项记录直接改包转发;如果是一条全新连接,就用配置好的调度算法选一台后端真实服务器,把新连接信息写进连接跟踪表,再执行转发。
这里说的“改包”,在不同工作模式下改的位置不一样。NAT模式修改目的IP,DR模式修改二层目的MAC,TUN模式则是在IP层再做一次封装。但不管改哪里,整个过程都在内核态完成,没有用户态和内核态来回切换的额外开销,也没有HTTP解析、日志写入这类高消耗动作。这就是LVS在四层转发上效率极高的根本原因。
1.3 LVS和七层代理之间,实际的边界在哪里
我之前参与过的一次压测,能很直观地说明这个问题。某个业务入口用开源七层代理扛流量,压力一起来,CPU先到顶,因为每个请求都要经历用户态和内核态的多次切换,还要做HTTP头部解析、连接管理、超时处理。后来把入口改成LVS做四层转发,后端RS几乎没变,前置机CPU占用率立刻掉了一大截。
七层代理擅长的是“看懂请求内容”:按URL、Cookie、Header做路由,做鉴权、改写、限流。LVS完全不在乎这些,它只关心四层以内的信息——IP、端口、协议类型。所以边界很清楚:需要精细控制请求,用七层;需要扛极端并发、海量新建连接、对协议透明,用四层。很多大型系统实际是两层叠加,LVS在入口先把大部分流量按IP分给后面的七层集群,再由七层做更细的转发。
1.4 高可用不是LVS自带的能力,别混淆keepalived的职责
还有一个概念要提前捋清楚:LVS本身只提供转发机制,不提供健康检查,也不提供虚拟IP自动漂移。生产环境里常说的“LVS高可用”,其实是叠加了keepalived实现的。keepalived借助VRRP协议让主备两台机器共享同一个VIP,主节点故障时备用节点接管VIP,同时由它对后端RS做健康检查,把故障机器从转发列表里自动摘除。明白了这个架构,就不会在排查问题时找错方向:LVS负责数据面,keepalived负责控制面。
2. DR、NAT、TUN三种模式的选择:核心是流量回程成本放在谁身上
2.1 先分清“进”和“回”两条路径
评估负载均衡方案时,很多人只盯着请求进来那一段,却忽略了响应返回这一段。一个完整的TCP会话有两条路径:客户端到LVS再到后端RS的“进向路径”,以及RS处理完响应后回到客户端的“回向路径”。不同工作模式最大的差异,其实就在于回向流量由谁来承载。
一般来说,谁承载回向流量,谁就是系统最大的瓶颈。如果回向流量也要经过负载均衡器,那么负载均衡器的带宽、连接表容量、CPU处理能力都会被放大一倍的消耗。理解了这一点,再看三种模式的取舍就会很清晰。
2.2 NAT模式:支持端口变化,但回程必须绕回LVS
NAT模式的转发逻辑比较直观:客户端请求到达LVS后,LVS会把目的IP从VIP改成后端RS的真实IP,同时可能做源地址转换,把源IP改成LVS自己的内网IP。RS处理完请求后,把响应包回给LVS,LVS再把源IP改回VIP,送回客户端。
这种模式最大的特点,是支持端口映射。虚拟服务的端口和后端RS的端口可以不一致,因为LVS在转发时会把端口一并改写。这在某些需要兼容旧服务端口的场景里很好用。
但代价同样明显:所有回向流量都必经LVS。假设客户端下载一个大文件,LVS不仅要在进向承担请求,还要在回向承载几乎等于文件体积的响应流量。流量一大,LVS自身的网卡带宽、连接跟踪表、CPU都会先被压垮。所以NAT模式适合后端RS数量不多、整体吞吐量可控、需要端口映射或后端网络隔离的场景;一旦规模上去,第一个该换掉的就是它。
2.3 DR模式:改的是MAC,出包直接从RS回客户端
DR模式的做法,是把数据包的二层目的MAC改成后端RS网卡的MAC,源IP、目的IP都不动。RS收到包后,发现目的IP是自己在回环接口上配置的VIP,于是正常接收并处理,然后把响应包通过自己的物理网卡直接发给客户端,完全不经过LVS。
这就是DR模式性能高的根本原因:进向流量压在一个点上,回向流量分散到所有RS各自承担,LVS的带宽压力一下子少了一半。因此DR模式是三类模式里生产环境用得最多的。
但它有两个硬性约束。第一,LVS和RS必须在同一个二层广播域内,因为MAC地址转发依赖ARP,出了二层就找不到RS的MAC。第二,RS必须愿意接收目的IP是VIP的包——需要在每台RS的回环接口上绑一个VIP,同时要抑制RS对外应答VIP的ARP请求。否则整个网络里会有多台机器声称自己拥有这个VIP,ARP表一乱,转发就彻底没法看。
2.4 TUN模式:IP层再套一层,解决跨网段问题
TUN模式的做法,是把原始数据包整体封装进一个新的IP包里。外层包的目的IP是后端RS的真实IP,内层包保留原来的客户端IP和VIP。RS收到外层包后解开封装,取出内层包正常处理,响应包直接通过自己的路由发回客户端,不回LVS。
因为不依赖MAC转发,TUN模式天然支持跨网段,后端RS甚至可以分布在不同机房。这算是它和DR模式最大的区别。
但TUN要付出额外代价:后端内核需要支持隧道封装模块;封装会带来额外的IP头部开销,可能导致整体包长超过MTU,一旦触发分片,性能会明显劣化;而且封装和解封装虽然在内核完成,毕竟比直接改MAC多了一层处理。实际生产里,除非有明确的跨机房分布式需求,否则大多数团队还是优先选DR,把TUN留给比较特殊的场景。
2.5 三种模式的选型对照
| 模式 | 转发手段 | 回程是否经过LVS | 端口映射 | 网络要求 | 典型场景 |
|---|---|---|---|---|---|
| NAT | 修改目的IP/端口 | 是 | 支持 | 可跨网段 | 后端数量少、需要改端口、后端网络隔离 |
| DR | 修改目的MAC | 否 | 不支持 | 同二层广播域 | 高并发同机房,生产中最常见 |
| TUN | IP封装 | 否 | 不支持 | 可跨网段 | 跨机房、后端RS规模很大 |
从这张表能看出一个规律:越往后,回程越轻,转发效率越高,但对网络环境的约束也越特殊。没有哪种模式是绝对碾压的,一切取决于你的后端RS能不能满足对应的网络前提。
3. 调度算法选型:每一种都在和“连接状态”博弈
3.1 先看懂IPVS的连接表
LVS能支撑高并发,除了转发路径短之外,还靠一张庞大的连接跟踪表。每一个新建连接进入时,IPVS都会查表:如果表里已有这条连接的记录,就直接按记录继续转发;如果不存在,才调用调度算法选一台RS,并把连接关系写进表里。
这个机制决定了调度算法只作用在“新建连接”的一瞬间。一旦连接被建立,后续所有数据包都按表项走,不会再重新调度。所以算法好不好,核心看它在短时间内处理大量新连接时,能不能把负载相对均匀地分出去。
3.2 轮询与加权轮询:简单可控,但前提是后端足够匀称
轮询(rr)就是一台接一台地轮流分配,加权轮询(wrr)在后端权重不同的情况下,按比例分配新连接。这类算法逻辑最简单,调度开销也最小。
适合的场景,是后端RS规格基本一致、请求处理时间接近、连接存活时间不悬殊。比如都是同配置的云主机,跑的都是无状态短接口,rr或wrr就足够用了。但要注意,加权轮询里的权重怎么定有讲究。很多人按内存大小配权重,实际上对负载均衡来说,CPU核数、网卡带宽、业务主要消耗的资源类型才是更关键的参考指标。权重配错了,高配机器闲死,低配机器忙死的情况我见过不止一次。
3.3 最少连接与加权最少连接:默认选择,但也最容易被误读
最少连接(lc)会挑当前连接数最少的那台RS,加权最少连接(wlc)再加一层权重计算,用连接数和权重的比值做比较。IPVS的默认调度算法通常就是wlc,这也是很多人一上来就在用的配置。
但“连接数最少”不等于“负载最低”。举个反例:假设RS-A上挂了10条连接,每条都是在处理一个需要几十秒的慢查询;RS-B上挂了20条连接,但每条都是毫秒级返回的短事务。按连接数看,RS-A更空闲,实际业务压力却可能比RS-B大得多。所以wlc适合大量短连接、请求处理时间相对均衡的场景;如果业务里长连接和慢请求占比高,只看连接数会失真,不如直接用加权轮询来得可控。
3.4 哈希类算法:源地址哈希和目标地址哈希的取舍
源地址哈希(sh)会把来自同一个源IP的请求固定分配到同一台RS。这种策略天然实现了会话保持——同一个用户总是落在同一台机器上,不需要额外的中间存储来记录会话归属。
目标地址哈希(dh)则是按目的IP做哈希,通常用于正向代理或缓存类业务,让同一个目标地址的请求集中到固定RS,有利于提升本地命中率。
哈希类算法的隐患在于,一旦RS列表发生变化,哈希映射会重新分配,大量存量连接可能被整体打散。如果业务无法容忍这种情况,要配合VIP的持久性参数来做粘滞,给连接留出足够的存在时间,而不是频繁变更RS列表。
3.5 局部性算法和延迟类算法:知道存在就好,别轻易上用
lblc(基于局部性的最少连接)和lblcr(带复制的基于局部性最少连接)是面向Cache类场景设计的,目标是尽量把同一目标IP的请求调度到最近使用过的那台RS,提升缓存命中率。sed(最少期望延迟)、nq(不排队)等算法会结合服务器权重和当前连接数做更复杂的估算。这些算法在特定场景确实有设计上的合理性,但生产环境里配置复杂、行为不如前几类可预期,我一般不建议轻易使用。真正跑得最多的,还是wlc、wrr、sh三种。选型可以参照下面的思路:
| 场景 | 推荐算法 | 原因 |
|---|---|---|
| 后端规格一致、请求简短 | rr / wrr | 简单可控,调度开销小 |
| 后端规格不一致、短连接为主 | wlc | 能动态平衡负载 |
| 长连接、需要会话保持 | sh + persistence | 同一来源始终落同一台 |
| 缓存类业务 | dh / lblcr | 提高本地命中率 |
4. keepalived + LVS:把单点入口变成可切换的高可用集群
4.1 为什么单台LVS注定扛不起生产环境
LVS本身的转发能力再强,也只是单点。它所在的服务器一断电、一断网,整个入口就瘫了。没有高可用方案的话,恢复流程只能靠人工改IP或者重新配置,业务中断时间以小时为单位,这是生产环境绝对不能接受的。
keepalived解决的就是这个问题。它通过VRRP协议,让两台或更多LVS节点共享同一个VIP:正常情况下VIP落在主节点上,主节点故障后,备用节点会自动抢占VIP。对客户端来说,访问的还是同一个IP,完全感知不到后端的切换。
4.2 一份可以直接改用的最小配置
下面是一份主节点侧的keepalived配置,结构清晰,可以直接替换IP改用于自己的环境:
global_defs { router_id lvs_master } vrrp_instance VI_LVS { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 192.0.2.10/24 } } virtual_server 192.0.2.10 80 { delay_loop 6 lb_algo wlc lb_kind DR protocol TCP real_server 192.0.2.11 80 { weight 1 TCP_CHECK { connect_timeout 3 nb_get_retry 3 delay_before_retry 2 } } real_server 192.0.2.12 80 { weight 1 TCP_CHECK { connect_timeout 3 nb_get_retry 3 delay_before_retry 2 } } }备用节点的配置基本一样,区别在于state改成BACKUP,priority设低一些,比如95。这里有几个值得注意的细节:virtual_router_id在主备两台机器上必须一致;auth_pass也要求一致;interface要确认填的是承载VIP网段的实际网卡名,填错了VIP根本起不来。
4.3 健康检查的配置细节:TCP握手成功不等于服务正常
配置里的TCP_CHECK是keepalived最常见的健康检查方式,原理就是做一次TCP三次握手。握手成功,就认为后端RS是健康的。这种检查成本低、不会干扰业务,但只能证明端口还在监听,证明不了应用真的能正常处理请求。
如果业务是HTTP服务,更合适的探活方式是HTTP_GET,让keepalived发一个HTTP请求去匹配状态码:
HTTP_GET { url { path /healthz status_code 200 } connect_timeout 3 }这里的connect_timeout和nb_get_retry配置要小心。设得太短,后端稍微响应慢一点就会被误判成故障,在业务高峰期误摘会导致流量雪崩;设得太长,后端真的挂了也要等很久才能摘除,故障时间被拉长。一般connect_timeout用3秒、重试3次、间隔2秒是比较常见的起点,后续根据业务耗时分布再微调。
4.4 高可用是否生效,至少要从四个角度验证
配置改完,不要急着觉得万事大吉。我习惯按下面四个步骤做验证:
- 在主机上执行
ip addr show eth0,确认VIP绑定正常;在备机上确认同一时刻看不到VIP。 - 执行
ipvsadm -L -n --stats,确认转发规则在、每个RS的连接统计在涨。 - 停掉一台后端RS,观察它是否在
delay_loop周期内从转发列表消失,重新启动后再观察它是否自动回来。 - 模拟主节点故障,比如停掉keepalived服务,确认VIP能自动漂移到备用节点,且业务流量不中断。
这套流程走下来,比任何配置截图都让人放心。
5. 生产环境翻车实录:那些几乎每个LVS集群都会遇到的细节
5.1 DR模式第一坑:RS抢答VIP的ARP请求
DR模式下最常见的问题,就是RS对外响应了VIP的ARP请求。后果是网络上对VIP的ARP解析出了多台机器,发给LVS的流量被RS“抢答”走,连接异常、丢包、不通的问题会一起冒出来。
解决办法是,在每台RS的回环接口上绑定VIP,同时抑制ARP应答。基础配置是这样的:
ip addr add 192.0.2.10/32 dev lo # /etc/sysctl.conf net.ipv4.conf.all.arp_ignore=1 net.ipv4.conf.all.arp_announce=2 net.ipv4.conf.lo.arp_ignore=1 net.ipv4.conf.lo.arp_announce=2arp_ignore=1表示只回答目标IP是本接口IP的ARP请求,其他接口一律不答;arp_announce=2表示尽量使用接收ARP请求的那个网卡的最优地址来应答,避免把lo:0上的VIP暴露出去。改完记得sysctl -p生效。还有一个细节容易忽略:RS上VIP的掩码一定写成/32,写成/24之类的掩码,会导致路由表异常,后续排查能把人折腾疯。
5.2 后端长连接与优雅摘除:改配置不等于断连接
LVS对已经建立的连接是“懒管理”的。keepalived把某台RS从转发列表里摘除,只是让新连接不再调度过去,已经建立的TCP连接会继续存在,直到连接自然结束或被业务方主动关闭。这个特性在维护长连接服务时特别容易出状况——你明明已经把RS下线了,业务侧却还在持续报错,因为存量连接还在往那台机器上打流量。
更稳妥的做法是先摘“流量”再摘“机器”。通过ipvsadm -e把目标RS的权重改成0,让新建连接不再进来,等存量连接全部结束后再关停服务。很多有经验的运维会把这个动作写成交替脚本,配合业务方的连接优雅关闭机制,避免直接kill进程造成请求被硬切。
5.3 连接超时参数:短连接高压下的隐性瓶颈
IPVS的连接跟踪表是有超时时间的,默认值在某些场景下偏长。比如大量短连接业务里,一个请求几百毫秒就结束了,但表项可能还要保留很长时间。连接数一多,无意义残留会把整张连接表占满,新连接反而建不进来。
可以通过ipvsadm -L -n --timeout查看当前超时参数,必要时调整内核参数。比如net.ipv4.vs.timeout_tcp、net.ipv4.vs.timeout_tcp_fin、net.ipv4.vs.timeout_udp这些值。调低空闲超时能释放表项,但切忌盲目压得特别低,否则还在正常传输的大流量连接可能被误回收,表现为莫名其妙地断流。我一般会先观察业务连接存活时间分布,再按P99的值留出余量来设定。
5.4 网卡多队列与软中断:压测起来问题往往出在包接收上
LVS对CPU逻辑计算的消耗不高,但网卡收包和发包会触发软中断。如果Linux默认把所有中断都压到0号CPU上,流量一旦上来,单核软中断占比会拉满,业务看着没跑满,吞吐却上不去了。
可以给多队列网卡开启RSS,让不同队列的中断分散到不同CPU核心;硬件不支持多队列的,可以配置RPS(Receive Packet Steering),把收包软中断分摊到多个CPU。另外,irqbalance服务通常建议保持开启,让它自动平衡中断,比人工指定一两个CPU省心得多。压测验证时,用top按1看每个核心的软中断占比,如果只有一个核红了,基本就是这个环节的问题。
5.5 变更前留一份“正常状态”快照
最后分享一个我自己的习惯:任何LVS或keepalived变更之前,先执行一次ipvsadm -Ln > /tmp/ipvs_before.txt,把规则和权重快照留存。改动出问题时,拿快照一对比,立刻能看出哪里被改坏了。这个操作不花时间,但真到了凌晨两点排查故障的时候,它就是最踏实的后手。
如果把这套方案从头再搭一次,我自己会把重点放在这几件事上:先确认DR模式下RS的ARP抑制参数,再配keepalived的健康检查和VIP漂移,最后在压测阶段看网卡软中断和连接表增长情况。核心转发逻辑本身不复杂,真正决定集群稳不稳的,往往是这些藏在转发之外的细节。希望这篇详细拆解,能让你少走点弯路,也少熬几个夜。