Redis性能调优:深入剖析变慢原因及应对策略

如果观察到,这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为这个 Redis 实例确实变慢了。

1.如何查看实例的运行延迟

(1)redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60 执行该命令,就可以测试出这个实例 60 秒内的最大响应延迟,如下图:

从输出结果可以看到,这 60 秒内的最大响应延迟为 72 微秒(0.072毫秒)

(2)redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1 通过该命令查看一段时间内 Redis 的最小、最大、平均访问延迟,如下图:

以上输出结果是,每间隔 1 秒,采样 Redis 的平均操作耗时,其结果分布在 0.08 ~ 0.13 毫秒之间。

(3)执行redis-benchmark -h 127.0.0.1 -p 6379 -t set,lpush -n 100000 -c 50,这个命令会模拟50个并发客户端,每个客户端发送10万个请求,请求命令为set,lpush(如果省略每种命令都会执行一遍),对指定的Redis服务器进行性能测试。

2.使用复杂度过高的命令

通过SLOWLOG get 5命令,查看 Redis 的慢日志(slowlog),找出执行慢的命令操作。

如果应用程序执行的Redis命令有以下特点,那么有可能会导致操作延迟变大:

  • 经常使用 O(N) 以上复杂度的命令,例如 SORT、SUNION、ZUNIONSTORE 聚合类命令: 导致变慢的原因在于,Redis 在操作内存数据时,时间复杂度过高,要花费更多的 CPU 资源 优化建议:对于数据的聚合操作,放在客户端做

  • 使用 O(N) 复杂度的命令,但 N 的值非常大: 导致变慢的原因在于,Redis 一次需要返回给客户端的数据过多,更多时间花费在数据协议的组装和网络传输过程中 优化建议: 执行 O(N) 命令,保证 N 尽量的小(推荐 N <= 300),每次获取尽量少的数据,让 Redis 可以及时处理返回 N表示命令的个数

3.操作bigkey

如果查询慢日志发现,并不是复杂度过高的命令导致的,而都是 SET / DEL 这种简单命令出现在慢日志中,那么就要怀疑实例否写入了 bigkey。

如果一个key写入的value非常大,那么Redis在分配内存时就会比较耗时。同样的,当删除这个key时,释放内存也会比较耗时。

通过使用bigkey的命令,扫描大值key, 其实,使用这个命令的原理,就是 Redis 在内部执行了 SCAN 命令,遍历整个实例中所有的 key,然后针对 key 的类型,分别执行 STRLEN、LLEN、HLEN、SCARD、ZCARD 命令,来获取 String 类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数。

优化建议:

  • 业务应用尽量避免写入 bigkey

  • 如果你使用的 Redis 是 4.0 以上版本,用 UNLINK 命令替代 DEL,此命令可以把释放 key 内存的操作,放到后台线程中去执行,从而降低对 Redis 的影响

  • 如果你使用的 Redis 是 6.0 以上版本,可以开启 lazy-free 机制(lazyfree-lazy-user-del = yes),在执行 DEL 命令时,释放内存也会放到后台线程中执行

4.集中过期

如果发现,平时在操作 Redis 时,并没有延迟很大的情况发生,但在某个时间点突然出现一波延时,其现象表现为:变慢的时间点很有规律,例如某个整点,或者每间隔多久就会发生一波延迟。如果是出现这种情况,那么需要排查一下,业务代码中是否存在设置大量 key 集中过期的情况。

(1)为什么集中过期会导致 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 增加一个随机过期时间,把集中过期的时间打散,降低 Redis 清理过期 key 的压力

  • 如果你使用的 Redis 是 4.0 以上版本,可以开启 lazy-free 机制,当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程。 lazyfree-lazy-expire yes

5.实例内存达到上限

如果你的Redis实例设置了内存上限maxmemory,那么也有可能导致Redis变慢,当Redis内存达到maxmemory后,每次写入新的数据之前,Redis必须先从实例中踢出一部分数据,让整个实例的内存维持在maxmemory之下,然后才能把新数据写进来。这个踢出旧数据的逻辑也是需要消耗时间的,而具体耗时的长短,要取决于你配置的淘汰策略。

需要注意的是,Redis 的淘汰数据的逻辑与删除过期 key 的一样,也是在命令真正执行之前执行的,也就是说它也会增加我们操作 Redis 的延迟,而且,写 OPS 越高,延迟也会越明显。

优化建议:

  • 避免存储 bigkey,降低释放内存的耗时

  • 淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整)

  • 拆分实例,把淘汰 key 的压力分摊到多个实例上

  • 如果使用的是 Redis 4.0 以上版本,开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行(配置 lazyfree-lazy-eviction = yes)

6.fork耗时严重

主进程创建子进程( RDB 和 AOF ),会调用操作系统提供的fork函数。而fork在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时。而且这个fork过程会消耗大量的CPU资源,在完成fork之前,整个Redis实例会被阻塞住,无法处理任何客户端请求。

(1)那如何确认确实是因为 fork 耗时导致的 Redis 延迟变大呢?

答:在Redis上执行INFO命令,查看latest_fork_usec项,单位微秒。latest_fork_usec:59477 这个时间就是主进程在fork子进程期间,整个实例阻塞无法处理客户端请求的时间, 如果发现这个耗时很久,就要警惕起来了,这意味在这期间,整个 Redis 实例都处于不可用的状态。

优化建议:

  • 控制 Redis 实例的内存:尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久

  • 合理配置数据持久化策略:在 slave 节点执行 RDB 备份,推荐在低峰期执行,而对于丢失数据不敏感的业务(例如把 Redis 当做纯缓存使用),可以关闭 AOF 和 AOF rewrite

  • Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久

  • 降低主从库全量同步的概率:适当调大 repl-backlog-size 参数(环形内存的大小),避免主从全量同步

7.开启内存大页

我们都知道,应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的内存页大小是 4KB。Linux 内核从 2.6.38 开始,支持了内存大页机制,该机制允许应用程序以 2MB 大小为单位,向操作系统申请内存。应用程序每次向操作系统申请的内存单位变大了,但这也意味着申请内存的耗时变长。

(1)这对 Redis 会有什么影响呢?

答:主要跟redis的Copy On Write 机制有关。 主进程在拷贝内存数据时,这个阶段就涉及到新内存的申请,如果此时操作系统开启了内存大页,那么在此期间,客户端即便只修改 10B 的数据,Redis 在申请内存时也会以 2MB 为单位向操作系统申请,申请内存的耗时变长,进而导致每个写请求的延迟增加,影响到 Redis 性能。 同样地,如果这个写请求操作的是一个 bigkey,那主进程在拷贝这个 bigkey 内存块时,一次申请的内存会更大,时间也会更久。

优化建议:

对于 Redis 这种对性能和延迟极其敏感的数据库来说,我们希望Redis在每次申请内存时,耗时尽量短,所以不建议你在Redis机器上开启这个机制。

查看 Redis 机器是否开启了内存大页 :cat /sys/kernel/mm/transparent_hugepage/enabled

关闭redis内存大页机制 :echo never > /sys/kernel/mm/transparent_hugepage/enabled

8.AOF刷盘机制

根据业务需求选择AOF三种不同的刷盘时机,不同的处理策略会提升redis的性能。

在 Redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填:

  • Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;主线程

  • Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;AOF 刷盘线程(异步)

  • No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

9.使用Swap

如果发现 Redis 突然变得非常慢,每次的操作耗时都达到了几百毫秒甚至秒级,那此时就需要检查Redis是否使用到了 Swap,在这种情况下Redis基本上已经无法提供高性能的服务了。

(1)什么是swap?

答:如果对操作系统有些了解,就会知道操作系统为了缓解内存不足对应用程序的影响,允许把一部分内存中的数据换到磁上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域,就是 Swap。问题就在于,当内存中的数据被换到磁盘上后,Redis 再访问这些数据时,就需要从磁盘上读取,访问磁盘的速度要比访问内存慢几百倍!

(2) 查看Redis 进程是否使用到了 Swap?

答:# 先找到 Redis 的进程 ID $ ps -aux | grep redis-server

查看 Redis Swap 使用情况 $ cat /proc/$pid/smaps | egrep '^(Swap|Size)'

输出结果如下:

每一行Size表示Redis所用的一块内存大小,Size下面的Swap就表示这块Size大小的内存,有多少数据已经被换到磁盘上了,如果这两个值相等,说明这块内存的数据都已经完全被换到磁盘上了。如果只是少量数据被换到磁盘上,例如每一块Swap占对应Size的比例很小,那影响并不是很大。如果是几百兆甚至上GB的内存被换到了磁盘上,那么你就需要警惕了,这种情况Redis的性能肯定会急剧下降。

优化建议:

  • 增加机器的内存,让 Redis 有足够的内存可以使用

  • 整理内存空间,释放出足够的内存供 Redis 使用,然后释放 Redis 的 Swap,让 Redis 重新使用内存

10.碎片整理

Redis 的数据都存储在内存中,当我们的应用程序频繁修改 Redis 中的数据时,就有可能会导致 Redis 产生内存碎片,内存碎片会降低 Redis 的内存使用率

(1)如何计算碎片率?

答:可以通过执行 INFO 命令,得到这个实例的内存碎片率, mem_fragmentation_ratio = used_memory_rss / used_memory。其中 used_memory 表示 Redis 存储数据的内存大小,而 used_memory_rss 表示操作系统实际分配给 Redis 进程的大小。如果 mem_fragmentation_ratio > 1.5,说明内存碎片率已经超过了 50%,这时我们就需要采取一些措施来降低内存碎片了。

优化建议:

  • 如果你使用的是 Redis 4.0 以下版本,只能通过重启实例来解决

  • 如果你使用的是 Redis 4.0 版本,它正好提供了自动碎片整理的功能,可以通过配置开启碎片自动整理:开启自动内存碎片整理(总开关):activedefrag yes;当碎片达到 100mb 时,开启内存碎片整理:active-defrag-ignore-bytes 100mb; 当碎片超过 10% 时,开启内存碎片整理:active-defrag-threshold-lower 10;内存碎片超过 100%,则尽最大努力整理:active-defrag-threshold-upper 100;内存自动整理占用资源最小百分比:active-defrag-cycle-min 25; 内存自动整理占用资源最大百分比:active-defrag-cycle-max 75。

但是,开启内存碎片整理,它也有可能会导致 Redis 性能下降,原因在于,Redis 的碎片整理工作是也在主线程中执行的,当其进行碎片整理时,必然会消耗 CPU 资源,产生更多的耗时,从而影响到客户端的请求。所以,当你需要开启这个功能时,最好提前测试评估它对 Redis 的影响。

相关推荐
Psycho_MrZhang几秒前
MySQL JOIN算法实现和选择
数据库·mysql·算法
LIU_Skill1 分钟前
MySQL基础 -----MySQL数据类型
linux·数据库·mysql
DS小龙哥2 分钟前
【CC2530开发基础篇】继电器模块使用
数据库·mongodb·nosql
m0_607548766 分钟前
ubuntu中mysql只能通过sudo才能进入如何解决
数据库·mysql
小码快撩9 分钟前
VUE的缓存问题
前端·vue.js·缓存
蜡笔小鑫️10 分钟前
海量数据库使用操作
数据库
小马爱打代码11 分钟前
SpringBoot集成Canal实现MySQL实时同步数据到Redis
spring boot·redis·mysql
Victory_orsh13 分钟前
Hadoop运行Mapreduce问题集锦——Ubuntu虚拟机配置
大数据·数据库·hadoop
黑客老李27 分钟前
【$25000】利用Zendesk Nday获取漏洞赏金
运维·服务器·数据库·sql·安全·web安全·小程序
林慢慢脑瓜子嗡嗡的28 分钟前
如何使用数据泵工具将数据从一个Oracle数据库迁移到另一个Oracle数据库?
数据库·oracle