做后端的同学几乎没有不跟负载均衡打交道的。无论是用Nginx转发API请求还是用网关做流量分发本质上都在问同一件事这一批请求到底该交给哪台机器去处理才能既快又不容易出错。我见过不少团队业务代码写得非常工整却在负载均衡策略上吃了亏——要么机器利用率天上地下要么某个节点一挂网关被雪崩流量直接冲垮。负载均衡策略虽然平时不太起眼但一旦出了问题往往是整个链路里最先响警报的环节。这篇文章我就把自己在网关和自研负载均衡器上折腾过的经验整理一遍会先把常见的内置策略捋清楚说清楚每种策略适用的场景和吃了亏的原因再重点讲“自定义”这件事——什么情况下内置策略确实不够用有哪些思路可以自己去实现以及实现之后要配套哪些机制才能真正落地。适合后端开发、运维和正在选型网关的同学看如果你刚入行想搞明白负载均衡到底在做什么这篇也能帮你把整个脉络理清楚。1. 先把负载均衡的定义域画清楚它到底在均衡什么聊策略之前必须先搞清楚一个容易被忽略的问题负载均衡策略并不是只在网关那一层出现。很多人一说负载均衡就想到Nginx、LVS其实整个请求链路里至少有三个位置都有均衡问题每个位置的策略诉求完全不一样。1.1 从DNS到数据层均衡无处不在第一层是DNS层面的地域均衡。用户在浏览器里输入域名DNS会根据用户的地理位置返回不同的机房IP这本质上也是一种负载均衡策略——只不过它均衡的是“用户流量进入哪个机房”。这一层要是配不好会出现华东用户拥堵、华南机房空转的荒唐事。第二层是接入层的流量分发也就是大家最常说的网关或反向代理。这一层面对的是成千上万的并发请求核心诉求是“把请求均匀且可控地分给后面的服务实例”Nginx、云负载均衡、Kubernetes的Service都是干这个活的。第三层是数据层的读写均衡。比如数据库的读写分离、分库分表后请求打到哪个分片消息队列的消费者如何分摊分区这些也属于负载均衡的范畴。这一层的策略往往和业务强相关不能简单地轮询否则会出现热点数据全部砸到某一个分片上的情况。1.2 均衡的四个隐性指标正确、均匀、稳定、可控很多人评估负载均衡只看“均不均匀”但实际在线上混过你就知道光均匀根本不够。我习惯把指标拆成四个正确性请求不能被分到一个“已经不健康”的节点上否则用户直接看到超时或5xx。这背后需要健康检查机制兜底。均匀性各个节点的CPU、内存、连接数大体接近不要出现一台被榨干、另一台闲着喝茶的状态。稳定性策略不能频繁切换节点否则缓存命中率下降、连接反复重建业务表现比不均衡更差。可控性运维和开发能通过配置或接口干预调度结果比如临时摘掉某台机器、把某个用户流量固定到一组节点而不是策略完全黑盒、没人能碰。这四个指标其实对应的是“分布式系统三高”之外的另类维度。你选策略的时候可以在心里拿这四个指标过一遍很多看似高级的算法在某个指标上会突然拉胯。2. 内置策略其实就两派静态分配派与动态观测派市面上所有负载均衡产品无论名字多花哨底层思路基本都能归到静态和动态两大派。把这两派看熟了再去看具体的配置项就不会懵。2.1 静态派轮询、加权轮询、随机、哈希静态派的特点是不看节点实时状态纯粹按事先定好的规则做选择。轮询Round Robin是最朴素的方式请求按顺序轮流发给每个节点。它的优点是实现简单、无状态缺点也很明显扛不住节点性能差异。如果两台机器一台4核一台32核轮询会让4核机器和32核机器承受同等流量4核很快被打满。所以现在生产环境很少用纯轮询基本都是加权轮询。加权轮询Weighted Round Robin给每个节点配一个权重权重高的分到更多请求。配置起来很直观新买的机器权重给大一点老机器给小一点。但加权轮询有个坑——它只解决“每轮分多少”的问题不解决“当前这台机器是否扛得住”的问题。如果权重配得不准或者代码出现了性能劣化加权轮询依旧会傻傻地往性能差的机器上分发。随机策略和轮询本质一样只是把“轮流转”换成了“随机挑”。概率上它可以近似均匀但会产生一些抖动的尾巴而且无法保证同一类请求稳定落在同一节点上。生产环境里我几乎不用纯随机除非是压测工具。哈希策略是静态派里最有业务价值的一个。它按客户端IP、用户ID或请求URL的哈希值取模选出一个节点。好处是同一个Key永远落在同一台机器上天然契合带缓存、带本地状态的服务。坏处是节点数量一变扩缩容或宕机摘除哈希取模的结果就会大范围改变大量请求会重新映射缓存瞬间被打穿。为了解决这个问题才有了后面要详细讲的一致性哈希。2.2 动态派最少连接、最快响应、资源感知动态派的核心是无视事先配置优先看节点当下的真实负载状态。最少连接Least Connections在长连接场景下非常实用。它会把请求分给当前连接数最少的节点因为连接数往往和CPU占用率呈正相关。我之前维护过一个长连接推送服务起初用轮询结果连接全堆到第一台机器上换成最少连接之后各节点连接数曲线立刻变得整齐。加权最少连接是它的升级版在连接数基础上再乘以权重系数兼顾了机器性能和当前压力是目前云负载均衡和网关产品里我最推荐的基础配置之一。最快响应时间Latency-based则把“谁能最快返回结果”当成调度依据。这种策略在网络抖动比较常见的时候很好用但它需要持续统计每个节点的响应时间统计窗口设短了会把某一次偶然抖动放大设长了又反应迟钝调参要很小心。还有一类资源感知策略比如按CPU使用率、内存占用率或带宽来调度。这类策略最理想但落地难度也最大因为“资源指标”采集本身就有延迟而且采集探针的CPU消耗也可能反过来干扰调度判断。实际项目中我见过很多号称“智能负载均衡”的方案最后排障发现根因是资源指标采集不准。以下是几类常见策略的横向对比方便选型时一眼看明白策略是否依赖实时状态优点典型风险适用场景轮询否实现简单、公平节点性能差异时拖后腿同配置实例、无状态服务加权轮询否能体现机器权重权重不准会放大问题性能差异可控的服务IP/URL哈希否同一来源稳定映射扩缩容引发缓存雪崩缓存服务、本地会话最少连接是长连接场景均衡短连接场景效果平平推送、WebSocket等加权最少连接是兼顾性能与压力需要准确权重混合配置集群最快响应是对慢节点敏感统计窗口调参难网络抖动多的场景资源感知是最贴近真实负载采集成本高、滞后高定制自研场景2.3 为什么默认策略总是“够用但不够好用”大多数网关产品的默认配置是加权轮询或最少连接。对一般业务来说这个默认值足够撑起日常流量这也是很多人从不深究策略的原因。但一旦流量形态发生变化默认值的短板就会显现。我给你说一个真实经历。某一次大促预热期间我们某个服务的流量突然比平时高出五倍网关用的是加权轮询。刚开始还算稳定但半小时后有几台机器CPU撞到90%以上另几台却只有50%。排查之后发现问题不在网关策略而在上一步——部分热点用户大量集中访问同一个数据分片这个分片所在节点的CPU自然飙升轮询策略不会识别“这组请求来自热点分片”只会机械地按权重转发。这种“业务热点导致的倾斜”恰恰是内置策略解决不了的。这也是我从“无脑信任默认策略”转到“认真考虑自定义”的核心转折点。3. 什么时候真的需要自定义策略不满足这三个信号就先别急自定义负载均衡策略听起来很酷但要付出的维护成本是真的。改网关配置、写脚本、加监控哪一样都在增加复杂度。所以我先把“什么情况才值得动手”的信号列清楚避免一时冲动给自己挖坑。3.1 信号一负载均衡结果始终和业务预期偏离如果流量明明不大但各节点负载严重不均而且持续时间超过十几分钟基本可以判定内置策略不合适。这种偏离通常分两种一种是节点性能差异过大加权轮询的权重配不平另一种是流量本身存在天然的热点比如某个大客户每秒发出上万请求它的请求如果全局随机分发会导致所有节点放着没明确顺序地抖动而如果全部命中到某一台又会让这台机器成为攻坚战。判断方法很简单把网关的调度日志或者各节点请求量统计拉出来算一下变异系数标准差除以均值。如果变异系数长期超过0.3说明负载分布已经明显倾斜了这时候就应该考虑更智能的调度。3.2 信号二业务对“打到谁身上”有硬性要求这类信号最典型的就是多租户隔离。我做过一个多租户SaaS服务不同大客户对延迟和稳定性的要求完全不同有的客户要求隔离计算资源有的大客户之间不能互相抢CPU。内置策略不管租户关系只会机械地分流量。这种情况下必须要做“按租户维度”的自定义调度——同一个租户的流量固定打到一个指定节点组不同租户之间物理隔离。灰度发布也是这类信号。服务升级时我们希望只把一小部分内测用户的流量打到新版本节点上其余用户继续走老版本。这里的调度规则是“按用户标识判断是否命中灰度名单”内置策略根本没有这个能力。3.3 信号三你希望负载均衡参与成本和容灾控制有些团队对带宽和成本有严格要求希望负载均衡能感知到“某些节点运行便宜服务器、某些节点运行贵服务器”从而把流量优先打到便宜节点只在高峰时溢出到贵节点。这其实是一种带业务目标的最优化问题内置策略同样不会替你考虑。还有容灾维度。比如两个机房同时提供服务主用机房带宽充足、备用机房带宽紧张。平时希望90%流量走主机房主用机房断网后自动切到备用机房并且慢慢回切而不是瞬间全量恢复。这种带状态、带时间约束的调度内置策略基本无能为力只能自己写。对照上面三个信号如果你的场景一个都不沾那真的不用折腾自定义——内置的加权轮询或最少连接足够可靠折腾反而添乱。如果中了两条以上那继续往下看后面的内容就是为你准备的。4. 自定义负载均衡策略的三种主流实现路径自定义不等于从零写一个Nginx。实际上大部分情况下只需要在现有网关上做扩展即可。我按成本从低到高给你整理三条路径你可以根据自己的技术栈和预算来选。4.1 路径一用网关脚本扩展实现业务感知调度这是投产最快、维护成本最低的方案代表是OpenResty的Lua脚本。OpenResty基于Nginx把Lua解释器嵌入其中可以在每个请求进入时执行一小段Lua代码实现完全自定义的调度逻辑。前两年我为某跨平台系统改造过一个网关。背景是上游两个数据服务节点配置完全一样但其中一个节点会间歇性出现慢查询加权轮询根本感知不到。当时就写了一段Lua脚本思路是每个请求过来时先看这个请求的目标路径判断它属于哪类业务比如普通查询还是重度报表。重度报表请求走专门的高规格节点组普通查询按默认轮询。节点组内部增加了主动健康探测每5秒打一个轻量探测接口如果响应超过200ms就把该节点在探测周期内的权重临时调低一半。简化后的脚本逻辑大致是这样-- 自定义路由表每个target代表一台上游机器 local routing_table { { target 10.0.0.11:8080, category heavy, weight 5 }, { target 10.0.0.12:8080, category heavy, weight 5 }, { target 10.0.0.13:8080, category normal, weight 3 }, { target 10.0.0.14:8080, category normal, weight 3 }, } function get_category(uri) if string.match(uri, ^/api/report) then return heavy end return normal end function choose_upstream(uri) local category get_category(uri) local available {} for _, item in ipairs(routing_table) do if item.category category then table.insert(available, item) end end -- 这里简化了加权轮询逻辑实际应按权重累计区间取随机数 local idx math.random(#available) return available[idx].target end local upstream choose_upstream(ngx.var.uri) ngx.var.upstream upstream这里有一个非常需要注意的坑Lua脚本在OpenResty中的执行位置。如果写在access阶段它有很大概率可以改变upstream但如果写在rewrite阶段过早的位置访问变量可能还没准备好。建议先在OpenResty环境里用模拟请求验证通了再上生产。脚本扩展也有一点要克制不要在Lua脚本里做重计算不要加太复杂的循环不要写日志刷屏。因为网关是每请求都经过的热路径任何多余CPU消耗都会被放大。我见过一个团队在Lua里加了加密解密操作做路由判断结果网关CPU直接飙到80%这是典型的“为自定义而自定义”。4.2 路径二基于网关插件机制实现定向扩展现在很多云原生网关比如某些以插件模式扩展的API网关本身就预留了自定义负载均衡插件接口。如果你使用的网关支持这种机制那就没必要去写裸脚本而是应该按照网关定义的扩展点去实现一个标准插件。这种方式的好处是平台帮你管理了接口协议、生命周期和失效兜底。比如某个插件在执行过程中出错网关会直接跳过该插件回退到默认策略而不影响正常流量。在脚本扩展中如果Lua代码抛异常处理起来往往更麻烦甚至可能导致请求直接5xx。插件化自定义负载均衡通常要关心三个扩展点路由选择扩展点这里决定请求最终打到哪个节点是核心逻辑所在可以读取请求头、解析JWT、查询用户标签。节点评分扩展点这里可以为每个节点生成一个动态分值然后由网关选择一个分值最高的节点。自定义重点可以放在“如何评分”上比如把CPU、内存、延迟、历史错误率组合成一个综合分。状态同步扩展点某些场景下需要把自定义路由的结果对接到配置中心比如新节点上线时自动加入路由拓扑而不是靠手动改配置。用插件机制还有一个好处多个策略可以并存按路由规则分成不同策略组。比如内部API用A策略、外部API用B策略、灰度流量用C策略互相不干扰。这在脚本式扩展里实现起来比较麻烦因为所有逻辑都堆在同一个入口里很容易变成一个大泥球。4.3 路径三自研负载均衡器核心是选择合适的哈希与权重算法如果前面两种方案都满足不了你——比如你对调度性能要求极高、需要在进程内直接做路由又或者你连网关都不想引入只想在服务框架内部自带负载均衡能力那就得走自研路线。自研负载均衡器最常用的自定义思想是一致性哈希Consistent Hashing。一致性哈希解决的是前面提到的“节点变化导致哈希大面积失效”问题。它的核心思想是把所有节点和所有请求都映射到一个环形哈希空间请求沿环找离它最近的节点。节点增减时只有环上邻近区间的请求会重新映射其他请求不受影响。实现起来并不复杂我自己写过一份简化版思路是import hashlib class ConsistentHashBalancer: def __init__(self, nodesNone, replicas150): self.replicas replicas self.ring {} # 哈希值 - 节点 self.sorted_keys [] if nodes: for node in nodes: self.add_node(node) def _hash(self, key): return int(hashlib.md5(key.encode()).hexdigest()[:8], 16) def add_node(self, node): for i in range(self.replicas): virtual_key f{node}#{i} h self._hash(virtual_key) self.ring[h] node self.sorted_keys.append(h) self.sorted_keys.sort() def remove_node(self, node): for i in range(self.replicas): virtual_key f{node}#{i} h self._hash(virtual_key) del self.ring[h] self.sorted_keys.remove(h) def get_node(self, key): if not self.sorted_keys: return None h self._hash(key) for sk in self.sorted_keys: if sk h: return self.ring[sk] return self.ring[self.sorted_keys[0]] # 示例使用 nodes [10.0.0.1:8080, 10.0.0.2:8080, 10.0.0.3:8080] balancer ConsistentHashBalancer(nodesnodes) print(balancer.get_node(user-10043))复制节点时需要注意replicas参数。replicas是每个物理节点对应的虚拟节点数量这个值直接影响负载分布的均匀程度。配得太小比如20数据很容易集中在少数几个物理节点配得太大比如500内存占用和查找耗时都会上升。我个人推荐在150到200之间起步然后根据实际分布曲线调整。如果觉得一致性哈希仍然不够均匀可以考虑更现代的Rendezvous Hashing最高随机权重哈希。它的思路更直接对每个节点算一个加权分值分值最高的节点获得请求。它不需要环结构节点增减时影响范围同样很小而且内存占用更低。感兴趣的话可以在服务发现场景里优先试验这个方案。还有一种自研思路是基于“轮询动态评分”的加权算法。比如让每个节点上报健康分均衡器每秒刷新一次请求过来后以概率方式选择节点分值高的被选中的概率大。这个思路实现最简单但要注意“打分”的口径要统一不要有的节点打0到10分、有的打0到100分排序结果会被量纲扭曲。4.4 自定义策略能真正落地还差三个配套机制自定义策略写出来只是万里长征第一步真正能在线上可靠运行必须配套上健康检查、观测和降级机制。我见过太多团队写了很炫的脚本但健康检查没做好脚本把一个已宕机的节点照常分到流量结果事故比不使用自定义策略时更严重。第一个配套机制是健康检查。健康检查必须与自定义策略联动。策略的输出不应该直接决定“请求发给谁”而应该先经过“节点是否可用”的过滤。健康检查分为主动探测和被动探测。主动探测是定时间隔去请求节点的健康检查接口被动探测是统计最近一段时间内节点的错误率。两者结合是最理想的主动探测能尽早发现问题节点被动探测能发现那些“探针正常但实际错误率爆表”的节点。第二个配套机制是观测与灰度。自定义策略上线时不要一步到位全量替换。应该先让1%的流量走新策略对比新老策略下的错误率、延迟分布和节点负载方差确认没有异常后再逐步放大。每一次放大都要通过Dashboard观察至少一个大流量周期比如一天内的峰值时段。第三个配套机制是降级。自定义策略一旦崩溃需要能快速回退到内置策略。这个“快速回退”必须要提前演练。每次发布自定义策略代码时顺手验证一下“手动关闭策略开关”后流量是否平滑切换到默认策略确认不会出现断连或者瞬间全部超时。我记得有一次演练回退因为忘记清掉网关的连接池缓存回退后连接仍在旧节点上结果排查了很久。这种细节不做演练真的发现不了。5. 自定义负载均衡的踩坑记录与排查技巧这一部分比起前面的方法论更值得仔细阅读。以下问题全都是我在生产环境中亲眼见过或者亲手调过的每个问题后面我会附上排查思路。5.1 健康检查抖动导致节点被误摘流量全部打到一个节点某一次我们为自定义策略增加了更敏感的健康检查探测间隔从10秒缩短到3秒失败3次就摘除节点。看起来更智能结果是某台机器的健康检查接口因为依赖的一个数据库连接池暂时打满响应超时了两秒于是被判不健康。三个周期后节点被摘除原本分给它的一半流量瞬间转移到剩下两台机器那两台机器很快就扛不住了接着又触发自己的健康检查超时形成雪崩。排查思路是在被摘除节点的网关日志里先确认摘除时间点再看同时段的健康检查响应时间然后顺藤摸瓜找到数据库连接池的占用曲线。根本原因不在负载均衡策略而在健康检查口径。后来我们把健康检查改成“本机本地资源足够和多线程池处理能力正常才算健康”不再把远程依赖的临时抖动纳入指标节点摘除频率立刻降了下来。经验是健康检查要尽可能做“本地自检”而不是“远程全链路检查”否则容易把下游抖动误判为本节点故障。5.2 自定义策略导致连接池耗尽表面看问题在业务层最少连接策略推广到短连接服务上时有时候会看到一个反直觉的现象每个节点的连接数虽然均衡但整体延迟变高了。原因是最少连接策略会倾向于把请求分给当前空闲的节点而这个空闲可能是“新建连接刚建立尚未计数”的短暂时段。大量的短连接服务在频繁重建连接而重建连接本身有握手开销和内存占用反而拖慢了每个请求。排查时我通常会把“每秒钟新建连接数”指标拉出来和“活跃连接数”放在同一张图上对比。如果是短连接模型在固定QPS下新建连接数曲线应该相对平稳如果发现新建连接数出现了明显的锯齿状波动再配上活跃连接数的水位线就能发现策略在频繁地切换目标节点。这类问题的解决办法往往很朴素比如把最小连接数策略改成“连接数加上一个固定惩罚值”的方式让新建连接在初始阶段不那么容易被选中保护连接池。5.3 会话保持误伤了负载均衡缓存和会话状态错乱某个遗留老项目没有做无状态化改造用户的登录Session直接存在本地内存里。采用轮询策略后同一个用户两次请求可能被分到不同节点第二次请求就找不到Session被迫重新登录。为了解决这个问题我们最初上了IP哈希同一个IP固定到一个节点。结果因为公司出口IP可能同时带出几百个用户流量倾斜比轮询更严重。排查思路看用户IP和节点映射曲线再对比各节点的活跃Session数量。最终我们做的不是简单IP哈希而是基于“用户ID哈希后映射到一个小的缓存节点组”在节点组内部再用一致性哈希做二级调度。用户ID比IP更稀疏这样的路由结果相对均匀也能保证同一个用户稳定落在同一台机器上。这里面要额外说明的是会话保持不应该只在负载均衡策略上做文章更彻底的做法是让业务服务无状态化把Session统一放到共享存储或JWT这类无状态凭证里。负载均衡策略只是辅助手段不能替代架构调整。5.4 自定义脚本本身成了瓶颈CPU消耗和日志刷屏并存网关脚本扩展很容易写出性能灾难。有一次为了做精细路由脚本里对每个请求都查询了一次Redis结果网关QPS一上来Redis成了新的瓶颈响应时间从5ms涨到300ms。更隐蔽的坑是脚本异常分支里写日志平时流量小无所谓一旦流量峰值到来日志输出量每秒几十万条网关所在磁盘IO直接被打满。排查的方式是给脚本逻辑加上性能profile。OpenResty里可以开启lua-code-profiler统计每个函数的CPU耗时日志刷屏则可以按“单IP每秒钟最多输出N条”做限流。我现在的原则是脚本里只做本地计算和轻量决策任何需要访问外部存储的变量都提前在网关启动时加载到内存每几秒增量刷新绝不在每个请求的热路径里访问Redis或数据库。5.5 自定义策略一切正常但监控里看不到调度依据黑盒出了问题最后一个坑比较隐性策略运行得很好但缺少调度日志等真正出问题时没有任何一手数据可以回溯。比如某个节点权重被脚本动态调低了50%一个月后有人发现这台机器流量明显少于其他机器开始排查结果谁都说不清是被哪条规则调低的。解决办法是在自定义策略里埋点输出“决策原因”。每跳过一个节点要记录是“健康检查不通过”还是“动态评分太低”还是“灰度白名单未命中”。这些日志不用特别详细但至少要把决策的关键输入和输出记录下来方便回溯。网关层面则可以把这些日志采集到集中日志平台按节点、按策略类型建立索引。我后来在自研均衡器里直接加了一个决策结构体每个请求的调度结果都会带上选择的节点、评分、候选节点列表和过滤原因。虽然日志量变大了一些但排障效率显著提升。这个投入我认为非常值得。写在最后的一个实操建议上面聊了很多策略原理和坑最后我想分享一个已经内化成习惯的做法。每次上线负载均衡策略改动我都要求团队先回答三个问题策略改动的预期收益是什么、回退方案是什么、判断成功的指标是什么。这三个问题只要有一个答不上来这次改动就不应该上线。负载均衡策略没有绝对“最好”的版本只有相对于当前业务形态“足够合适”的版本。流量模型会变业务会调机器会换所以每隔一段时间重新评估一次策略比追求一劳永逸的实际意义大得多。如果你刚起步建议不要急着写自定义策略先把加权轮询和最少连接在监控面板上的表现看熟感知到“默认策略不够用”的痛点再回来看看这篇文章里提到的三种实现路径你会有完全不同的体会。