Redis为什么慢了

Redis作为我们常用的缓存中间件广泛应用到生产中,应用过程中或多或少可能会出现一系列的性能问题,Redis响应速度变慢了如何处理呢?命令执行变慢了如何处理呢?Redis某个时间段突然变慢了之后又恢复正常了如何排查呢?等等这篇文章来记录下Redis性能问题的排查方案。

Redis基线测试

某业务逻辑处理变慢了首先需要考虑的是到底是不是Redis自身影响的呢?因为整个业务链路包括多个参与者(APP、Redis、Mysql等等)每个参与者的阻塞都有可能导致整个业务的性能问题,这时需要知晓每个参与者的响应时间,如果真是Redis链路响应过慢,就可以进行接下来的排查。

Redis响应过慢判断指标可以通过基准性能来确定

什么是基准性能呢?

基准性能指的就是在一个系统在低压力、无干扰下的基本性能,不同的服务器配置,基准性能不同,不能一概而论

如下所示,得到100秒内的最大延时(测试机器配置较低,指标仅参考

### 表示100秒内的响应延迟
### 如果redis是默认端口可以采用下面的命令,如果端口非默认需要指定端口./redis-cli -p port --intrinsic-latency 100
[root@zzf993 bin]# ./redis-cli --intrinsic-latency 100
Max latency so far: 1 microseconds.
Max latency so far: 7 microseconds.
Max latency so far: 12 microseconds.
Max latency so far: 34 microseconds.
Max latency so far: 70 microseconds.
Max latency so far: 3140 microseconds.
Max latency so far: 4210 microseconds.
Max latency so far: 23074 microseconds.
Max latency so far: 104617 microseconds.

2165236605 total runs (avg latency: 0.0462 microseconds / 46.18 nanoseconds per run).
Worst run took 2265206x longer than the average latency.

我们还能使用以下命令得到最小、最大、平均延时等

### i表示间隔多久统计一次单位秒
[root@zzf993 bin]# ./redis-cli --latency-history -i 1
min: 0, max: 2, avg: 0.27 (98 samples) -- 1.01 seconds range
min: 0, max: 7, avg: 0.33 (96 samples) -- 1.00 seconds range
min: 0, max: 2, avg: 0.21 (97 samples) -- 1.00 seconds range
min: 0, max: 2, avg: 0.18 (97 samples) -- 1.00 seconds range
min: 0, max: 6, avg: 0.30 (94 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.17 (98 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.24 (97 samples) -- 1.00 seconds range
min: 0, max: 2, avg: 0.19 (98 samples) -- 1.01 seconds range

知道基准性能概念后,我们可以按照如下步骤判断

  • 找配置相同的服务器得到基准性能。
  • 在可能变慢实例的服务器上得到最大延时。
  • 如果实例的最大延时是基准性能延时的2倍以上,说明是Redis实例内部的问题,可以继续排查。

Redis变慢排查方向

Redis实例确实出现变慢的情况,这时需要更加细致的排查,一般分为三大块Redis自身特性、文件系统、操作系统

Redis自身特性的影响

慢查询命令

在Redis中慢查询命令一般分为两类

  • 如SORT、聚合操作(SUNION、ZUNIONSTORE、SINTER)、KEYS等,因为操作内存数据需要花费更多的CPU资源。
  • 分段查询一次查询大量数据如(LRANGE key 0 N,N足够大)Redis需要返回给客户端大量的数据,更多的时间花费在网络传输上。

这两者的操作都是在主线程中完成,都有可能造成网络阻塞,但这只是有可能,我们可以通过慢查询日志slowlog观察,慢查询日志可以在redis.conf文件中配置参数slowlog-log-slower-than N表示执行时间超过N毫秒开始记录慢日志,当N=0时表示所有的命令都会记录到慢查询日志中测试时可以这样设置,查询慢日志命令是SLOWLOG get N获取最新的多少个慢查询日志,结果如下

127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 1946 ### 每个慢查询条目的唯一的递增标识符
   2) (integer) 1651750547 ### 处理记录命令的unix时间戳。
   3) (integer) 19 ### 命令执行所需的总时间,以微秒为单位。
   4) 1) "SLOWLOG" ### 组成该命令的参数数组
      2) "get"
      3) "20"
   5) "127.0.0.1:57602"
   6) ""

当slowlog中出现上述慢查询命令时,我们可以进行如下优化

  • 在服务端尽量不进行聚合运算等复杂操作,复杂操作交由客户端完成。
  • 如果是分段查询,可以少量多次查询,避免Redis的阻塞。

操作bigkey

如果在慢日志中包含有get、set等命令那就需要考虑bigkey的情况,因为get、set命令时间复杂度是O(1),在slowlog-log-slower-than N配置设置正常的情况下是不会出现这种情况的,最大可能就是bigkey,我们可以采用命令查看实例的bigkey分布情况

### 如果redis是默认端口可以采用下面的命令,如果端口非默认需要指定端口./redis-cli -p 6379 --bigkeys

[root@zzf993 bin]# ./redis-cli --bigkeys

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
## 得到比目前统计要大的bigkey(如果新加入的key比上面的bigkey要大就会统计到,如果小就不会统计)
[00.00%] Biggest list   found so far '"database"' with 3 items
[00.00%] Biggest string found so far '"key2"' with 4 bytes
[00.00%] Biggest list   found so far '"test3"' with 13 items
[00.00%] Biggest string found so far '"name"' with 8 bytes

-------- summary -------

Sampled 7 keys in the keyspace!
Total key length in bytes is 32 (avg len 4.57)

Biggest   list found '"test3"' has 13 items
Biggest string found '"name"' has 8 bytes

2 lists with 16 items (28.57% of keys, avg size 8.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
5 strings with 22 bytes (71.43% of keys, avg size 4.40)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)

如果是查询存在bigkey那么需要优化业务逻辑解决,如果是删除bigkey阻塞,可以优化如下

  • 如果Redis版本是4.0之后的那么将DEL改为UNLINK解决,该命令会在另一个线程中回收内存,非阻塞操作。
  • 如果Redis版本是4.0之后的也可以采用lazy-free方案,将redis.conf中设置lazyfree-lazy-server-del yes属性,效果和UNLINK类似。

即使删除有UNLINK方案和lazy-free同样需要注意的是在业务中应避免bigkey的存在,在很多场景bigkey都有性能限制。

key的过期操作

key过期后的删除机制分为两种情况,定时自动删除和被动删除。

  • 被动删除:是指当客户端访问某个key时,会判断这个key的是否已经过期,如果过期需要从实例中删除。
  • 定时自动删除:Redis中存在一个定时任务,默认每隔100毫秒会自动从过期哈希表中随机取出20个key(1秒就是200个),然后删除key,当过期key的比例超过25%时,将重复删除过程,直到过期key的比例小于25%为止。

定时自动删除这里的比例超过25%后重复删除过程这里会在主线程的运行,一但过期key的比例高那么将阻塞主线程。

而过期key的比例高换句话说就是key的集中过期,导致了定时自动删除拖延主线程任务处理时间,这个场景大部分出现在EXPIREAT给大量key设置相同的过期时间,解决办法有如下方案

  • 将过期时间上一个小的随机时间,确保单个时间点上不会有大量key过期的情况。
  • 如果Redis是4.0以上版本,可以开启lazy-free异步执行键值删除逻辑
### redis.conf中配置默认no不开启
lazyfree-lazy-expire yes

我们还需要关注实例过期key的数量,如果新增需要立即排查,我们可以通过如下命令

### 统计的是实例过期key的数量,如果激增表明key有集中失效的情况
[root@zzf993 bin]# ./redis-cli info | grep expired_keys
expired_keys:1

另外补充注意点,如果过期删除时删除的是一个bigkey,这时Redis的命令执行延时高,但是慢日志不会记录,主要耗时就在执行Redis的key值删除的逻辑上。

Redis内存达到maxmemory

在redis.conf文件中配置了最大内存后 maxmemory <bytes>如果内存使用量超过这个配置,那么内存淘汰机制就将触发,内存淘汰机制需要先按照淘汰规则淘汰部分key后才能让新数据写入,自然操作的延迟会增加。

另外需要注意的是,内存淘汰和删除是类似的,如果淘汰的是一个bigkey那么可能导致Redis命令的执行延时高

内存淘汰规则如下所示

  • noeviction:不淘汰任何 key,实例内存达到 maxmeory 后,再写入新数据直接返回错误
  • allkeys-lru:淘汰目标是所有的key,淘汰最近访问最少的。
  • allkeys-random:淘汰目标是所有的key,随机淘汰key。
  • allkeys-ttl:淘汰目标是所有的key,淘汰即将过期的 key。
  • volatile-lru:淘汰目标是设置了过期时间的 key,淘汰最近访问最少的。
  • volatile-random:淘汰目标是设置了过期时间的 key,随机淘汰key。
  • allkeys-lfu:淘汰目标是所有的key,淘汰访问频率最低的 key(4.0+版本支持)
  • volatile-lfu:淘汰目标是设置了过期时间的 key,只淘汰访问频率最低(4.0+版本支持)

lru规则就是每次从实例中随机抽取一定数量的key,淘汰一个最少使用的,然后将其余的放入到一个池子中,继续随机从实例中随机抽取一定数量的key,并与之前池子中的key作比较,选取一个最少使用的反复执行,将内存占用控制到maxmemory以下。

lfu规则是对lru的改进,lfu是保留访问相对频繁的key,丢弃访问不频繁的key,而lru是去淘汰最近访问最少的,相对概念稍有不同。

如果出现上述场景,可以进行如下调整

  • 拆分实例将淘汰key的压力均摊到多个实例上。
  • 淘汰策略变更,如果业务允许可以采取随机淘汰策略,随机淘汰策略比lru或lfu淘汰速度快。
  • 如果Redis版本是4.0以上版本,可以开启lazy-free方案修改redis.conf文件lazyfree-lazy-eviction yes

频繁短连接

如果客户端和服务端连接采用短连接的形式那么Redis的部分性能消耗在连接建立和释放上,可能会造成业务延迟,所以业务应用建议尽量使用长连接的形式。