Redis Proxy中连接倾斜的排除之旅

原文标题:诡异!Redis Proxy RT上升后连接倾斜

原文作者:阿里云开发者

冷月清谈:

- Redis Proxy出现连接倾斜,即一台机器负载过高,其他机器负载较低。
  • 排查原因时发现,AliLB连接分配均衡,排除了AliLB问题。

  • 同时,怀疑客户端连接泄露,但无法复现。

  • 后续复现时发现,连接池默认管理策略为LIFO,导致访问慢的代理连接被高频使用,连接向访问慢的代理倾斜。

  • 优化方案:对于有Proxy的Redis集群版,建议将LIFO设置为false,使连接池策略保持均衡。对于直连的Redis集群版或主从版,建议将LIFO设置为true,以复用连接并及时释放空闲连接。




怜星夜思:


1、针对Redis Proxy的连接倾斜问题,除了本文提到的连接池策略外,还有什么其他潜在原因?
2、文中提到的LIFO策略在Redis Proxy连接池中是如何影响连接分布的?
3、除了文中提到的优雅关闭连接协议,还有哪些其他机制可以动态平衡连接数、负载和RT?

原文内容

阿里妹导读


本文细致地描述了关于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 占比较小,实际情况下还没有因为这种情况导致问题。

负载不均衡

  • 应用可能使用 pipeline 或异步的方式在一个连接上发送大量请求,这导致处理该连接的 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.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() {
@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 的连接因为更早归还到连接池中,被放到了队列尾部,在低峰期不会被使用,因此连接空闲过段时间就被自动释放了。

长期下来每次高峰期 Jedis 会扩一批连接,低峰期又将 RT 正常的 proxy 连接释放,最后大部分连接都会集中在问题 proxy 上,导致负载不均,对业务的影响也越来越大。

问题避免

还是上面的测试代码,但增加下面设置 ,此时 Jedis 会均匀地使用所有连接,再次测试的结果。

config.setLifo(false)
所有 proxy 的连接和流量都很平稳(分钟级监控把波动拉平了)。

但这样所有连接都会被使用,连接基本不会因为空闲被释放。





问题根因

Jedis、lettuce 客户端连接池使用:

org.apache.commons.pool2.impl.GenericObjectPool 管理,该对象池默认策略为 LIFO,这会导致访问慢的连接被放到队列头更高频地使用,而访问快的连接放到队列尾,空闲时被关闭,最终连接会向访问慢的 proxy 倾斜。

优化方案

对于有 proxy 的 Redis 集群版,建议设置 LIFO 为 false,这样每个 proxy 的负载更均匀,而且不会出现连接倾斜的问题。但该设置会导致连接很难处于空闲状态,总连接数可能会上升,对于连接数很多的应用需要具体评估。

对于直连的 Redis 集群版或主从版,建议设置 LIFO 为 true,这样会尽量复用连接,空闲连接能及时释放,有利于提升 Redis 性能。

目前 LIFO 的推荐设置不是固定的,客户端在不知道后端统计信息的情况下很难自动调节连接池策略来保持最优,比较好的方案是 proxy/Redis 和 客户端之间具备一种优雅关闭连接的协议,当服务端检测到负载不均、RT 异常时能通知客户端无损关闭连接,这样在连接数、负载、RT 上做动态平衡,优雅关闭连接的协议同样能减少变配、升级场景下对业务的影响。

云架构必修课:云上高可用架构


业务的持续稳定可服务,决定着企业对客户的服务质量,是企业发展的基础。而应用部署的高可用架构对于业务的稳定与发展起着至关重要的作用,本方案从企业上云最基础的需求出发,面向可能遇到的单点故障风险,介绍了经典的“业务上云高可用架构”方案设计。


点击阅读原文查看详情。


主动断开连接:当检测到连接长时间未被使用或响应缓慢时,可以主动断开该连接,并从连接池中移除。这有助于减少连接池中处于空闲或低效状态的连接。

客户端请求分布不均匀,比如某些客户端发送的请求量远高于其他客户端。

  1. 客户端代码问题。如果客户端代码在遇到连接异常后没有正确处理,可能会导致连接泄露。

  2. 网络问题。如果网络出现抖动或中断,可能会导致连接中断,从而造成连接不均衡。

  3. Proxy本身的性能问题。如果某个Proxy出现性能问题,如内存泄漏或CPU使用率过高,也可能导致连接倾斜。

circuit breaker机制:当检测到某个Proxy的RT或负载过高时,可以暂时将该Proxy从连接池中移除,从而避免连接倾斜。当Proxy恢复正常后再将其加入连接池。

LIFO策略的优点是能够提高缓存命中率,因为最近访问的连接更有可能是再次被访问的。然而,在Redis Proxy场景中,由于Proxy性能可能存在差异,LIFO策略可能会导致连接分布不均衡。

权重调整:根据Proxy的实时负载情况,调整连接池中每个Proxy的权重,将更多的连接分配给负载较低的Proxy。

LIFO策略(后进先出)会导致访问慢的Proxy连接被高频使用,而访问快的Proxy连接被低频使用。随着时间的推移,连接会逐渐向访问慢的Proxy倾斜。

形象地比喻,就像一个队列,访问慢的Proxy连接排在队首,被优先使用;而访问快的Proxy连接排在队尾,被后使用。