
导读
本文细致地描述了关于Redis Proxy RT上升后连接倾斜问题的排查过程和根本原因,最后给出了优化方案。
问题背景
Redis 代理集群版流量模型如下图,客户端通过域名访问到 AliLB,这是一个 4 层的负载均衡,会把连接均匀地分发到后端的 proxy 上,理论上每个 proxy 上处理的客户端连接数应该相近。

▶︎ 连接不均衡
早期 AliLB 调度算法采用了 WRR,这是一种带权重的调度算法,当后端 proxy 在增加或减少时,由于算法本身的问题会出现连接调度不均,目前换为 RR 调度算法后,该问题不再出现。
部分 proxy 重启。RR 算法下 AliLB 是轮训调度,不会考虑后端 proxy 上的连接数,所以当部分 proxy 重启后,重启的 proxy 连接数会变 0,后续新建连接数和其他 proxy 相同,总连接数会低于其他 proxy。但非主动重启的 proxy 占比较小,实际情况下还没有因为这种情况导致问题。
▶︎ 负载不均衡
应用可能使用 pipeline 或异步的方式在一个连接上发送大量请求,这导致处理该连接的 proxy 负载很高。该问题只能修改业务的访问代码来优化,将请求通过更多的连接分发来达到均衡。
除了上述已知原因导致的不均衡外,还有一个困扰了 1-2 年的连接不均衡问题。
问题分析
▶︎ 怀疑 AliLB 连接分配不均
proxy 是被动接受新连接,连接数多于其他 proxy 肯定是分配过来的新连接更多。所以该问题首先猜测是 AliLB 调度不均匀。
但根据原理判断问题 proxy 更有可能出现到 AliLB 的健康保活失败,理论上应该调度过来的新连接更少才对,这和现象相反, 拉 ALB 相关同学分析后台日志并没有连接调度不均的情况。当时 proxy 的监控信息没有建连总数,问题排查阻塞了,只能增加日志继续观察。
▶︎ 怀疑客户端连接泄露
后来问题第二次出现了,这次从 proxy 日志看到问题时间段每个 proxy 上新建连接数确实是相近的,那么连接数不均衡只能是一个原因,就是问题 proxy 上断连的数量变少了。
但问题时间段 proxy 没有主动断连,所有的断连请求都是客户端发起,这就非常奇怪,客户端所有的连接的目的端地址都是指向 AliLB 的域名,对于客户端而言每个连接没什么区别,它怎么会保留问题 proxy 的连接而断开其他的。
思来想去得出一个结论,可能是客户端访问 RT 高的 proxy 时出现了超时异常,代码没有处理好异常导致连接泄露,于是问题 proxy 上的连接就越来越多。
该逻辑能够解释通,后续和业务方一起通过压测尝试复现过该问题,通过统计日志能够看到具体的客户端 ip 和 qps,但实际场景非常复杂,业务方有多个应用使用不同的模型访问 Redis,统计日志中没有找到明显的连接数、流量上升的客户端 ip,也没有找到客户端上连接泄露的具体代码。
问题复现
测试环境
服务端为 4 个 proxy 的 Redis 集群版,其中一个 proxy 增加了 200ms 延时。
客户端使用 Jedis 连接池来访问 Redis。
流量模型为每个客户端进程每秒 100 get 请求。
每 10 秒一次流量峰值,每秒 150 get 请求。
同时启动 4 个客户端进程。
测试代码
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.6.3</version></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() {@Overridepublic 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 的连接因为更早归还到连接池中,被放到了队列尾部,在低峰期不会被使用,因此连接空闲过段时间就被自动释放了。
长期下来每次高峰期 Jedis 会扩一批连接,低峰期又将 RT 正常的 proxy 连接释放,最后大部分连接都会集中在问题 proxy 上,导致负载不均,对业务的影响也越来越大。
问题避免
config.setLifo(false
所有 proxy 的连接和流量都很平稳(分钟级监控把波动拉平了)。
但这样所有连接都会被使用,连接基本不会因为空闲被释放。

问题根因
org.apache.commons.pool2.impl.GenericObjectPool 管理,该对象池默认策略为 LIFO,这会导致访问慢的连接被放到队列头更高频地使用,而访问快的连接放到队列尾,空闲时被关闭,最终连接会向访问慢的 proxy 倾斜。
优化方案
对于有 proxy 的 Redis 集群版,建议设置 LIFO 为 false,这样每个 proxy 的负载更均匀,而且不会出现连接倾斜的问题。但该设置会导致连接很难处于空闲状态,总连接数可能会上升,对于连接数很多的应用需要具体评估。
对于直连的 Redis 集群版或主从版,建议设置 LIFO 为 true,这样会尽量复用连接,空闲连接能及时释放,有利于提升 Redis 性能。

点击了解 云数据库Redis版








