这个问题本质是在问什么情况下redis的主线程会被阻塞住。
在不考虑硬件资源差异的情况下,redis变慢有以下可能:
a. 使用复杂度过高的命令
发生原因:使用O(N)及以上复杂度的指令,而且N非常大,阻塞主线程;
特征:redis的OPS(即客户端每秒对redis的操作次数)不高,但CPU很高;
排查方式:开启和查看slowlog慢日志;
解决方法:
避免在redis内聚合数据(如Sort、SUNION等),而是在业务代码聚合;
执行 O(N) 命令时,每次获取N尽量少的数据;
b. big key操作
发生原因:一个key的value很大时,redis分配内存(写入该key)和释放内存(删除该key)以及读取该key比较耗时,阻塞主线程;
特征:slowlog中发现某个操作是常数复杂度的set/del,但是也记录到slowlog中的情况。
排查方式:
--bigkeys,其本质是在内部执行了 SCAN 命令,然后针对 key 的类型,分别执行 STRLEN、LLEN、HLEN、SCARD、ZCARD 命令,来获取 String 类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数;
redis-rdb-tools工具离线排查;
排查注意事项:
bigkey 扫描时,Redis 的 OPS 会突增,最好控制一下扫描的频率(指定 -i 参数,它表示扫描过程中每次扫描后休息的时间间隔,单位是秒)
bigkey 扫描容器类型(List、Hash、Set、ZSet)的 key,只能扫描出元素最多的 key。但一个 key 的元素多,不一定表示占用内存也多。
解决方法:
删除不常用的bigkey,改用db存储;
对常用的bigkey拆分为多个小key;
如何删除big key:
Redis 4.0以前,del 只能在主线程删除,对于容器型key,del的时间复杂度是O(M),其中M是容器类型key包含的元素个数。
可以使用 hscan + hdel、sscan + srem、ltrim、zremrangebyrank每次删除容器的部分元素。
Redis 4.0 以上用 UNLINK 命令替代 DEL,此命令可以把释放 key 内存的操作,放到后台线程中去执行;
Redis 6.0 以上可开启 lazy-free 机制,在执行 DEL 命令时,释放内存也会放到后台线程中执行;
c. key集中过期
特征:redis变慢的时间点很有规律,例如某个整点,或者每间隔多久就会发生一波延迟。
原因:为什么key集中过期会让redis变慢?这涉及到redis的主动过期策略。
Redis 每隔 100 毫秒就会从设置了过期时间的全局哈希表中随机取出 20 个 key,删除其中过期的 key。如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒,才会退出循环。该过程在 Redis 主线程中执行的,会阻塞主线程,且不会记录到slowlog中。
排查方式:
代码层面:检查业务代码出现的expireat / pexpireat 命令。
运维层面:监控 Redis 的各项运行状态数据(执行 INFO 命令就可拿到这个实例的运行数据)。重点关注 expired_keys 这一项,它代表整个实例累计删除过期 key 的数量。 当这个指标在很短时间内突增,需要及时报警出来,然后与业务应用报慢的时间点进行对比分析。
解决方法:
key 增加一个随机过期时间;
开启 lazy-free 机制,避免阻塞主线程;
lazy-free有很多参数,例如 lazyfree-lazy-user-del = yes 表示用户的del操作采用lazy free机制;lazyfree-lazy-expire = yes表示redis主动清理过期key时采用lazy free机制;
d. 实例内存达到 maxmemory 上限
原因:实例内存达到上限时,每次执行写入新key前都需要按照淘汰策略删除旧key,淘汰策略算法是有一定复杂度的。因此在内存达到上限时,OPS越大,延迟越高。
特征:redis实例占用内存达到 maxmemory;
解决方案:
避免存储 bigkey,降低释放内存的耗时;
淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整);
拆分实例,扩大内存;
开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行(配置 lazyfree-lazy-eviction = yes)
e. fork耗时严重
特征:Redis 延迟变大,都发生在 Redis 后台 RDB 和 AOF rewrite 期间;
原因:RDB 和 AOF rewrite 在执行时,主进程会fork出一个子进程进行。主进程需要拷贝自己的内存页表(注意是拷贝内存页表,而非拷贝内存空间)给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时。在完成 fork 之前,整个 Redis 实例会被阻塞住。
排查方式:执行 INFO 命令,查看 latest_fork_usec 项(fork的耗时),单位微秒。
解决方法:
控制 Redis 实例的内存尽量在 10G 以下;
合理配置持久化策略:在 slave 节点执行 RDB 备份;
Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久;
调大复制积压缓冲区(repl-backlog-size),避免主从同步发生全量同步;
f.开启内存大页
原因:应用程序向操作系统申请内存时,是按4KB的内存页进行申请的 。 Linux 支持内存大页机制,允许应用程序以 2MB 大小为单位申请内存。
这在fork子进程的copy on write时会导致,客户端即便只修改 10B 的数据,Redis 的主进程为这10B的数据申请 2MB,申请内存的耗时变长。
解决方法:
关闭系统的内存大页机制。
g.绑定CPU
redis节点绑定单个CPU会导致主进程和子进程竞争CPU资源,而影响到客户端请求;
不要绑定单个CPU即可。
h.系统内存不足
我们知道系统内存不足时,虚拟内存技术会将部分内存换入磁盘。频繁的缺页会导致换入换出的磁盘IO次数增加,使redis操作不再是单纯的内存操作。
先找到 Redis 的进程 ID
$ ps -aux | grep redis-server
查看 Redis Swap 使用情况
$ cat /proc/$pid/smaps | egrep '^(Swap|Size)'
Size: 1256 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 63488 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 65404 kB
Swap: 0 kB
Size: 1921024 kB
Swap: 0 kB
每一行 Size 表示 Redis 所用的一块内存大小,Size 下面的 Swap 就表示这块 Size 大小的内存,有多少数据已经被换到磁盘上了,如果这两个值相等,说明这块内存的数据都已经完全被换到磁盘上了。
解决方法:
增加机器的内存,或者采用redis cluster分布式架构;
整理redis的内存碎片,redis 4.0版本提供了整理碎片的功能,但可能会导致 Redis 性能下降,因为这是在主线程中执行的,需要提前测试评估它对 Redis 的影响。。
# 开启自动内存碎片整理(总开关)
activedefrag yes
# 内存使用 100MB 以下,不进行碎片整理
active-defrag-ignore-bytes 100mb
# 内存碎片率超过 10%,开始碎片整理
active-defrag-threshold-lower 10
# 内存碎片率超过 100%,尽最大努力碎片整理
active-defrag-threshold-upper 100
# 内存碎片整理占用 CPU 资源最小百分比
active-defrag-cycle-min 1
# 内存碎片整理占用 CPU 资源最大百分比
active-defrag-cycle-max 25
# 碎片整理期间,对于 List/Set/Hash/ZSet 类型元素一次 Scan 的数量
active-defrag-max-scan-fields 1000
i. 网络带宽过载
原因:redis流量太大,导致带宽占满,响应返回客户端被阻塞或者接收redis操作请求被阻塞。
排查方式:监控Redis 机器的网络流量,在网络流量达到一定阈值时提前报警。
解决方法:
增加带宽;
一主多从,让流量负载均衡;
检查是否有热key,有则对热key冗余存储到其他redis节点并分担流量;