民工哥死磕Redis教程(二十 ):性能优化与问题排查

2023年 7月 10日 27.3k 0

前言

你们是否遇到过以下这些场景:

  • 在 Redis 上执行同样的命令,为什么有时响应很快,有时却很慢?
  • 为什么 Redis 执行 SET、DEL 命令耗时也很久?
  • 为什么我的 Redis 突然慢了一波,之后又恢复正常了?
  • 为什么我的 Redis 稳定运行了很久,突然从某个时间点开始变慢了?

Redis真的变慢了吗?

首先,在开始之前,你需要弄清楚Redis是否真的变慢了?

如果你发现你的业务服务 API 响应延迟变长,首先你需要先排查服务内部,究竟是哪个环节拖慢了整个服务。

比较高效的做法是,在服务内部集成链路追踪(打印日志的方式也可以),也就是在服务访问外部依赖的出入口,记录每次请求外部依赖的响应延时。

如果你发现确实是操作 Redis 的这条链路耗时变长了,那么此刻你需要把焦点关注在业务服务到 Redis 这条链路上。

Redis这条链路变慢的原因可能也有 2 个:

  • 业务服务器到 Redis 服务器之间的网络存在问题,例如网络线路质量不佳,网络数据包在传输时存在延迟、丢包等情况;
  • Redis 本身存在问题,需要进一步排查是什么原因导致 Redis 变慢。

天坑的我,两种都遇到了,哈哈哈哈

第一种情况发生的概率比较小,如果有,找网络运维。我们这篇文章,重点关注的是第二种情况。更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis,本系列持续更新中。

什么是基准性能?

排除网络原因,如何确认你的 Redis 是否真的变慢了?首先,你需要对 Redis 进行基准性能测试,了解你的 Redis 在生产环境服务器上的基准性能。基准性能就是指 Redis 在一台负载正常的机器上,其最大的响应延迟和平均响应延迟分别是怎样的?

  • 方式一:redis-cli --intrinsic-latency
  • 方式二:redis-benchmark

使用复杂度过高的命令

你需要去查看一下 Redis 的慢日志slowlog(又不会!)。Redis 提供了慢日志命令的统计功能,它记录了有哪些命令在执行时耗时比较久。 redis.config文件:

  • 慢日志的阈值:CONFIG SET slowlog-log-slower-than 5000
  • 只保留最近 500 条慢日志 : CONFIG SET slowlog-max-len 500
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"

通过查看慢日志,我们就可以知道在什么时间点,执行了哪些命令比较耗时。

  • 经常使用 O(N) 以上复杂度的命令,例如 keys、flushdb类命令。
  • 使用O(N) 复杂度的命令,但 N 的值非常大。如:hgetall、lrange、smembers、zrange等并非不能使用,但是需要明确N的值。

第一种情况导致变慢的原因在于,Redis 在操作内存数据时,时间复杂度过高,要花费更多的 CPU资源。

第二种情况导致变慢的原因在于,Redis 一次需要返回给客户端的数据过多,更多时间花费在数据协议的组装和网络传输过程中。

另外,如果你的应用程序操作 Redis 的QPS不是很大,但 Redis 实例的 CPU 使用率却很高,那么很有可能是使用了复杂度过高的命令导致的。Redis 是单线程处理客户端请求的,如果你经常使用以上命令,那么当Redis处理客户端请求时,一旦前面某个命令发生耗时,就会导致后面的请求发生排队,对于客户端来说,响应延迟也会变长民工哥死磕Redis教程(二十 ):性能优化与问题排查

操作 bigkey

你查询慢日志发现,并不是复杂度过高的命令导致的,而都是 SET / DEL 这种简单命令出现在慢日志中,那么你就要怀疑你的实例否写入了bigkeyRedis 在写入数据时,需要为新的数据分配内存,相对应的,当从 Redis 中删除数据时,它会释放对应的内存空间。如果一个 key 写入的 value 非常大,那么 Redis 在分配内存时就会比较耗时。同样的,当删除这个 key 时,释放内存也会比较耗时,这种类型的 key 我们一般称之为 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)

每种 ** 数据类型(5个基础类型,不是全部数据) ** 所占用的最大内存 / 拥有最多元素的 key 是哪一个,以及每种数据类型在整个实例中的占比和平均大小 / 元素数量。使用这个命令的原理,就是 Redis 在内部执行了 SCAN 命令,遍历整个实例中所有的 key,然后针对 key 的类型,分别执行 STRLEN、LLEN、HLEN、SCARD、ZCARD 命令,来获取 String 类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数。更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis ,本系列持续更新中。

当执行这个命令时,要注意 2 个问题:

  • 对线上实例进行 bigkey 扫描时,Redis 的 OPS ( 每秒操作次数)会突增,为了降低扫描过程中对 Redis 的影响,最好控制一下扫描的频率,指定-i参数即可,它表示扫描过程中每次扫描后休息的时间间隔,单位是秒
  • 扫描结果中,对于容器类型(List、Hash、Set、ZSet)的 key,只能扫描出元素最多的 key。但一个 key 的元素多,不一定表示占用内存也多,你还需要根据业务情况,进一步评估内存占用情况。
  • 第二种:rdb_bigkeys工具,go写的一款工具,分析rdb文件,找出文件中的大key,直接导出到csv文件,方便查看,个人推荐使用该工具去查找大key。

工具地址:https://github.com/weiyanwei412/rdb_bigkeys

针对 bigkey 导致延迟的问题,有什么好的解决方案呢?

  • 拒绝bigkey(十分推荐)
    • 导致redis阻塞
    • 网络拥塞
    • 过期删除:设置了过期时间,当它过期后,会被删除,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞Redis的可能性。
  • Redis 是 4.0 以上版本,用 UNLINK 命令替代 DEL,此命令可以把释放 key 内存的操作,放到后台线程中去执行,从而降低对 Redis 的影响;
  • Redis 是 4.0 以上版本,可以开启 lazy-free 机制(lazyfree-lazy-user-del = yes),在执行 DEL 命令时,释放内存也会放到后台线程中执行

bigkey 在很多场景下,依旧会产生性能问题。例如,bigkey 在分片集群模式下,对于数据的迁移也会有性能影响,数据过期、数据淘汰、透明大页,都会受到bigkey的影响。

集中过期

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

Redis对于过期键有三种清除策略:

被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key;

主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的keyRedis 内部维护了一个定时任务,默认每隔 100 毫秒就会从全局的过期哈希表中随机取出 20 个 key,然后删除其中过期的 key,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒,才会退出循环。**这个主动过期 key 的定时任务,是在 Redis 主线程中执行的。**

当前已用内存超过maxmemory限定时,触发内存淘汰策略。

也就是说如果在执行主动过期的过程中,出现了需要大量删除过期 key 的情况,那么此时应用程序在访问Redis时,必须要等待这个过期任务执行结束,Redis 才可以服务这个客户端请求。此时需要过期删除的是一个 bigkey,那么这个耗时会更久。而且,这个操作延迟的命令并不会记录在慢日志中。慢日志中没有操作耗时的命令,但我们的应用程序却感知到了延迟变大,其实时间都花费在了删除过期 key 上,这种情况我们需要尤为注意。 更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis,本系列持续更新中。

解决方法
  • 集中过期 key 增加一个随机过期时间,把集中过期的时间打散,降低 Redis 清理过期key的压力;
  • Redis 是 4.0 以上版本,可以开启lazy-free机制,当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程。

实例内存达到上限

原因在于,当 Redis 内存达到 maxmemory 后,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据写进来。

  • 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+版本支持)

Redis 的淘汰数据的逻辑与删除过期 key 的一样,也是在命令真正执行之前执行的,也就是说它也会增加我们操作 Redis 的延迟,而且,写 OPS 越高,延迟也会越明显。Redis 实例中还存储了 bigkey,那么在淘汰删除 bigkey 释放内存时,也会耗时比较久。 优化建议:

  • 避免存储 bigkey,降低释放内存的耗时;
  • 淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整);
  • 拆分实例,把淘汰key的压力分摊到多个实例上;
  • 如果使用的是 Redis 4.0 以上版本,开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行(配置 lazyfree-lazy-eviction = yes);

持久化/同步影响

fork耗时严重

操作 Redis 延迟变大,都发生在 Redis 后台 RDB 和AOF rewrite 期间,那你就需要排查,在这期间有可能导致变慢的情况。当 Redis 开启了后台 RDB 和 AOF rewrite 后,在执行时,它们都需要主进程创建出一个子进程进行数据的持久化。主进程创建子进程,会调用操作系统提供的fork函数。而 fork 在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时。而且这个 fork 过程会消耗大量的CPU资源,在完成fork之前,整个 Redis 实例会被阻塞住,无法处理任何客户端请求。如果此时你的 CPU 资源本来就很紧张,那么 fork 的耗时会更长,甚至达到秒级,这会严重影响 Redis 的性能。

你可以在 Redis 上执行 INFO 命令,查看 latest_fork_usec 项,单位微秒。 民工哥死磕Redis教程(二十 ):性能优化与问题排查数据持久化会生成 RDB 之外,当主从节点第一次建立数据同步时,主节点也创建子进程生成 RDB,然后发给从节点进行一次全量同步,所以,这个过程也会对 Redis 产生性能影响。

优化
  • 控制 Redis 实例的内存:尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久
  • 合理配置数据持久化策略:在 slave 节点执行 RDB 备份,推荐在低峰期执行,而对于丢失数据不敏感的业务(例如把 Redis 当做纯缓存使用),可以关闭 AOF 和 AOF rewrite
  • Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久。
  • 降低主从库全量同步的概率:适当调大· repl-backlog-size· 参数,避免主从全量同步。

开启内存大页

什么是内存大页?

我们都知道,应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的内存页大小是 4KB

Linux 内核从2.6.38开始,支持了内存大页机制,该机制允许应用程序以2MB大小为单位,向操作系统申请内存。应用程序每次向操作系统申请的内存单位变大了,但这也意味着申请内存的耗时变长。主进程fork子进程后,此时的主进程依旧是可以接收写请求的,而进来的写请求,会采用 Copy On Write(写时复制)的方式操作内存数据。主进程一旦有数据需要修改,Redis 并不会直接修改现有内存中的数据,而是先将这块内存数据拷贝出来,再修改这块新内存的数据。写时复制你也可以理解成,谁需要发生写操作,谁就需要先拷贝,再修改。

注意,主进程在拷贝内存数据时,这个阶段就涉及到新内存的申请,如果此时操作系统开启了内存大页,那么在此期间,客户端即便只修改 10B 的数据,Redis 在申请内存时也会以 2MB 为单位向操作系统申请,申请内存的耗时变长,进而导致每个写请求的延迟增加,影响到 Redis 性能。如果这个写请求操作的是一个 bigkey,那主进程在拷贝这个 bigkey 内存块时,一次申请的内存会更大,时间也会更久。可见,bigkey 在这里又一次影响到了性能。更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis ,本系列持续更新中。 民工哥死磕Redis教程(二十 ):性能优化与问题排查

开启AOF

  • AOF 配置为 appendfsync always,那么 Redis 每处理一次写操作,都会把这个命令写入到磁盘中才返回,整个过程都是在主线程执行的,这个过程必然会加重Redis写负担。
  • AOF 配置为appendfsync noRedis 每次写操作只写内存,什么时候把内存中的数据刷到磁盘,交给操作系统决定,对 Redis 的性能影响最小,但当 Redis 宕机时,会丢失一部分数据,为了数据的安全性。
  • AOF 配置为appendfsync everysec ,当 Redis 后台线程在执行 AOF 文件刷盘时,如果此时磁盘的IO负载很高,那这个后台线程在执行刷盘操作(fsync系统调用)时就会被阻塞住。此时的主线程依旧会接收写请求,紧接着,主线程又需要把数据写到文件内存中(write 系统调用),但此时的后台子线程由于磁盘负载过高,导致 fsync 发生阻塞,迟迟不能返回,那主线程在执行 write 系统调用时,也会被阻塞住,直到后台线程fsync执行完成后,主线程执行 write 才能成功返回。

民工哥死磕Redis教程(二十 ):性能优化与问题排查我总结了以下几种情况,你可以参考进行问题排查:

  • 子进程正在执行 AOF rewrite,这个过程会占用大量的磁盘 IO 资源;
  • 有其他应用程序在执行大量的写文件操作,也会占用磁盘 IO 资源;

Redis 的 AOF 后台子线程刷盘操作,撞上了子进程 AOF rewriteRedis 提供了一个配置项,当子进程在 AOF rewrite 期间,可以让后台子线程不执行刷盘(不触发 fsync 系统调用)操作。

这相当于在 AOF rewrite期间,临时把 appendfsync 设置为了 none,配置如下:

# AOF rewrite 期间,AOF 后台子线程不进行刷盘操作
# 相当于在这期间,临时把 appendfsync 设置为了 none
no-appendfsync-on-rewrite yes

开启这个配置项,在 AOF rewrite 期间,如果实例发生宕机,那么此时会丢失更多的数据,性能和数据安全性,你需要权衡后进行选择。

碎片整理

Redis 的数据都存储在内存中,当我们的应用程序频繁修改Redis中的数据时,就有可能会导致 Redis产生内存碎片。内存碎片会降低 Redis 的内存使用率,我们可以通过执行INFO命令,得到这个实例的内存碎片率:民工哥死磕Redis教程(二十 ):性能优化与问题排查used_memory 表示 Redis 存储数据的内存大小, used_memory_rss 表示操作系统实际分配给 Redis 进程的大小。 mem_fragmentation_ratio> 1.5,说明内存碎片率已经超过了 **50%**,这时我们就需要采取一些措施来降低内存碎片了。

解决的方案一般如下:
  • 如果你使用的是 Redis 4.0 以下版本,只能通过重启实例来解决
  • 如果你使用的是 Redis 4.0 版本,它正好提供了自动碎片整理的功能,可以通过配置开启碎片自动整理但是,开启内存碎片整理,它也有可能会导致Redis性能下降。

原因在于,Redis 的碎片整理工作是也在主线程中执行的,当其进行碎片整理时,必然会消耗CPU资源,产生更多的耗时,从而影响到客户端的请求。

其他原因

  • 频繁短连接:你的业务应用,应该使用长连接操作 Redis,避免频繁的短连接。
  • 其它程序争抢资源:其它程序占用 CPU、内存、磁盘资源,导致分配给 Redis 的资源不足而受到影响。

总结

你应该也发现了,Redis 的性能问题,涉及到的知识点非常广,几乎涵盖了 CPU、内存、网络、甚至磁盘的方方面面,同时,你还需要了解计算机的体系结构,以及操作系统的各种机制。

资源使用角度来看,包含的知识点如下:

  • CPU相关:使用复杂度过高命令、数据的持久化,都与耗费过多的 CPU 资源有关
  • 内存相关:bigkey 内存的申请和释放、数据过期、数据淘汰、碎片整理、内存大页、内存写时复制都与内存息息相关
  • 磁盘相关:数据持久化、AOF 刷盘策略,也会受到磁盘的影响
  • 网络相关:短连接、实例流量过载、网络流量过载,也会降低 Redis 性能
  • 计算机系统:CPU 结构、内存分配,都属于最基础的计算机系统知识
  • 操作系统:写时复制、内存大页、Swap、CPU 绑定,都属于操作系统层面的知识

优化的一些建议

  • 1、尽量使用短的key

当然在精简的同时,不要为了key的“见名知意”。对于value有些也可精简,比如性别使用0、1。

  • 2、避免使用keys *

keys *, 这个命令是阻塞的,即操作执行期间,其它任何命令在你的实例中都无法执行。当redis中key数据量小时到无所谓,数据量大就很糟糕了。所以我们应该避免去使用这个命令。可以去使用SCAN,来代替。

  • 3、在存到Redis之前先把你的数据压缩下

redis为每种数据类型都提供了两种内部编码方式,在不同的情况下redis会自动调整合适的编码方式。

  • 4、设置key有效期

我们应该尽可能的利用key有效期。比如一些临时数据(短信校验码),过了有效期Redis就会自动为你清除!

  • 5、选择回收策略(maxmemory-policy)

当Redis的实例空间被填满了之后,将会尝试回收一部分key。根据你的使用方式,强烈建议使用 volatile-lru(默认) 策略——前提是你对key已经设置了超时。但如果你运行的是一些类似于 cache 的东西,并且没有对 key 设置超时机制,可以考虑使用 allkeys-lru 回收机制,具体讲解查看 。maxmemory-samples 3 是说每次进行淘汰的时候 会随机抽取3个key 从里面淘汰最不经常使用的(默认选项)。

maxmemory-policy 六种方式 :
volatile-lru   #只对设置了过期时间的key进行LRU(默认值)
allkeys-lru    #是从所有key里 删除 不经常使用的key
volatile-random #随机删除即将过期key
allkeys-random #随机删除
volatile-ttl   #删除即将过期的
noeviction  #永不过期,返回错误
  • 6、使用bit位级别操作和byte字节级别操作来减少不必要的内存使用
bit位级别操作:GETRANGE, SETRANGE, GETBIT and SETBIT

byte字节级别操作:GETRANGE and SETRANGE
  • 7、尽可能地使用hashes哈希存储
  • 8、当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能
    • 数据持久化时需要在持久化和延迟/性能之间做相应的权衡.
  • 9、想要一次添加多条数据的时候可以使用管道
  • 10、限制redis的内存大小(64位系统不限制内存,32位系统默认最多使用3GB内存)

数据量不可预估,并且内存也有限的话,尽量限制下redis使用的内存大小,这样可以避免redis使用swap分区或者出现OOM错误。(使用swap分区,性能较低,如果限制了内存,当到达指定内存之后就不能添加数据了,否则会报OOM错误。可以设置maxmemory-policy,内存不足时删除数据)

  • 11、SLOWLOG [get/reset/len]
slowlog-log-slower-than #它决定要对执行时间大于多少微秒(microsecond,1秒 = 1,000,000 微秒)的命令进行记录。
slowlog-max-len #它决定 slowlog 最多能保存多少条日志,当发现redis性能下降的时候可以查看下是哪些命令导致的。

来源:https://blog.csdn.net/weixin_42128977/article/details/127622146

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论