源码路由权重底层原理?

访客 源码剖析 2

本文目录导读:

  1. 核心概念:将“权重”转化为“概率区间”
  2. 主要实现方式
  3. 源码层面的关键细节(以 Java 和 Dubbo 为例)
  4. 硬件与底层指令

这是一个非常有深度的问题。“源码路由权重”通常指的是在负载均衡器(如 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 为例):

  1. 初始化:所有节点的 currentWeight 初始化为配置的 weight
  2. 每次调度
    • 遍历所有节点,计算 total = sum(currentWeight of all nodes)
    • 找到 最大 currentWeight 的节点(如果相同,取第一个或按顺序)。
    • 选中的节点执行:
      • selectedNode.currentWeight = selectedNode.currentWeight - total (减去总权重)
    • 其他节点执行:
      • otherNode.currentWeight = otherNode.currentWeight + otherNode.effectiveWeight (增加自己的有效权重)
  3. 最终结果:所有节点的 currentWeight 之和再次归零(为下一轮做准备)。

为什么平滑? 高权重的节点虽然被选中的次数多,但每次被选中后,其 currentWeight 会大幅下降(减去总权重),导致下一轮它大概率不会被选中,从而让低权重的节点有机会被选中,避免了“饥饿现象”。

普通加权轮询 —— 简单粗暴(适合边缘设备)

这是最直观的实现方式。

逻辑:

  1. 构建一个数组,[A, A, A, A, A, B, B, B, C, C](权重 5:3:2)。
  2. 在数组中按顺序轮询(index % length)。

缺点

  • 内存浪费:如果权重比例很大(如 10000:1),数组会非常大。
  • 不均匀:会产生突发流量,例如连续 5 个请求都打到 A,B 才处理,对于突发敏感的服务不友好。

基于区间的随机权重 —— 适合微服务客户端(如 Dubbo 的随机负载均衡)

逻辑(加权随机):

  1. 归一化:将所有节点的权重映射到数轴上。
    • 节点 A(权重 5):区间 [0, 5)
    • 节点 B(权重 3):区间 [5, 8)
    • 节点 C(权重 2):区间 [8, 10)
  2. 生成随机数random = Math.random() * totalWeight(例如随机得到 6.7)。
  3. 二分查找/遍历:看 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 层面),这些“权重计算”本质上是:

  1. 随机数生成:调用 Math.random()ThreadLocalRandom,底层涉及操作系统提供的熵池或伪随机数生成算法(如 LCG)。
  2. 整数比较与加减:CPU 的 ALU(算术逻辑单元)执行 addsubcmp 指令。
  3. 条件分支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) 可能很大

底层本质“源码路由权重”本质上就是通过数学映射(线性映射到区间)和逻辑判断(比较、减法),将离散的“权重数字”转化为连续的“调度概率”,并尽可能保证结果的统计均匀性和计算的高效性。

标签: 源码路由权重

抱歉,评论功能暂时关闭!