这是最近一个同事在开发中遇到的问题,觉得很有意思。记录了下来。
问题如下:
线上的服务,每个请求需要从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.
可行的方案:
- 在读写redis的时候都在key中的hash的部分手动取模,模取10。
- 优点:
- 修改简单
- 可以直接复用Redis,不需要重新申请资源
- 缺点:
- 该集群会有10个节点(假定slot不同)的实例负载会比其他的高,埋了个坑。
- 如果将来将模修改为20的话,需要写入端先双写,然后服务端再修改代码,发版
- 优点:
- 缩容到10个分片,为了应对高QPS,每个分片多几个副本。
- 优点:
- 不需要给redis key特殊处理
- 如果分片数增加,可以依赖redis自身的机制,无缝迁移
- 缺点:
- 通过master-slave模式,会限制写入的吞吐
- SRE在扩容时,可能会直接扩了shard,而不是新增副本,导致延时上涨
- 优点:
- 独立一个Redis集群,控制分片数
- 该Redis仅给我们当前的服务使用,这样整体的写QPS就不大。但是还是需要和SRE进行约定。
最终,我们选择了方案3,离线任务使用自己的Redis,仅在最终写入数据的时候才双写到新集群。在线侧仅读取新集群。
注意,这里由于我们的key的总规模是千万级别,但是QPS和KPS很高,所以才适合该方式。对于其他的应用场景不一定能直接参考。