Redis MGET延时排查

这是最近一个同事在开发中遇到的问题,觉得很有意思。记录了下来。

问题如下:

线上的服务,每个请求需要从Redis中 mget 300个key。发现P99延时高达20ms。不满足上线要求。

最后我们从集群负载,大Key问题,Proxy任务数以及分片的角度进行的分析。

以下是我们的排查过程:

Redis和Redis Proxy的负载

由于数据量比较大,使用了Redis Cluster + Proxy 的方式来部署。目前负载较低。

大Key问题

如果Redis中的Key、Val比较大,则容易出现大Key问题。该使用场景,key是比较短的字符串,val是浮点数(字符串),应该不存在大Key问题。

Redis Proxy的单个任务过大

不知道这是否是所在公司特有的问题,之前在使用zset存储数据的时候,在线使用pipeline + zrange的方式获取数据,pipeline中的command过多(100个)的时候,延时会很高。但是如果拆分成多个pipeline并发调用,每个仅包含几十个command的时候,延时就十分正常。推测这里的proxy的实现应该有问题(虽然但是,有问题也没用,中间件团队并不会真的优化和支持)。

鉴于上述反例,我们将300个key拆分为2-10组,然后并发访问,每组都是一个mget。进行了测试。在2组的时候,似乎延时有点改善,但不大。10的时候甚至出现的劣化。

显然分组越多的话,对Redis Proxy的扇出数就会越多,也会影响P99。但是从上述的表现可以发现,单组的key的数目对整体延时影响不大。

Redis分片过多

随后,我们使用CLUSTER INFO命令查看了集群的分片数。发现高达50个!如果300个key分布均匀的话,那么一次请求会在Redis Proxy中被拆分成50个请求,然后每个请求包含6个key。

验证分片数对延时的影响:

我们离线编写的单测的代码。运行前,在redis中写入300个key,其中key的格式为sprintf("test_{%d}_xxx_%d", id % shard, id)。在Redis中,通过{}指定计算hash的seed。这样我们就能通过控制shard的值,来模拟分片数。最终发现(这里的测试的key数目很少,结果只能简单看看):

shard 1 avg: 683.379µs p99: 2.385465ms
shard 2 avg: 723.663µs p99: 4.276358ms
shard 4 avg: 829.423µs p99: 5.022665ms
shard 8 avg: 951.461µs p99: 6.656493ms
shard 16 avg: 1.249993ms p99: 8.688271ms
shard 32 avg: 1.510463ms p99: 9.577033ms

显然分片数会直接影响到mget的性能。

那么下一步优化的核心就是如何减少分片数。假定我们最终的分片数是10.

可行的方案:

  1. 在读写redis的时候都在key中的hash的部分手动取模,模取10。
    1. 优点:
      1. 修改简单
      2. 可以直接复用Redis,不需要重新申请资源
    2. 缺点:
      1. 该集群会有10个节点(假定slot不同)的实例负载会比其他的高,埋了个坑。
      2. 如果将来将模修改为20的话,需要写入端先双写,然后服务端再修改代码,发版
  2. 缩容到10个分片,为了应对高QPS,每个分片多几个副本。
    1. 优点:
      1. 不需要给redis key特殊处理
      2. 如果分片数增加,可以依赖redis自身的机制,无缝迁移
    2. 缺点:
      1. 通过master-slave模式,会限制写入的吞吐
      2. SRE在扩容时,可能会直接扩了shard,而不是新增副本,导致延时上涨
  3. 独立一个Redis集群,控制分片数
    1. 该Redis仅给我们当前的服务使用,这样整体的写QPS就不大。但是还是需要和SRE进行约定。

最终,我们选择了方案3,离线任务使用自己的Redis,仅在最终写入数据的时候才双写到新集群。在线侧仅读取新集群。

注意,这里由于我们的key的总规模是千万级别,但是QPS和KPS很高,所以才适合该方式。对于其他的应用场景不一定能直接参考。