Redis为什么变慢了?常见延迟问题定位与分析
Redis作为内存数据库,拥有非常高的性能,单个实例的QPS能够达到10W左右。但我们在使用 Redis 时,经常时不时会出现访问延迟很大的情况,如果你不知道 Redis 的内部实现原理,在排查问题时就会一头雾水。
Redis出现访问延迟变大,都与我们的使用不当或运维不合理导致的。
以下这篇文章我们就来分析一下 Redis 在使用过程中,经常会遇到的延迟问题以及如何定位和分析。
文章目录
链路追踪
基准性能
使用了复杂度过高的命令
操作、存储大的bigkey
大量数据集中过期
Redis实例内存使用达到了上限
fork子进程导致响应变慢
开启了AOF存储
链路追踪
链路追踪就是在服务访问外部依赖的出入口,记录下每次请求外部依赖的响应延时。
如果发现确实是操作 Redis 的这条链路耗时变长了,那么此时需要把焦点关注在业务服务到 Redis 这条链路上。
从服务到 Redis 这条链路变慢的原因可能有网络(网络不好,数据包在传输时存在延迟、丢包等),或者是 Redis本身的问题,吸引进一步排查问题的所在。
如果是服务器之间网络存在问题,这时候就要找网络运维同事,让其协助解决网络问题。
基准性能
简单来讲,基准性能就是指 Redis 在一台负载正常的机器上,其最大的响应延迟和平均响应延迟分别是怎样的?
如果对比曾经上线压测过的正常情况下Redis响应时长,确实是执行命令耗时变长了,那再进行下面的分析操作:
查看Redis所在服务器的网络使用情况,排查是否为网络负载达到瓶颈
查看应用服务器与Redis服务器之间网络响应时长
使用redis-cli -h 172.0.0.1 -p 6379 --intrinsic-latency 60,查看Redis在60秒内的最大网络响应延迟(单位:微秒)
这里是说在60秒内的最大响应时长是84微秒(0.084毫秒)
还可以使用redis-cli -h 172.0.0.1 -p 6973 --latency-history -i 1查看redis在一段时间内最大、最小、平均访问延迟。
这里是每1秒钟输出一次采样的redis平均操作耗时,平均耗时这个指标分布在0.08ms和0.13ms之间
如果观察到,这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为这个 Redis 实例确实变慢了
接下来我们一步步来分析可能导致 Redis 变慢的原因是什么。
使用了复杂度过高的命令
如果在使用Redis时,发现访问延迟突然增大,如何进行排查?
这种情况可以通过增加 Redis 慢日志统计来看
首先设置 Redis的 慢日志阈值,只有超过阈值的命令才会被记录,这里的单位是微秒,例如设置慢日志的阈值为 5 毫秒,同时设置只保留最近 500 条慢日志记录:
//命令执行超过 5 毫秒记录慢日志
CONFIG SET slowlog-log-slower-than 5000
//只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500
1
2
3
4
记录慢日志之后,可以使用 slowlog get 5 来查看 top 5 的操作慢的命令:
127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 32693 # 慢日志ID
2) (integer) 1593763337 # 执行时间
3) (integer) 5299 # 执行耗时(微秒)
4) 1) "LRANGE" # 具体执行的命令和参数
2) "user_list_2000"
3) "0"
4) "-1"
2) 1) (integer) 32692
2) (integer) 1593763337
3) (integer) 5044
4) 1) "GET"
2) "book_price_1000"
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
通常来说命令执行慢有两种原因:
使用了O(n)以上的复杂度很高的命令,例如 sort、sunion 等,这种原因通常表现为 Redis 的 CPU 使用率很高,查询数据量不一定很大
单次操作返回的数据量太大,导致 Redis 在数据组装和返回传输上面耗时
由于开源版本6.0之前的 Redis 是单线程的,上面两种原因会导致其他命令阻塞,表现为整体 Redis 操作命令都很耗时。
针对这种情况如何解决呢?
尽量不使用 O(N) 以上复杂度过高的命令,对于数据的聚合操作,放在客户端做
执行 O(N) 命令,保证 N 尽量的小(推荐 N <= 300),每次获取尽量少的数据,让 Redis 可以及时处理返回
操作、存储大的bigkey
如果代码内部都是的一些简单操作命令,例如set、get、hset、hget等,但是慢日志里面仍然能看到这些命令,那就要检查是否操作了大key,这种类型的 key 我们一般称之为 bigkey。因为大 key 在内存分配的时候会比较耗时,释放内存也更耗时,这是导致 Redis 响应慢的原因。
Redis 提供了扫描 bigkey 的命令,我们可以使用redis-cli -h 172.0.01 -p 6379 --bigkeys -i 0.01查看,一个实例中 bigkey 的分布情况:
Sampled 829675 keys in the keyspace!
Total key length in bytes is 10059825 (avg len 12.13)
Biggest string found 'key:291880' has 10 bytes
Biggest list found 'mylist:004' has 40 items
Biggest set found 'myset:2386' has 38 members
Biggest hash found 'myhash:3574' has 37 fields
Biggest zset found 'myzset:2704' has 42 members
36313 strings with 363130 bytes (04.38% of keys, avg size 10.00)
787393 lists with 896540 items (94.90% of keys, avg size 1.14)
1994 sets with 40052 members (00.24% of keys, avg size 20.09)
1990 hashs with 39632 fields (00.24% of keys, avg size 19.92)
1985 zsets with 39750 members (00.24% of keys, avg size 20.03)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
这个命令的原理其实是在 Redis 内部执行了 scan 命令,遍历 Redis 里面所有的key,分别执行 strlen、llen、hlen、scard、zcard 命令,来获取 String 类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数。
当执行这个命令时,需要注意一下几点问题:
进行 bigkey 扫描时,会导致 Redis 的 OPS 陡增,生产环境一定要控制好执行的频率, -i参数后面就是执行的间隔时间,单位是秒
而对于容器类型的key,只能扫描出元素最多的key,但元素最多的key不一定占用内存最多,这一点需要我们注意下。不过使用这个命令一般我们是可以对整个实例中key的分布情况有比较清晰的了解。
针对 bigkey 导致延迟的问题。我们可以这么做:
尽量避免写入 bigkey
针对大key的问题,Redis官方在4.0版本推出了lazy-free的机制,用于异步释放大key的内存,降低对Redis性能的影响。即使这样,我们也不建议使用大key,大key在集群的迁移过程中,也会影响到迁移的性能
大量数据集中过期
我们是否在操作 Redis 的过程中,每次都在一个时间点就会发生一波延迟。这时候我们需要排查一下是否存在大量的 key 在同一个时间过期呢。
为什么集中过期就会导致 Redis 延迟变大?
集中过期导致 Redis 响应慢的原因是跟 Redis 的过期策略有关,主要有两种:
主动过期:Redis 内部维护了一个定时任务,默认是间隔 100毫秒 就会从全局的过期哈希表中随机取出 20 个 key进行删除,如果过期哈希表里面的 key 的数量占比超过了总 key 数量的 25%,则会重复该删除操作,直到过期 key 数量占比下降到 25% 以下或者删除操作超过了 25 毫秒才退出循环
被动过期是当访问某个 key 的时候会判断他是否过期,如果过期了,则直接删除
Redis 设置集中过期的命令是 expireat 和 pexpireat,排查集中过期的时候可以搜索这些关键字。
解决这种集中过期有两种方案:
集中过期 key 增加一个随机过期时间,把集中过期的时间打散,比如(redis.expireat(key, expire_time + random(1000)))降低 Redis 清理过期 key 的压力
可以开启 lazy-free 机制(lazyfree-lazy-expire yes),当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程
Redis实例内存使用达到了上限
如果 Redis 实例设置了内存上限 maxmemory,内存使用也达到了 maxmemory,这也可能导致 Redis 响应慢。
Redis 作为缓存使用时,通常会给这个实例设置一个内存上限 maxmemory,然后设置一个数据淘汰策略。
当实例的内存达到了 maxmemory 后,会发现之后每次写入新数据,操作延迟变大了,这是为什么?
Redis 数据达到上限了,每次写入新数据之前,Redis 需要先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据放进内存中,这个踢出同样是耗时的操作,淘汰数的逻辑跟删除过期key的逻辑一样,所以淘汰会增加 Redis 响应时间。
淘汰策略:
allkeys-lru:不管 key 是否设置了过期,淘汰最近最少访问的 key
volatile-lru:只淘汰最近最少访问、并设置了过期时间的 key
allkeys-random:不管 key 是否设置了过期,随机淘汰 key
volatile-random:只随机淘汰设置了过期时间的 key
allkeys-ttl:不管 key 是否设置了过期,淘汰即将过期的 key
noeviction:不淘汰任何 key,实例内存达到 maxmeory 后,再写入新数据直接返回错误
allkeys-lfu:不管 key 是否设置了过期,淘汰访问频率最低的 key(4.0+版本支持)
volatile-lfu:只淘汰访问频率最低、并设置了过期时间 key(4.0+版本支持)
具体使用哪种策略需要根据场景来配置,一般都是使用allkeys-lru或volatile-lru淘汰策略。
如果此时 Redis 的实例中还存储了 bigkey,那么在淘汰删除 bigkey 释放内存时也会耗时比较久。
fork子进程导致响应变慢
如果 Redis 开启了自动生成 RDB 和 AOF 重写功能,那么有可能在后台生成 RDB 和 AOF 重写时导致 Redis 的访问延迟增大,而等这些任务执行完毕后,延迟情况消失。
如果遇到这种情况,一般就是执行生成RDB和AOF重写任务导致的。
生成RDB和AOF都需要父进程fork出一个子进程进行数据的持久化,在fork执行过程中,父进程需要拷贝内存页表给子进程,如果整个实例内存占用很大,那么需要拷贝的内存页表会比较耗时,此过程会消耗大量的CPU资源,在完成fork之前,整个实例会被阻塞住,无法处理任何请求,如果此时CPU资源紧张,那么fork的时间会更长,甚至达到秒级。这会严重影响Redis的性能。
除了因为备份的原因生成 RDB 之外,在主从节点第一次建立数据同步时,主节点也会生成 RDB 文件给从节点进行一次全量同步,这时也会对 Redis 产生性能影响。
可以在 Redis 上面执行 info 命令查看 latest_fork_usec 是多少,单位是微秒。
# Stats
total_connections_received:2
total_commands_processed:4
instantaneous_ops_per_sec:0
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
# 上一次 fork 耗时,单位微秒
latest_fork_usec:0
migrate_cached_sockets:0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
要想避免 Redis 在 fork 的时候耗时太久,可以采取一下方案进行:
redis内存不要设置太大,最好不要超过10G,因为fork子进程的时候跟实例大小有关,实例越大,fork耗时越久
配置redis持久化策略的时候,注意RDB的操作尽量在业务低峰期进行,对于可以从其他非内存数据库恢复的数据,可以关闭AOF rewrite,这样可以增强性能
如果有条件,尽量不要把redis部署在虚拟机上,因为虚拟机fork子进程比物理机更耗时
调大主从库全量数据同步的概率,可以调到repl-backlog-size参数,避免主从全量同步
开启了AOF存储
关于数据持久化方面,还有影响 Redis 性能的因素,这次我们重点来看 AOF 数据持久化。
如果了解 Redis 执行 AOF 命令的原理,就知道为什么开启 AOF 会导致 Redis 响应慢了。
当 Redis 开启 AOF 后,其工作原理如下:
在redis执行了写操作命令后,会先把这个命令写入到AOF文件内存中,由write系统调用
根据redis配置的AOF刷盘策略,再把内存数据刷到磁盘中,由fsync系统调用
为了保证 AOF 文件数据的安全性,提供了3中刷盘策略,其中各有利弊:
appendfsync always:主线程每次执行写操作后立即刷盘,此方案会占用比较大的磁盘 IO 资源,但数据安全性最高。这种操作会阻塞redis操作命令,redis的写命令执行需要等数据刷盘了才能返回结果,这样会严重拖慢redis的执行性能,生产环境一般都不会这么玩。
appendfsync no:主线程每次写操作只写内存就返回,内存数据什么时候刷到磁盘,交由操作系统决定,此方案对性能影响最小,但数据安全性也最低,Redis 宕机时丢失的数据取决于操作系统刷盘时机。如果我们的redis存储的这些数据只用作缓存,丢失了可以去关系型数据库查,对系统无影响,那可以考虑这种方式,如果不是这样,那生产环境也不会采用这种策略。
appendfsync everysec:主线程每次写操作只写内存就返回,然后由后台线程每隔 1 秒执行一次刷盘操作(触发fsync系统调用),此方案对性能影响相对较小,但当 Redis 宕机时会丢失 1 秒的数据。这种策略取了中间平衡,但是仍然存在Redis 延迟变大的情况,甚至会阻塞整个 Redis。