再谈负载均衡:从轮询到等开销,四层与七层的工程实践

发布时间:2026/9/7 14:30:26
再谈负载均衡:从轮询到等开销,四层与七层的工程实践 很多人对负载均衡的理解还停留在“把请求分发到几台服务器上”觉得这事简单。真正做过线上系统的朋友心里都清楚负载均衡是一个贯穿网络协议栈、操作系统、业务架构、容量评估的交叉地带很多问题不压测到深夜根本发现不了。这篇文章没有入门科普的意思重点聊几个容易被想当然的环节轮询为什么不等于均衡四层和七层的真实边界在哪里等开销负载均衡的成本模型怎么落地以及会话保持、故障转移、回源过载这些下游长尾问题到底怎么处理。适合已经上手过Nginx、LVS、云负载均衡但还想把背后机制弄得更透彻的人。一是负载均衡到底在“均衡”什么轮询不是平均看见这个标题有同学可能会笑——轮询当然是把请求均匀分给每台机器平均得不能再平均了。但你把指标拆开看就会发现“分发均匀”和“压力均衡”是两回事。1.1 请求次数均衡不等于负载均衡一个典型的反例是你的服务有4个节点前面用Nginx默认的round-robin按请求数来看每个节点都收到了大致相同的请求量但压测一跑其中一台机器的CPU已经90%另一台才40%。为什么会这样原因在于每个请求的开销是不一样的。有的接口是纯内存操作几毫秒就返回了有的接口要查大表、做复杂计算、调下游RPC可能要几百毫秒。假设每个节点处理能力的差异是1:2:1:2但请求按1:1:1:1分发过去重接口多打到弱节点上整个集群的吞吐就被那个最慢的节点卡住了。这就是轮询策略最典型的盲区它在请求维度平均分配但根本没有感知每个请求的真实成本也没感知每个节点的实时容量。1.2 均衡的四个维度别只盯着QPS做负载均衡的人至少需要关注四个统计维度请求数QPS、连接数、每秒字节数、以及节点资源利用率。四层负载均衡如LVS工作在内核态看到的主要是连接和字节天然适合按连接数和流量做分发七层负载均衡如Nginx、HAProxy能看到HTTP请求本身可以做更细粒度的“按请求”分发。但无论哪个维度本质上都是用一个可观测的指标去近似“真实负载”。真正的负载是CPU、内存、IO、线程池排队长度、GC停顿这些综合出来的东西这些在负载均衡层通常很难直接感知。所以后文要谈的等开销负载均衡说到底就是想办法让“负载均衡层能观测到的指标”和“后端节点的真实成本”之间建立起一个尽量准确的映射关系。1.3 一个容易被忽略的维度连接数量的雪球效应如果你用的是短连接模式每个请求都新建一个TCP连接那么连接数大致和QPS成正比问题不大。但如果你在负载均衡层开了连接复用keepalive或者后端服务对并发连接数有上限约束那么“连接数”这个维度就变得极其敏感。我踩过这样一个坑有个Java服务用了Tomcat默认maxConnections是8192流量高峰时单个节点连接数逼近上限新请求排起了长队。负载均衡是按照QPS分发的从请求数看各节点挺均匀但实际上有问题的那个节点已经“半死”状态——TCP连接全部占满请求进来全在队列里等着。这时候负载均衡层如果不感知连接数还会继续往里打流量故障就被放大了。后来在Nginx upstream里加了max_conns限制同时把负载均衡算法从round-robin改成了least_conn这个雪球才算止住。这个教训说明选哪个统计维度做分发一定要和你后端服务的瓶颈特征对上。二是四层与七层的选择边界从LVS、Nginx到F5每次聊负载均衡都绕不开四层还是七层这个问题。很多文章把四层和七层的区别归纳成“四层快但功能少七层慢但功能多”这么说大方向没错但真正的决策点在细节里。2.1 四层负载均衡在做什么四层负载均衡工作在传输层它看到的是TCP/UDP报文不会去解析HTTP头、URI、Cookie这些内容。LVSLinux Virtual Server是最典型的开源方案DR模式尤其常用客户端请求先到LVSLVS把MAC地址改写后直接转给后端节点后端节点的回包绕过LVS直接发给客户端。因为这个“请求进负载均衡、响应不走负载均衡”的非对称特性LVS DR模式的处理能力可以做得非常高转发面几乎不受大流量响应影响。但四层方案也不是没有代价。它的转发基于IP和端口意味着它无法根据URL路径做路由也无法真正理解HTTP层的会话状态。很多云厂商的SLB四层模式本质上是把公网IP的流量通过隧道或NAT方式转发到后端同样遵循“不解析七层内容”的原则。2.2 七层负载均衡的多出来的“本事”和开销七层负载均衡能看到HTTP协议内容因此可以做很多事按域名分流、按URI前缀路由到不同服务、读取Cookie实现会话保持、对响应做缓存、在均衡层做TLS终结统一管理证书、甚至基于Header或参数做灰度流量分配。这些能力对应的代价是CPU开销。HTTP解析、正则路由、TLS握手终结这些操作都需要消耗计算资源。尤其TLS终结如果QPS很高光握手和加解密就可能占掉负载均衡节点的大量CPU。我见过一个团队把所有HTTPS流量交给一台8核 Nginx去终结结果QPS只有两万多个CPU就吃满了。后来把TLS终结下沉到四层之后的节点或者换用支持硬件加速的网关问题才解决。2.3 F5这类商业设备到底值不值得上热搜词里出现了F5代表很多人对商业负载均衡设备有好奇心。F5的优势在于硬件转发性能强、四层并发处理能力高、配置管理界面成熟、内置各种应用模板、支持脚本化管控。在金融、运营商这类对可用性极其敏感的场景F5依然是常见选择因为它经过了大量真实生产环境的验证出了问题可以找原厂兜底。但从成本角度讲F5设备动辄几十万起步而且硬件设备的扩缩容不可能像软件方案那样灵活。你如果用Nginx集群或者云厂商的负载均衡产品已经把可用性做到99.99%F5的增量价值其实没有想象中那么大。我的观点是商业硬件适合“预算充足、合规要求高、运维人力少”的场景对于大多数互联网团队软件负载均衡加上良好的健康检查、冗余部署、容量规划完全可以达到同样的可用性目标。2.4 一张表说清楚选型边界为了让这部分更直观我把常见的选型判断整理成一张表评估维度四层负载均衡七层负载均衡选择倾向转发性能高内核态转发中需协议解析高吞吐纯TCP往四层走路由粒度IP 端口URL / Header / Cookie需要路径分流选七层TLS终结不支持或能力弱支持集中管理证书想省后端CPU选七层会话保持靠源IP哈希较粗Cookie注入精准移动端动态IP场景选七层Cookie灰度发布基本不支持支持Header/Cookie分流有灰度需求必须七层典型方案LVS、云厂商四层SLB、F5Nginx、HAProxy、云厂商七层SLB两者常配合组成两级架构生产环境里最常出现的架构是两级外层用四层负载均衡承接海量TCP连接转发给内层七层集群内层Nginx/HAProxy做精细化路由、TLS终结、请求改写再把流量打到业务节点上。这个金字塔结构既兼顾了性能和灵活性也是大规模系统的标准解法。三是等开销负载均衡从“平摊请求”到“成本感知”等开销负载均衡是热搜词里最能体现深度的一个概念。直译过来就是“让每个请求在后端节点上的开销相等”这是一个比“平均分发请求数”高级得多的目标。3.1 什么是“开销”怎么度量它说到开销首先需要一个可度量的成本函数。对于一个典型的Web服务单个请求的成本至少与以下因素相关请求类型读接口、写接口、批量接口的耗时差异巨大数据规模同一个接口传10条数据和1万条数据是完全不同的成本下游依赖是否调用数据库、缓存、外部HTTP服务以及这些依赖的延迟节点状态当前CPU占用、内存使用率、GC暂停时间、线程池排队长度等开销负载均衡要做的事就是用一个反馈机制让负载均衡器根据后端节点的实时表现动态调整分发给每台机器的请求数或权重让所有节点的“真实压力”尽量接近。3.2 最简单实用的实现基于延迟或队列长度的动态权重最朴素的等开销算法长这样每个后端节点周期性上报自己的“开销信号”比如平均响应时间、活跃连接数、或者线程池队列深度负载均衡器收到开销信号后计算节点的权重开销越小的节点分到越多请求。公式可以很简单weight_i base_weight_i / (1 alpha * load_i)其中load_i是节点i的归一化开销信号比如CPU使用率40%就是0.4alpha是一个反馈强度系数决定负载对权重的影响力度。alpha太大会导致权重在短时间剧烈抖动太小则追踪不到压力变化需要在实际压测里慢慢调。这个思路和TCP拥塞控制的加法增大乘法减小类似本质是一个负反馈系统——节点越闲分到的流量越多节点越忙流量自动减少。在生产环境中用的时候要给权重变化加上平滑和上下限避免某个节点突然被打死。比如weight_i clamp(weight_i, min_weight, max_weight)3.3 工程落地时一定要处理的三个问题第一个问题是采样窗口的选择。开销信号不能太实时否则受瞬时抖动影响很大也不能太滞后否则节点都挂了你还不知道。我的经验是CPU采样窗口取5到10秒队列深度或RT采样窗口取1到2秒相对靠谱具体得结合节点的压测表现来定。第二个问题是避免同步共振。如果所有节点都在同一时刻上报开销信号负载均衡器又在同一周期重新计算权重就可能出现“集群集体震荡”——一会儿全打给节点A一会儿全打给节点B。解决办法是每个节点上报时加一个随机相位偏移或者负载均衡器在计算权重时做指数平滑smooth_load_i 0.7 * prev_smooth_load_i 0.3 * current_load_i第三个问题是要给异常节点单独处理。开销信号为0或者无穷大都是异常状态这时不能简单地按公式计算而要进入隔离流程要么标记节点状态为“冷却中”过一段时间再探测要么暂时把它的权重降到底线只留极小比例健康检查流量。3.4 等开销算法在真实业务中的表现我之前在一个日活过百万的电商后端试过这套方案。当时的痛点是大促期间热门商品接口打了某个分片导致集群里两台节点CPU飙到85%另外两台还在40%。用轮询完全没感知用最少连接数效果也一般因为每个接口RT差别太大连接数无法反映真实成本。后来按“RT指数移动平均 CPU使用率”组合加权大促高峰时最忙和最优闲节点的CPU差距缩小到10个百分点以内整体吞吐直接提升了大约30%。这个数字当然有场景特殊性但至少证明了一点只要后端请求成本方差大等开销的思路就一定会比轮询强。如果所有请求成本几乎一致比如纯静态内容服务轮询已经足够没必要引入复杂度。四是会话保持与故障转移两个最容易想当然的环节负载均衡的职责不只是分发请求还要在节点出问题、用户会话需要连续的时候作出正确决策。这两个环节特别容易出问题因为它们的判断逻辑不是“谁的负载低就选谁”而是“谁符合业务约束”。4.1 会话保持的三种实现方式与取舍会话保持本质上是让同一个用户的多次请求打到同一台后端节点上。如果是本地Session方案不在负载均衡层做会话保持用户请求被分到另一台节点就重新登录了。实现方式通常有三类源地址哈希是最简单的把客户端IP用哈希函数映射到固定节点。代价是多用户共享NAT出口时哈希会严重倾斜而且如果某个节点下线该节点上的大量会话全部失效因为哈希分布全变了。Cookie插入方式对用户透明度和均衡效果都比较理想负载均衡器给首次请求的响应设置一个带节点ID的Cookie后续请求根据Cookie路由。缺点是要求负载均衡层能解析和改写HTTP响应而且Cookie可能被用户删除。一致性哈希介于两者之间它把节点和请求都映射到同一个哈希环上节点变化时只影响环上相邻区间的一小部分请求极大地缩小了故障带来的会话失效范围。对缓存类业务比如Redis分片前置代理这是首选方案。4.2 故障转移不只是“换个节点再发一次”很多人以为健康检查失败后负载均衡器直接把流量切走就完事了。但实际故障转移涉及好几个容易被忽略的细节。第一个细节是连接耗尽。如果后端节点已经处于半死状态不会立刻拒绝TCP连接而是让新连接在accept队列里排队。此时负载均衡器虽然标记节点不健康但已经建立的连接还在继续发送请求。处理办法是在负载均衡层设置proxy_next_upstream或类似机制允许单次请求在超时/错误时重试到下一个节点但重试次数必须有限制否则故障节点会把所有请求拖死。第二个细节是TIME_WAIT。短连接模式下大量请求在故障转移时会在负载均衡节点上积累TIME_WAIT状态连接如果TIME_WAIT数量逼近上限新连接就无法建立。优化手段是开启tcp_tw_reuse、调大端口范围或者在负载均衡与后端之间走长连接复用。第三个细节是健康检查的探测请求不能太“温柔”。很多团队的健康检查就是一个TCP端口探测或者一个设计出来的/healthz接口。但生产环境有些节点只是业务能力下降比如缓存连接池泄漏、线程池卡死端口照样通、健康检查照样返回200。更可靠的做法是健康检查不仅探测存活还同步探测关键依赖列表项数据库延迟是否超阈值、消息队列积压是否过高、缓存是否可用。类似门卫不仅在门口看证件还要检查仓库货物是否正常才敢放行。4.3 “摘除”比“接入”更需要设计节点上线只要确认服务正常、流量慢慢打进来就行但节点下线的操作顺序一旦反了就会造成瞬间错误。标准的优雅摘除流程是在负载均衡层把节点标记为维护状态停止分配新请求等待一段超过最大请求时长的窗口比如30到60秒让已进入的请求自然结束确认节点活跃连接数降到0或者调用服务的/admin/drain接口做优雅停机最后才杀进程或关实例这个流程里的关键参数是“等待窗口”。窗口太短长请求被硬杀窗口太长发布效率低。我的经验是先看后端接口TP99延迟把等待窗口设为TP99的两倍以上基本能保证绝大多数请求安全结束。五是回源压力、热点与复杂故障负载均衡下游的长尾问题负载均衡层配得再好也只是把流量分到了后端。真正让系统崩溃的往往发生在负载均衡的“下游”。5.1 回源压力缓存击穿被负载均衡放大一个典型的场景是缓存击穿某个热点Key失效瞬间大量同质请求穿透缓存直接打到数据库。此时负载均衡依然在正常工作它甚至非常“公平”地把这些请求分发到了所有节点然而每台节点都在等着数据库返回线程池全部占满整条链路被打垮。这种情况下负载均衡层能做的不是隔离缓存失效而是在上游配合限流和熔断。最容易落地的方案是“单机限流 集群限流”的组合负载均衡层根据后端节点的容量做令牌桶限流超出的请求直接返回降级响应或排队等待不要让它们全部去打爆数据库。这里再次体现一个原则负载均衡器不只是分发器也是整个系统容量的守门员它的限流阈值必须根据后端水位实时调整。5.2 热点打满单节点等开销也救不了的场景有时候请求成本本身不均衡哪怕用上了动态权重还是有一台节点被打满——因为热点Key导致单台节点就是处理不过来。比如一个爆款商品的SKU信息所有用户都查它一致性哈希策略会把同一Key的请求都路由到同一个节点那台节点必然过热。处理手段通常是在负载均衡上层做拆Key、多级缓存、以及把热点读请求复制到多台节点。负载均衡本身能做的就检测到单节点开销持续偏高时暂时把热点流量做镜像或扩展副本。说得更直白一点负载均衡负责“分”热点治理负责“拆”两者必须配合光靠哪一边都解决不了流量集中问题。5.3 一次的真实排障过程从负载均衡日志找到故障源头有次线上告警“订单接口成功率从99.98%降到97%”团队首先怀疑负载均衡配置是不是有问题。登录Nginx一看各节点nginx error log里全是upstream timed out。从负载均衡日志能看到故障源头是特定的后端节点在特定时间窗口内响应速度变慢导致七层负载均衡等待超时。排查链路是这样的先看负载均衡日志定位是哪个upstream、哪个时间段出现超时比例高再登录对应后端节点看GC日志发现老年代GC特别频繁堆内存曲线接近天花板进一步分析dump文件定位到一个内存泄漏的缓存组件。整个过程里负载均衡日志和数据监控分别扮演了“侦察兵”和“验尸官”的角色。这个案例也说明学好负载均衡不只是调配置还要学会通过这些日志反推下游系统的健康状况。 日志的价值在于故障发生前它们是容量规划的输入故障发生时它们是快速定位的路标。5.4 全局负载均衡把“地方均衡”升级到“地域均衡”当一个系统发展到多机房、多地域部署时就绕不开全局负载均衡GSLB。它解决的不再是单机房内的请求分发而是让用户就近接入最近的机房并在机房故障时把流量切到其他地域。常用的方式包括DNS解析、HTTP重定向、以及Anycast路由。DNS方式最简单但缓存失效慢HTTP重定向灵活但多一次交互Anycast在网络层就近选路性能最好但部署门槛高。对于中等规模团队用云厂商的全局负载均衡产品加上合理的智能DNS解析已经足够覆盖绝大多数场景。核心规划原则是每个地域要有足够的冗余容量否则故障切换后把流量切到另一个机房那台机房的容量未必接得住。六是我的实测心得权重怎么调、压测怎么做、上线怎么验证写到这里前面几节的技术逻辑已经讲得比较完整了。这一节分享一些偏“手感和经验”的东西都是从压测和生产环境里摔打出来的体会。6.1 权重调整必须走“小步快跑”而不是一步到位有同学上线动态权重算法直接把参数从alpha0拉到alpha2结果流量在节点之间剧烈抖动RT掉了一截。问题出在反馈强度过高产生了系统振荡。正确做法是把alpha从0.1开始每压测一轮加一次观察CPU方差和RT是否同步下降。如果RT上升说明反馈过强降回上一档再观察。在权重参数上所有经验都指向同一个道理慢就是快你先让集群稳定再追求均衡效率。6.2 压测脚本要能模拟“成本不均匀”的请求很多团队的压测脚本用压测工具直接打同一套URL这测的是“均匀流量下的负载均衡”测不出等开销算法的优势。想验证动态权重是否有效压测流量必须和后端真实流量一样杂乱有快接口、有慢接口、有带大参数的请求甚至要有低概率的长尾请求。我在本地压测会用一个发压脚本混入三种请求类型比例按线上抓包统计出来这样压出来的数据才有参考价值。6.3 上线新负载均衡策略前至少要有三个画面新策略上线前我会先确认三个监控画面准备到位一是负载均衡层的节点流量视图每秒请求数、活跃连接数、权重变化曲线二是后端节点的资源视图CPU、RT、线程池队列、GC三是业务层的成功率视图。没看全这三块不要轻易把流量切上去。任何一次负载均衡策略调整都等于在线上动态改系统的流量分配规则必须像发布业务代码一样慎重——灰度、观察、回滚预案一个都不能少。6.4 关于“日志里看不到真相”的提醒最后分享一句压箱底的话负载均衡日志和数据指标能告诉你“哪里出了事”但很少能直接告诉你“为什么会出事”。真正的原因往往藏在后端节点里——JVM堆栈、数据库慢查询、网络重传、磁盘IO抖动。所以排查故障时别把所有精力都放在负载均衡配置上把一半时间花在顺着负载均衡日志找到后端节点、分析后端日志上这才是解决问题的有效路径。毕竟负载均衡是高速公路上的分流闸口远方的拥堵还得去拥堵点现场才能看清。说回“再谈负载均衡”这个话题我觉得它值得反复谈的原因恰恰是因为它在很多系统里无处不在但深度理解它的人远比想象中少。很多人以为配好了Nginx、BGP、SLB负载均衡就算完工了但实际上每一层配置都有它背后的假设和适用边界——轮询假设请求成本均匀最少连接数假设连接与成本成正比等开销算法假设你能准确探测成本。把这些假设在真实流量里一遍遍验证、调整、再验证才是把负载均衡从“会用工具”提升到“理解系统”的唯一路径。没人能一步到位但每一步都值得认真对待。