Redis - 慢日志原因及排查思路
1、先进行基准测试,查看redis是否存在查询过慢情况,根据自己的情况而定
2、检查网络连接是否出现延迟,数据丢包问题(可能性小)
3、开启慢查询日志,通过日志可以清楚知道哪些命令比较耗时,同时避免使用复杂O(N) 等命令
4、查看是否写入了bigkey,避免写入bigkey
5、查看是否出现大量集中的key过期,合适设置过期淘汰机制,监控 expired_keys 这一项,它代表整个实例到目前为止,累计删除过期 key 的数量,出现过多是进行及时处理
6、设置内存上限 maxmemory值,然后设置一个数据淘汰策略
7、避免频繁的与redis进行短连接
8、避免其它程序争抢资源,最好redis服务器专项专用
https://way2j.com/a/767
简介
Redis速度是很快的,性能很高。但是,Redis有时候会存在执行很慢、性能很差的情况。
Redis执行命令流程:
Redis是单线程操作,如果在Redis中执行耗时较长的操作,就会阻塞其他请求了。
Redis客户端执行一条命令,分为4部分:发送命令=>命令排队=> 命令执行=> 返回结果
慢查询在第三步,统计第三步执行时间。
排查步骤
第一步:确定Redis是否真的变慢了
1)排查思路:
若发现业务服务 API 响应延迟变长,首先需要排查服务内部,究竟是哪个环节拖慢了整个服务。
比较高效的做法:在服务内部集成链路追踪,也就是在服务访问外部依赖的出入口,记录下每次请求外部依赖的响应延时。若发现确实是操作 Redis 的这条链路耗时变长了,那此刻需要把焦点关注在业务服务到 Redis 这条链路上。
从业务服务到 Redis 这条链路变慢的原因可能也有 2 个:
- 业务服务器到 Redis 服务器之间的网络存在问题,例如网络线路质量不佳,网络数据包在传输时存在延迟、丢包等情况
- Redis 本身存在问题,需要进一步排查是什么原因导致 Redis 变慢
重点关注的是第二种情况。如何确认Redis 是否真的变慢了?
2)基准性能
首先,需要对 Redis 进行基准性能测试,了解Redis 在生产环境服务器上的基准性能。
- 基准性能就是指 Redis 在一台负载正常的机器上,其最大的响应延迟和平均响应延迟分别是怎样的。
- 为什么要测试基准性能?参考别人提供的响应延迟,判断自己的 Redis 是否变慢不行吗?
-
答案是否定的。
因为 Redis 在不同的软硬件环境下,它的性能是各不相同的。
例如,机器配置比较低,当延迟为 2ms 时,就认为 Redis 变慢了,但是如果硬件配置比较高,那么在你的运行环境下,可能延迟是 0.5ms 时就可以认为 Redis 变慢了。
所以,只有了解了Redis 在生产环境服务器上的基准性能,才能进一步评估,当其延迟达到什么程度时,才认为 Redis 确实变慢了。
3)具体如何做?
为了避免业务服务器到 Redis 服务器之间的网络延迟,需要直接在 Redis 服务器上测试实例的响应延迟情况。执行以下命令,就可以测试出这个实例 60 秒内的最大响应延迟:
$ redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
Max latency so far: 1 microseconds.
Max latency so far: 15 microseconds.
Max latency so far: 17 microseconds.
Max latency so far: 18 microseconds.
Max latency so far: 31 microseconds.
Max latency so far: 32 microseconds.
Max latency so far: 59 microseconds.
Max latency so far: 72 microseconds.
1428669267 total runs (avg latency: 0.0420 microseconds / 42.00 nanoseconds per run).
Worst run took 1429x longer than the average latency.
从输出结果可以看到,这 60 秒内的最大响应延迟为 72 微秒(0.072毫秒)。
还可以使用以下命令,查看一段时间内 Redis 的最小、最大、平均访问延迟:
$ redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1
min: 0, max: 1, avg: 0.13 (100 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.12 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.10 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (98 samples) -- 1.00 seconds range
min: 0, max: 1, avg: 0.08 (99 samples) -- 1.01 seconds range
...
以上输出结果是,每间隔 1 秒,采样 Redis 的平均操作耗时,其结果分布在 0.08 ~ 0.13 毫秒之间。
了解了基准性能测试方法,那就可以按照以下几步,来判断Redis 是否真的变慢了:
- 在相同配置的服务器上,测试一个正常 Redis 实例的基准性能
- 找到认为可能变慢的 Redis 实例,测试这个实例的基准性能
- 如果观察到,这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为这个 Redis 实例确实变慢了
第二步:查看slowlog(慢日志)
Redis 提供了慢日志命令的统计功能,它记录了有哪些命令在执行时耗时比较久。
查看 Redis 慢日志之前,需要设置慢日志的阈值。例如,设置慢日志的阈值为 5 毫秒,并且保留最近 500 条慢日志记录:
# 命令执行耗时超过 5 毫秒,记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500
设置完成之后,所有执行的命令如果操作耗时超过了 5 毫秒,都会被 Redis 记录下来。
此时,可以执行以下命令,就可以查询到最近记录的慢日志:
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) "user_info:1000"
...
通过查看慢日志,就可以知道在什么时间点,执行了哪些命令比较耗时。
那Redis变慢的原因可能有哪些?(redis 命令相关)
原因1:使用复杂度过高的命令
- 经常使用 O(N) 以上复杂度的命令,例如 SORT、SUNION、ZUNIONSTORE 聚合类命令。原因:Redis 在操作内存数据时,时间复杂度过高,要花费更多的 CPU 资源
- 使用 O(N) 复杂度的命令,但 N 值非常大。原因:Redis 一次需要返回给客户端的数据过多,更多时间花费在数据协议的组装和网络传输过程中
另外,还可以从资源使用率层面来分析,如果应用程序操作 Redis 的 OPS 不是很大,但 Redis 实例的 CPU 使用率却很高,那么很有可能是使用了复杂度过高的命令导致的。
除此之外,Redis 是单线程处理客户端请求的,如果经常使用以上命令,那么当 Redis 处理客户端请求时,一旦前面某个命令发生耗时,就会导致后面的请求发生排队,对于客户端来说,响应延迟也会变长。
解决方案
- 尽量不使用 O(N) 以上复杂度过高的命令,对于数据的聚合操作,放在客户端做
- 执行 O(N) 命令,保证 N 尽量的小(推荐 N <= 300),每次获取尽量少的数据,让 Redis 可以及时处理返回
原因2:操作bigkey(value很大)
1)排查思路
若查询慢日志发现,并不是复杂度过高的命令导致,而都是 SET / DEL 这种简单命令出现在慢日志中,那么就要怀疑实例否写入了 bigkey。
2)导致变慢的操作
Redis 在写入数据时,需要为新的数据分配内存,相对应,当从 Redis 中删除数据时,它会释放对应的内存空间。如果一个 key 写入的 value 非常大,那么 Redis 在分配内存时就会比较耗时。同样的,当删除这个 key 时,释放内存也会比较耗时,这种类型的 key 一般称之为 bigkey。此时,需要检查业务代码是否存在写入 bigkey 的情况。需要评估写入一个 key 的数据大小,尽量避免一个 key 存入过大的数据。
3)找出bigkey
若已经写入了 bigkey,可以扫描出实例中 bigkey 的分布情况吗?
答案是可以的。Redis 提供了扫描 bigkey 的命令,执行以下命令就可以扫描出,一个实例中 bigkey 的分布情况,输出结果是以类型维度展示的:
$ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01
...
-------- summary -------
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)
输出结果可以很清晰地看到,每种数据类型所占用的最大内存 / 拥有最多元素的 key 是哪一个,以及每种数据类型在整个实例中的占比和平均大小 / 元素数量。
其实,使用这个命令的原理,就是 Redis 在内部执行了 SCAN 命令,遍历整个实例中所有的 key,然后针对 key 的类型,分别执行 STRLEN、LLEN、HLEN、SCARD、ZCARD 命令,来获取 String 类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数。
执行这个命令时,要注意 2 个问题:
- 对线上实例进行 bigkey 扫描时,Redis 的 OPS 会突增,为了降低扫描过程中对 Redis 的影响,最好控制一下扫描的频率,指定 -i 参数即可,它表示扫描过程中每次扫描后休息的时间间隔,单位是秒
- 扫描结果中,对于容器类型(List、Hash、Set、ZSet)的 key,只能扫描出元素最多的 key。但一个 key 的元素多,不一定表示占用内存也多,你还需要根据业务情况,进一步评估内存占用情况
4)针对 bigkey 导致延迟的解决方案
- 业务应用尽量避免写入 bigkey
- 将释放key的操作放到后台线程执行
- Redis4.0 以上版本:用 UNLINK 命令替代 DEL,此命令可以把释放 key 内存的操作,放到后台线程中去执行,从而降低对 Redis 的影响
- Redis6.0 以上版本:可以开启 lazy-free 机制(lazyfree-lazy-user-del = yes),在执行 DEL 命令时,释放内存也会放到后台线程中执行
但即便可以使用方案 2,也不建议在实例中存入 bigkey。
这是因为 bigkey 在很多场景下,依旧会产生性能问题。例如,bigkey 在分片集群模式下,对于数据的迁移也会有性能影响,以及后面即将讲到的数据过期、数据淘汰、透明大页,都会受到 bigkey 的影响。
原因3:集中过期
1)排查思路
如果发现平时在操作 Redis 时,并没有延迟很大的情况发生,但在某个时间点突然出现一波延时,其现象表现为:变慢的时间点很有规律,例如某个整点,或者每间隔多久就会发生一波延迟。
如果是出现这种情况,那需要排查一下,业务代码中是否存在设置大量 key 集中过期的情况。
2)导致变慢的原因
如果有大量的 key 在某个固定时间点集中过期,在这个时间点访问 Redis 时,就有可能导致延时变大。
为什么集中过期会导致 Redis 延迟变大?这就需要了解 Redis 的过期策略是怎样的。
Redis 的过期数据采用被动过期 + 主动过期两种策略:
- 被动过期:只有当访问某个 key 时,才判断这个 key 是否已过期,如果已过期,则从实例中删除
- 主动过期:Redis 内部维护了一个定时任务,默认每隔 100 毫秒(1秒10次)就会从全局的过期哈希表中随机取出 20 个 key,然后删除其中过期的 key,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒,才会退出循环
注意,这个主动过期 key 的定时任务,是在 Redis 主线程中执行的。也就是说如果在执行主动过期的过程中,出现了需要大量删除过期 key 的情况,那么此时应用程序在访问 Redis 时,必须要等待这个过期任务执行结束,Redis 才可以服务这个客户端请求。此时就会出现,应用访问 Redis 延时变大。
如果此时需要过期删除的是一个 bigkey,那么这个耗时会更久。而且,这个操作延迟的命令并不会记录在慢日志中。因为慢日志中只记录一个命令真正操作内存数据的耗时,而 Redis 主动删除过期 key 的逻辑,是在命令真正执行之前执行的。所以,此时会看到,慢日志中没有操作耗时的命令,但应用程序却感知到了延迟变大,其实时间都花费在了删除过期 key 上,这种情况需要尤为注意。
3)解决方案
1)如果你使用的 Redis 是 4.0 以上版本,可以开启 lazy-free 机制集中过期 key 增加一个随机过期时间,把集中过期的时间打散,降低 Redis 清理过期 key 的压力,这样当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程
Redis 4.0 以上版本,开启 lazy-free 机制:
# 释放过期 key 的内存,放到后台线程执行
lazyfree-lazy-expire yes
2)集中过期 key 增加一个随机过期时间,把集中过期的时间打散,降低 Redis 清理过期 key 的压力
在设置 key 的过期时间时,增加一个随机时间,伪代码可以这么写:
# 在过期时间点之后的 5 分钟内随机过期掉
redis.expireat(key, expire_time + random(300))
这样一来,Redis 在处理过期时,不会因为集中删除过多的 key 导致压力过大,从而避免阻塞主线程。
运维层面
另外,除了业务层面的优化和修改配置之外,还可以通过运维手段及时发现这种情况。
运维层面,需要把 Redis 的各项运行状态数据监控起来,在 Redis 上执行 INFO 命令就可以拿到这个实例所有的运行状态数据。
在这里需要重点关注 expired_keys 这一项,它代表整个实例到目前为止,累计删除过期 key 的数量。
需要把这个指标监控起来,当这个指标在很短时间内出现了突增,需要及时报警出来,然后与业务应用报慢的时间点进行对比分析,确认时间是否一致,如果一致,则可以确认确实是因为集中过期 key 导致的延迟变大。
原因4:开启AOF
当 Redis 开启 AOF 后,其工作原理如下:
- Redis 执行写命令后,把这个命令写入到 AOF 内存中(write 系统调用)
- Redis 根据配置的 AOF 刷盘策略,把 AOF 内存数据刷到磁盘上(fsync 系统调用)
为了保证 AOF 文件数据的安全性,Redis 提供了 3 种刷盘机制:
- appendfsync everysec(默认)
- 主线程每次写操作只写内存就返回,然后由后台线程每隔 1 秒执行一次刷盘操作(触发fsync系统调用)
- 此方案对性能影响相对较小,但当 Redis 宕机时会丢失 1 秒的数据
- appendfsync alwaysappendfsync no
- 主线程每次执行写操作后立即刷盘
- 此方案会占用比较大的磁盘 IO 资源,但数据安全性最高
- 主线程每次写操作只写内存就返回,内存数据什么时候刷到磁盘,交由操作系统决定
- 方案对性能影响最小,但数据安全性也最低,Redis 宕机时丢失的数据取决于操作系统刷盘时机。
开启AOF导致变慢的原因
依次来分析,这几个机制对性能的影响:
appendfsync always
- Redis 每处理一次写操作,都会把这个命令写入到磁盘中才返回,整个过程都是在主线程执行的,这个过程必然会加重 Redis 写负担。
- 原因也很简单,操作磁盘要比操作内存慢几百倍,采用这个配置会严重拖慢 Redis 的性能,因此我不建议你把 AOF 刷盘方式配置为 always。
appendfsync no
- Redis 每次写操作只写内存,什么时候把内存中的数据刷到磁盘,交给操作系统决定,此方案对 Redis 的性能影响最小,但当 Redis 宕机时,会丢失一部分数据。
appendfsync everysec
- Redis 主线程写完内存后就返回,具体的刷盘操作是放到后台线程中执行的,后台线程每隔 1 秒把内存中的数据刷到磁盘中。兼顾了性能又尽可能地保证数据安全,是不是觉得很完美?
- 这种方案还是存在导致 Redis 延迟变大的情况发生,甚至会阻塞整个 Redis。
为什么?我把 AOF 最耗时的刷盘操作,放到后台线程中也会影响到 Redis 主线程?
- 当 Redis 后台线程在执行 AOF 文件刷盘时,如果此时磁盘的 IO 负载很高,那这个后台线程在执行刷盘操作(fsync系统调用)时就会被阻塞住。
- 此时的主线程依旧会接收写请求,紧接着,主线程又需要把数据写到文件内存中(write 系统调用),但此时的后台子线程由于磁盘负载过高,导致 fsync 发生阻塞,迟迟不能返回,此时因为内存没剩余空间,那主线程在执行 write 系统调用时,也会被阻塞住,直到后台线程 fsync 执行完成后,主线程执行 write 才能成功返回。
当 Redis 后台线程在执行 AOF 文件刷盘时,如果此时磁盘的 IO 负载很高,那这个后台线程在执行刷盘操作(fsync系统调用)时就会被阻塞住。
此时的主线程依旧会接收写请求,紧接着,主线程又需要把数据写到文件内存中(write 系统调用),但此时的后台子线程由于磁盘负载过高,导致 fsync 发生阻塞,迟迟不能返回,此时因为内存没剩余空间,那主线程在执行 write 系统调用时,也会被阻塞住,直到后台线程 fsync 执行完成后,主线程执行 write 才能成功返回。
解决方案
什么情况下会导致磁盘 IO 负载过大?以及如何解决这个问题呢?
总结了以下几种情况,你可以参考进行问题排查:
- 子进程正在执行 AOF rewrite,这个过程会占用大量的磁盘 IO 资源
- 有其他应用程序在执行大量的写文件操作,也会占用磁盘 IO 资源
对于情况1,Redis 的 AOF 后台子线程刷盘操作,撞上了子进程 AOF rewrite
这怎么办?难道要关闭 AOF rewrite 才行?
幸运的是,Redis 提供了一个配置项,当子进程在 AOF rewrite 期间,可以让后台子线程不执行刷盘(不触发 fsync 系统调用)操作。这相当于在 AOF rewrite 期间,临时把 appendfsync 设置为了 none,配置如下:
1 2 3 | # AOF rewrite 期间,AOF 后台子线程不进行刷盘操作 # 相当于在这期间,临时把 appendfsync 设置为了 none no-appendfsync- on -rewrite yes |
当然,开启这个配置项,在 AOF rewrite 期间,如果实例发生宕机,那么此时会丢失更多的数据,性能和数据安全性,需要权衡后进行选择。
对于情况2:占用磁盘资源的是其他应用程序
那就比较简单了,需要定位到是哪个应用程序在大量写磁盘,然后把这个应用程序迁移到其他机器上执行就好了,避免对 Redis 产生影响。
当然,如果对 Redis 的性能和数据安全都有很高的要求,那么我建议从硬件层面来优化,更换为 SSD 磁盘,提高磁盘的 IO 能力,保证 AOF 期间有充足的磁盘资源可以使用。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库