为什么Dubbo服务端发布过快导致部分节点压力过?

417 阅读5分钟

问题现象

一些业务基础的应用QPS非常高,如用户中心,高峰QPS近2W,在发布过程中,如果发布过快,导致最后发布的部分节点出现QPS倍增,进而导致Reids连接池、数据库连接池打爆等一系列连锁反应。 最后发布的节点请求量激增,如下图所示: Reids连接池异常信息

at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:249)
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:425)
at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:132)
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:95)
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:82)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:211)
...

原因分析

dubbo负载均衡策略具有预热机制,默认预热10分钟,即一个节点重启后在10分钟以后才能达到平均负载水平,而发布平台在发版过程并无时间间隔控制,导致开发可以随意点击发布或重启按钮,这就导致未重启的节点承担大部分负载流量,在超出节点承受的压力后发生雪崩现象。

Dubbo Warmup机制

dubbo服务端在启动过程有预热机制,dubbo consumer端负载均衡策略保证新注册的节点灰度放量,随着服务运行时间增大,负载权重也慢慢增大,10分钟后达到平均负载水平。

Dubbo默认随机负载均衡策略

RandomLoadBalance 是加权随机算法的具体实现,它的算法思想很简单。假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。比如数字3会落到服务器 A 对应的区间上,此时返回服务器 A 即可。权重越大的机器,在坐标轴上对应的区间范围就越大,因此随机数生成器生成的数字就会有更大的概率落到此区间内。只要随机数生成器产生的随机数分布性很好,在经过多次选择后,每个服务器被选中的次数比例接近其权重比例。比如,经过一万次选择后,服务器 A 被选中的次数大约为5000次,服务器 B 被选中的次数约为3000次,服务器 C 被选中的次数约为2000次。

以上就是 RandomLoadBalance 背后的算法思想,比较简单。下面开始分析源码。


    public static final String NAME = "random";

    private final Random random = new Random();

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size();
        int totalWeight = 0;
        boolean sameWeight = true;
        // 下面这个循环有两个作用,第一是计算总权重 totalWeight,
        // 第二是检测每个服务提供者的权重是否相同
        for (int i = 0; i < length; i++) {
            int weight = getWeight(invokers.get(i), invocation);
            // 累加权重
            totalWeight += weight;
            // 检测当前服务提供者的权重与上一个服务提供者的权重是否相同,
            // 不相同的话,则将 sameWeight 置为 false。
            if (sameWeight && i > 0
                    && weight != getWeight(invokers.get(i - 1), invocation)) {
                sameWeight = false;
            }
        }
        
        // 下面的 if 分支主要用于获取随机数,并计算随机数落在哪个区间上
        if (totalWeight > 0 && !sameWeight) {
            // 随机获取一个 [0, totalWeight) 区间内的数字
            int offset = random.nextInt(totalWeight);
            // 循环让 offset 数减去服务提供者权重值,当 offset 小于0时,返回相应的 Invoker。
            // 举例说明一下,我们有 servers = [A, B, C],weights = [5, 3, 2],offset = 7。
            // 第一次循环,offset - 5 = 2 > 0,即 offset > 5,
            // 表明其不会落在服务器 A 对应的区间上。
            // 第二次循环,offset - 3 = -1 < 0,即 5 < offset < 8,
            // 表明其会落在服务器 B 对应的区间上
            for (int i = 0; i < length; i++) {
                // 让随机值 offset 减去权重值
                offset -= getWeight(invokers.get(i), invocation);
                if (offset < 0) {
                    // 返回相应的 Invoker
                    return invokers.get(i);
                }
            }
        }
        
        // 如果所有服务提供者权重值相同,此时直接随机返回一个即可
        return invokers.get(random.nextInt(length));
    }
}

Dubbo服务端发布及流量分发过程演示

刚启动的应用有小部分流量,为便于演示,采用极限分析发,刚发布的服务按0流量分析:

以5个节点的应用示例:

  • 正常流程平均负载:每个节点负载20%流量
  • 发布一台后负载:剩余节点负载20%流量
  • 快速发布4台后:最后一台负载100%流量

解决方案

第一种方案

发布平台控制每组之间的发布间隔,组内容器节点数量由业务合理配置。此方案由发布平台控制,下一个容器组是否可以发布由中间件提供接口控制。控制规则:服务启动时间超过10分钟的比例达到60%(可调整)。

  • 优点:实现简单
  • 缺点:控制粒度较粗,只能控制到容器组级别。

第二种方案

发布平台控制每个pod发布间隔,组内容器节点数量由业务随意配置。每组内的容器发布由k8s控制,此方案要求修改k8s调度策略。下一个pod是否可以发布由中间件提供接口控制。控制规则:服务启动时间超过10分钟的比例达到60%(可调整)。

  • 优点:控制粒度到pod级别,业务不需要考虑容器组节点数量配比问题,发布灵活
  • 缺点:修改k8s调度策略,实现难度较高