Redis凭借着丰富的数据类型和高并发处理能力已经成为目前最受欢迎的内存数据库,很多用户对Redis的印象就是高并发,高吞吐,低时延,殊不知Redis的性能也很“脆弱”,使用不当很容易出现性能问题。
Redis原理
Redis是一个单进程单线程的模型,网络收发包、协议处理以及命令处理都是在一个线程中完成。对每一个用户连接来说,主线程会读一批数据放到QueryBuffer中,然后尝试协议解析,每解析出一个完整的Request,就先处理,然后把Reply放到OutputBuffer中等待发出,然后再尝试解析QueryBuffer中剩下的数据,直到QueryBuffer中没有数据或者没有一个完整的Request后,处理下一个连接。
Redis常见慢查问题
阿里云Redis正常情况下可以达到8~10W的QPS,这意味着平均一条命令处理时间基本在微妙级。Redis的快是建立在所有操作逻辑都很快速的基础上的,实际情况并非一直这么美好。
大包
如果一个Request几十MB甚至几百MB,网络IO、协议解析都必然花更长的时间。而在TCP网络层,一个Redis Request也会分成很多个packet进行传输,任何一个包发生了超时重传,意味着这整个请求的响应时间都会变慢。
O(N)命令
Redis有不少O(N)类命令,这些操作的响应时间是和数据有关的。比如说keys命令,需要遍历内存中所有key,这期间主线程不能处理任何请求。还有hgetall这类命令,耗时取决于哈希中有多少条field+value。
流控
网络流量一旦超过规格限制会触发流控,Redis会延迟接收和处理命令,对客户端来说就会发现响应变慢了。
持久化
Redis的持久化机制不管是AOF还是RDB,都会fork一个子进程。fork系统调用本身会随着Redis内存增大而消耗更长的时间。
pipeline
部分客户端使用异步模式或者批处理模式发送命令,与之相对应的是ping-pong模式。事实上Redis并不区分ping-pong还是pipeline,不管是什么模式,在多个客户端并发访问的情况下,Request/Response无非处于这几种状态:
- 客户端所在机器内核socket buffer中等待TCP发送
- 网络传输中
- Redis所在机器内核socket buffer中
- Redis的QueryBuffer中等待协议解析和处理(pipeline)
- 命令处理中
- Redis的output buffer中
- 内核socket buffer中 or 网络传输中
- 客户端所在机器内核socket buffer中 or 等待客户端处理
一般业务测统计的Redis响应时间是从 1 -> 8 消耗的时间,Redis测统计的响应时间只包含了 4 -> 6。
由于Redis单线程的模型,一旦有一个命令慢了,意味着后面所有命令都会变慢,以1W QPS为例,有一个hgetall命令处理消耗了100ms,从客户端的角度来说,此时在在内核socket buffer以及以及读到用户态QueryBuffer中的命令都会变慢,不仅仅是这一个命令慢了,影响的可能是1000条命令。
客户端统计问题
上面提到了pipeline模式,如果客户端使用批量发送,需要特别注意客户端统计响应时间的方式。一般来说,客户端会在发送之前开始计时,在收到Response后停止计时。事实上,在批处理模式下,这种计时是很不准确的,仔细思考一下就明白,批量发送,最后一个Request的响应时间中绝大部分都是在等待前面请求处理。
命令 | 说明 |
---|---|
keys, flushadb, flushall | O(N),遍历所有Key |
mget, mset, del, unlink, exists | O(M),带的key不宜过多 |
smembers | O(N),返回集合中所有member,避免集合过大(大key问题) |
sinter, sinterstore | O(N*M),避免集合过大(大key问题) |
sunion sunionstore, sdiff, sdiffstore | O(N),避免集合过大(大key问题) |
zunionstore, zinterstore | O(NK)+O(Mlog(M)),避免有序集合过大(大key问题) |
hgetall, hkeys | O(N),避免哈希过大(大key问题) |
lrange, ltrim, lindex, linsert | O(S+N),避免列表太大(大key问题) |
Redis集群
阿里云Redis集群加了一层Proxy做转发,可以带来比较好的兼容性和扩展性。
Redis Proxy是集群的访问入口,挡住用户短连接。Redis Proxy和后端Redis之间使用长连接,可以最优的发挥Redis性能。
兼容性
Redis Proxy可以把多key命令拆分后转发到各个Redis节点上。比如说mget key1 key2 key3,key1和key3在Redis1上,key2在Redis2上,Redis Proxy会拆分成mget key1 key3和mget key2两个命令分别转发到Redis1和Redis2上。
Redis Proxy同时支持select命令。
扩展性
Redis Proxy承担了三次握手建连接、认证的过程,同时Redis Proxy可以做到无状态,方便扩展。
Redis集群常见慢查问题
排队
Redis Proxy内核为每个user connection维护了一个Session,Session中有一个队列,按照FIFO顺序记录了所有已经收到但还没有给客户端返回的Request。
所有Request按照Key映射到各个Redis分片上,类似上图所展示的,不同Redis分片上的Request并发处理,就有快有慢,而Response必须按照FIFO的顺序给客户端返回。假如Request1比Request2慢,这也就意味着在Session中Request1必须等待Request2处理完以后一起给客户端返回。如果Request1是一个耗时100ms的hgetall命令,那么Request2也会在Session中排队等待,客户端观察到的响应时间最少也是100ms。和上面Redis类似,一个慢会影响到其他大量的Request跟着慢。
多key命令
上面提到过,多key命令会拆分转发到多个Redis节点,拆分的所有子命令都返回聚合以后给客户端回复,这意味着任何一个子命令波动(Redis节点有慢查,网络抖动),这个请求的命令就会变慢,这个请求变慢意味着会有其他请求在排队,都会变慢。
mget k1 k2 k3被拆分分别转发到两个Redis节点上,mget k1 k3很快处理完,mget k2因为网络抖动,或者前面有一个hgetall正在执行,100ms以后才返回,这意味着Request2, 3也都必须等待mget k2。
多key命令因为会涉及多个节点,所以更容易受到影响。
Redis Proxy的slowlog
上面提到Redis的Slowlog只包含了命令的实际CPU处理时间,并没有包含网络IO, 协议编解码的时间。在集群模式下,Redis Proxy的slowlog从收到并解析出用户的Request开始计算,直到给用户返回Response截止,包含了与后端Redis的网络传输,Redis的协议处理,命令处理等所有环节。
Max RT & Avg RT
阿里云Redis Proxy上会统计Avg RT和Max RT(RT就是上面提到的从收到Request到给客户端回复Reponse的时间),Avg RT就是统计周期内所有Request的sum(RT) / Request数量,Max RT则是统计周期内RT最大的那个Request。
可以看到有很多原因可能导致Redis出现慢查,如果Max RT经常飙高可以根据控制台上的各项监控排查一下是否有上面提到的问题。如果只是偶发的Max RT毛刺,在排除本身有O(N)命令导致的情况下,可以结合QPS看下发生频率,假如一个Redis集群有100W QPS,一个小时出现一次Max RT的毛刺,算下来平均10亿个请求才会出现一个,基本上在网络抖动允许范围内。
慢查定位
控制台上查看Redis服务监控,包括流量、QPS、CPU、Avg Rt、Max RT等,如果监控数据比较正常,而业务感知到和Redis监控不一致的超时,可以看下ECS测是否存在干扰、客户端统计是否合理。也可以直接在ECS层直接抓包确定是否有网络重传或者其他原因。
如果发现监控数据不符合预期,可以继续在监控上看是否有比较耗时的命令,同时也可以控制台的“日志管理”中查看慢日志,对集群来来说,慢日志包括Redis Proxy层采集的和Redis采集的,找到罪魁祸首。
如果只是频率很小的Max RT抖动,只要业务没有影响,可以忽略,如果抖动较大可以提工单排查。