本文细致地描述了关于Redis Proxy RT上升后连接倾斜问题的排查过程和根本原因,最后给出了优化方案。
Redis 代理集群版流量模型如上图,客户端通过域名访问到 AliLB,这是一个 4 层的负载均衡,会把连接均匀地分发到后端的 proxy 上,理论上每个 proxy 上处理的客户端连接数应该相近。
如果 proxy 上出现负载不均,就可能出现一个 proxy 的 cpu 已经接近满的状态,但其他 proxy 还很空闲,用户的实际吞吐远低于集群的能力上限,但访问到高负载 proxy 的请求 RT 开始升高,导致业务受损。
导致 proxy 负载不均的原因通常有 2 类。
连接不均衡
-
早期 AliLB 调度算法采用了 WRR,这是一种带权重的调度算法,当后端 proxy 在增加或减少时,由于算法本身的问题会出现连接调度不均,目前换为 RR 调度算法后,该问题不再出现。
-
部分 proxy 重启。
RR 算法下 AliLB 是轮训调度,不会考虑后端 proxy 上的连接数,所以当部分 proxy 重启后,重启的 proxy 连接数会变 0,后续新建连接数和其他 proxy 相同,总连接数会低于其他 proxy。
但非主动重启的 proxy 占比较小,实际情况下还没有因为这种情况导致问题。
负载不均衡
除了上述已知原因导致的不均衡外,还有一个困扰了 1-2 年的连接不均衡问题。
该问题现象如下,在某一时刻,因为一台机器故障导致该机器上部署的 proxy RT 变高,或者因为瞬时的流量峰值导致其中一个 proxy RT 变高,从故障时间点开始该 proxy 上的连接数就逐步上升,负载越来越高,导致 RT 也变得更高,呈现一个雪崩的状态。
怀疑 AliLB 连接分配不均
proxy 是被动接受新连接,连接数多于其他 proxy 肯定是分配过来的新连接更多。所以该问题首先猜测是 AliLB 调度不均匀。
但根据原理判断问题 proxy 更有可能出现到 AliLB 的健康保活失败,理论上应该调度过来的新连接更少才对,这和现象相反, 拉 ALB 相关同学分析后台日志并没有连接调度不均的情况。当时 proxy 的监控信息没有建连总数,问题排查阻塞了,只能增加日志继续观察。
怀疑客户端连接泄露
后来问题第二次出现了,这次从 proxy 日志看到问题时间段每个 proxy 上新建连接数确实是相近的,那么连接数不均衡只能是一个原因,就是问题 proxy 上断连的数量变少了。
但问题时间段 proxy 没有主动断连,所有的断连请求都是客户端发起,这就非常奇怪,客户端所有的连接的目的端地址都是指向 AliLB 的域名,对于客户端而言每个连接没什么区别,它怎么会保留问题 proxy 的连接而断开其他的。
思来想去得出一个结论,可能是客户端访问 RT 高的 proxy 时出现了超时异常,代码没有处理好异常导致连接泄露,于是问题 proxy 上的连接就越来越多。
该逻辑能够解释通,后续和业务方一起通过压测尝试复现过该问题,通过统计日志能够看到具体的客户端 ip 和 qps,但实际场景非常复杂,业务方有多个应用使用不同的模型访问 Redis,统计日志中没有找到明显的连接数、流量上升的客户端 ip,也没有找到客户端上连接泄露的具体代码。
所以很长时间的结论是,AliLB 调度新建连接是均匀的,proxy 是被动地建立和释放连接,客户端对所有连接是一视同仁的,可能是业务代码哪里超时后没有释放连接。
排查另一个问题时发现 Jedis、lettuce 等客户端连接池默认管理策略是 LIFO,之前一直认为 Jedis 连接池是轮训调度,在内部讨论以及和业务方交流时从来没人质疑过这一点。LIFO 的调度策略本身是不均匀的,基于该策略考虑构造一个场景来复现该问题。
服务端为 4 个 proxy 的 Redis 集群版,其中一个 proxy 增加了 200ms 延时。
客户端使用 Jedis 连接池来访问 Redis。
流量模型为每个客户端进程每秒 100 get 请求。
每 10 秒一次流量峰值,每秒 150 get 请求。
同时启动 4 个客户端进程。
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.6.3version>
dependency>
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(200);
config.setMaxTotal(200);
config.setMinEvictableIdleTimeMillis(5000);
config.setTimeBetweenEvictionRunsMillis(1000);
config.setTestOnBorrow(false);
config.setTestOnReturn(false);
config.setTestWhileIdle(false);
config.setTestOnCreate(false);
JedisPool pool = new JedisPool(config, host, port, 10000, password);
Semaphore sem = new Semaphore(0);
for (int i = 0; i < 200; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Jedis jedis = null;
try {
sem.acquire(1);
jedis = pool.getResource();
jedis.get("key");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
}).start();
}
long last_peak_time = System.currentTimeMillis();
while (true) {
try {
long cur = System.currentTimeMillis();
if (cur - last_peak_time > 10000) {
last_peak_time = cur;
sem.release(150);
} else {
sem.release(100);
}
Thread.sleep(1000);
} catch (Exception e) {
}
}
问题 proxy 的连接数和流量逐步上升。
正常 proxy 的连接数和流量在下降。
因为 Jedis 连接池默认参数设置了 LIFO 为 True,该模式下后归还的连接会放在队列头,后续被更高频的使用。
当流量峰值时,会扩充连接池的大小,这些连接会随机建立到 4 个 proxy 上,但因为问题 proxy 的 RT 高,连接到问题 proxy 的连接会更晚归还到连接池中,导致后续请求会优先访问到问题 proxy。而那些 RT 更低的 proxy 的连接因为更早归还到连接池中,被放到了队列尾部,在低峰期不会被使用,因此连接空闲过段时间就被自动释放了。