问题现象
一些业务基础的应用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调度策略,实现难度较高