本文目录导读:
这是一个非常有深度的问题。“源码路由权重”通常指的是在负载均衡器(如 Nginx、Spring Cloud Gateway、Kong、Envoy 等)或服务框架(如 Dubbo、gRPC 的客户端负载均衡)中,根据预设的权重值来分发请求的底层实现逻辑。
虽然不同项目的具体代码不同,但核心原理和数据结构是一致的,以下是其底层的数学原理和常见实现方式:
核心概念:将“权重”转化为“概率区间”
权重的本质是比例,服务器 A(权重 5)、服务器 B(权重 3)、服务器 C(权重 2)。
- 总权重 = 5 + 3 + 2 = 10。
- A 被选中的概率 = ( 5/10 = 50\% )
- B 被选中的概率 = ( 3/10 = 30\% )
- C 被选中的概率 = ( 2/10 = 20\% )
源码底层要做的事情就是:随机或轮询地命中这个概率区间。
主要实现方式
平滑加权轮询 —— 最常用(如 Nginx Upstream)
这是目前工业界最推荐、最公平的算法,解决了普通加权轮询会连续请求同一个高权重节点的问题,使得请求分布更均匀。
数据结构: 每个节点有三个核心变量:
currentWeight(当前权重)effectiveWeight(有效权重,初始等于配置权重)weight(配置权重,固定不变)
算法流程(以 Nginx 为例):
- 初始化:所有节点的
currentWeight初始化为配置的weight。 - 每次调度:
- 遍历所有节点,计算
total = sum(currentWeight of all nodes)。 - 找到 最大
currentWeight的节点(如果相同,取第一个或按顺序)。 - 选中的节点执行:
selectedNode.currentWeight = selectedNode.currentWeight - total(减去总权重)
- 其他节点执行:
otherNode.currentWeight = otherNode.currentWeight + otherNode.effectiveWeight(增加自己的有效权重)
- 遍历所有节点,计算
- 最终结果:所有节点的
currentWeight之和再次归零(为下一轮做准备)。
为什么平滑?
高权重的节点虽然被选中的次数多,但每次被选中后,其 currentWeight 会大幅下降(减去总权重),导致下一轮它大概率不会被选中,从而让低权重的节点有机会被选中,避免了“饥饿现象”。
普通加权轮询 —— 简单粗暴(适合边缘设备)
这是最直观的实现方式。
逻辑:
- 构建一个数组,
[A, A, A, A, A, B, B, B, C, C](权重 5:3:2)。 - 在数组中按顺序轮询(
index % length)。
缺点:
- 内存浪费:如果权重比例很大(如 10000:1),数组会非常大。
- 不均匀:会产生突发流量,例如连续 5 个请求都打到 A,B 才处理,对于突发敏感的服务不友好。
基于区间的随机权重 —— 适合微服务客户端(如 Dubbo 的随机负载均衡)
逻辑(加权随机):
- 归一化:将所有节点的权重映射到数轴上。
- 节点 A(权重 5):区间 [0, 5)
- 节点 B(权重 3):区间 [5, 8)
- 节点 C(权重 2):区间 [8, 10)
- 生成随机数:
random = Math.random() * totalWeight(例如随机得到 6.7)。 - 二分查找/遍历:看 6.7 落在哪个区间,落在 [5, 8),所以选择 B。
为什么二分查找?
如果节点数很多(成百上千),用遍历找区间会较慢,因此会将区间终点存入数组 [0, 5, 8, 10],然后对随机数进行二分查找,定位它属于哪个节点,时间复杂度为 ( O(\log N) )。
源码层面的关键细节(以 Java 和 Dubbo 为例)
如果你去看 Dubbo 的 RandomLoadBalance 源码,核心逻辑大约是这样:
public class RandomLoadBalance extends AbstractLoadBalance {
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, ...) {
int length = invokers.size(); // 节点数量
int totalWeight = 0; // 总权重
boolean sameWeight = true; // 所有节点权重是否相同
// 1. 计算总权重,并检查权重是否相同
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), ...);
totalWeight += weight;
if (sameWeight && i > 0 && weight != getWeight(invokers.get(i-1), ...)) {
sameWeight = false;
}
}
// 2. 如果权重不同,执行加权随机
if (totalWeight > 0 && !sameWeight) {
int offset = ThreadLocalRandom.current().nextInt(totalWeight); // 随机偏移量
// 3. 遍历减权重,找到区间
for (int i = 0; i < length; i++) {
offset -= getWeight(invokers.get(i), ...);
if (offset < 0) {
return invokers.get(i); // 当 offset 小于0时,命中
}
}
}
// 4. 如果权重相同,或者总权重为0,则直接按普通随机
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
}
关键点:
offset初始值是一个 0 到totalWeight-1的随机数。- 每次减去一个节点的权重,
offset < 0,说明当前节点包含了这个随机数在数轴上的位置。 - 这个算法没有用专门的“区间数组”,而是利用减法结合循环来实现区间判断,空间复杂度 ( O(1) ),效率很高。
硬件与底层指令
在底层(CPU 层面),这些“权重计算”本质上是:
- 随机数生成:调用
Math.random()或ThreadLocalRandom,底层涉及操作系统提供的熵池或伪随机数生成算法(如 LCG)。 - 整数比较与加减:CPU 的 ALU(算术逻辑单元)执行
add、sub、cmp指令。 - 条件分支:
if (offset < 0)对应 CPU 的jle(小于等于则跳转)指令,这里需要注意的是分支预测——如果权重分布极度不均,可能导致 CPU 分支预测失败,从而产生性能惩罚。
| 特性 | 平滑加权轮询 | 加权随机 | 普通加权轮询 |
|---|---|---|---|
| 核心原理 | 动态调整 currentWeight | 区间 + 随机数 | 整数数组 + 取模 |
| 公平性 | 极佳(均匀分散) | 一般(随机抖动) | 差(瞬时高并发) |
| 性能 | O(N) 循环 | O(log N) 二分 / O(N) 遍历 | O(1) |
| 适用场景 | Nginx、网关、反向代理 | 微服务客户端(Dubbo、gRPC) | 简单路由、硬件设备 |
| 内存占用 | O(N) | O(N) | O(N) 可能很大 |
底层本质:“源码路由权重”本质上就是通过数学映射(线性映射到区间)和逻辑判断(比较、减法),将离散的“权重数字”转化为连续的“调度概率”,并尽可能保证结果的统计均匀性和计算的高效性。
标签: 源码路由权重