Redis 愈来愈慢?常见延迟问题定位与分析

image
来源:http://kaito-kidd.com/2020/07...redis

Redis做为内存数据库,拥有很是高的性能,单个实例的QPS可以达到10W左右。但咱们在使用Redis时,常常时不时会出现访问延迟很大的状况,若是你不知道Redis的内部实现原理,在排查问题时就会一头雾水。数据库

不少时候,Redis出现访问延迟变大,都与咱们的使用不当或运维不合理致使的。缓存

这篇文章咱们就来分析一下Redis在使用过程当中,常常会遇到的延迟问题以及如何定位和分析。安全

使用复杂度高的命令

若是在使用Redis时,发现访问延迟忽然增大,如何进行排查?网络

首先,第一步,建议你去查看一下Redis的慢日志。Redis提供了慢日志命令的统计功能,咱们经过如下设置,就能够查看有哪些命令在执行时延迟比较大。app

首先设置Redis的慢日志阈值,只有超过阈值的命令才会被记录,这里的单位是微秒,例如设置慢日志的阈值为5毫秒,同时设置只保留最近1000条慢日志记录:运维

# 命令执行超过5毫秒记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近1000条慢日志
CONFIG SET slowlog-max-len 1000

设置完成以后,全部执行的命令若是延迟大于5毫秒,都会被Redis记录下来,咱们执行SLOWLOG get 5查询最近5条慢日志dom

127.0.0.1:6379> SLOWLOG get5
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"
...

经过查看慢日志记录,咱们就能够知道在什么时间执行哪些命令比较耗时,若是你的业务常用O(n)以上复杂度的命令,例如sort、sunion、zunionstore,或者在执行O(n)命令时操做的数据量比较大,这些状况下Redis处理数据时就会很耗时。异步

若是你的服务请求量并不大,但Redis实例的CPU使用率很高,颇有多是使用了复杂度高的命令致使的。性能

解决方案就是,不使用这些复杂度较高的命令,而且一次不要获取太多的数据,每次尽可能操做少许的数据,让Redis能够及时处理返回

存储大key

若是查询慢日志发现,并非复杂度较高的命令致使的,例如都是SET、DELETE操做出如今慢日志记录中,那么你就要怀疑是否存在Redis写入了大key的状况。

Redis在写入数据时,须要为新的数据分配内存,当从Redis中删除数据时,它会释放对应的内存空间。

若是一个key写入的数据很是大,Redis在分配内存时也会比较耗时。一样的,当删除这个key的数据时,释放内存也会耗时比较久

你须要检查你的业务代码,是否存在写入大key的状况,须要评估写入数据量的大小,业务层应该避免一个key存入过大的数据量

那么有没有什么办法能够扫描如今Redis中是否存在大key的数据吗?

Redis也提供了扫描大key的方法:

redis-cli -h $host -p $port --bigkeys -i 0.01

使用上面的命令就能够扫描出整个实例key大小的分布状况,它是以类型维度来展现的。

须要注意的是当咱们在线上实例进行大key扫描时,Redis的QPS会突增,为了下降扫描过程当中对Redis的影响,咱们须要控制扫描的频率,使用-i参数控制便可,它表示扫描过程当中每次扫描的时间间隔,单位是秒。

使用这个命令的原理,其实就是Redis在内部执行scan命令,遍历全部key,而后针对不一样类型的key执行strlen、llen、hlen、scard、zcard来获取字符串的长度以及容器类型(list/dict/set/zset)的元素个数。

而对于容器类型的key,只能扫描出元素最多的key,但元素最多的key不必定占用内存最多,这一点须要咱们注意下。不过使用这个命令通常咱们是能够对整个实例中key的分布状况有比较清晰的了解。

针对大key的问题,Redis官方在4.0版本推出了lazy-free的机制,用于异步释放大key的内存,下降对Redis性能的影响。即便这样,咱们也不建议使用大key,大key在集群的迁移过程当中,也会影响到迁移的性能,这个后面在介绍集群相关的文章时,会再详细介绍到。

集中过时

有时你会发现,平时在使用Redis时没有延时比较大的状况,但在某个时间点忽然出现一波延时,并且报慢的时间点颇有规律,例如某个整点,或者间隔多久就会发生一次。

若是出现这种状况,就须要考虑是否存在大量key集中过时的状况。

若是有大量的key在某个固定时间点集中过时,在这个时间点访问Redis时,就有可能致使延迟增长。

Redis的过时策略采用主动过时+懒惰过时两种策略:

•主动过时:Redis内部维护一个定时任务,默认每隔100毫秒会从过时字典中随机取出20个key,删除过时的key,若是过时key的比例超过了25%,则继续获取20个key,删除过时的key,循环往复,直到过时key的比例降低到25%或者此次任务的执行耗时超过了25毫秒,才会退出循环

•懒惰过时:只有当访问某个key时,才判断这个key是否已过时,若是已通过期,则从实例中删除

注意,Redis的主动过时的定时任务,也是在Redis**主线程**中执行的,也就是说若是在执行主动过时的过程当中,出现了须要大量删除过时key的状况,那么在业务访问时,必须等这个过时任务执行结束,才能够处理业务请求。此时就会出现,业务访问延时增大的问题,最大延迟为25毫秒。

并且这个访问延迟的状况,不会记录在慢日志里。慢日志中只记录真正执行某个命令的耗时,Redis主动过时策略执行在操做命令以前,若是操做命令耗时达不到慢日志阈值,它是不会计算在慢日志统计中的,但咱们的业务却感到了延迟增大。

此时你须要检查你的业务,是否真的存在集中过时的代码,通常集中过时使用的命令是expireatpexpireat命令,在代码中搜索这个关键字就能够了。

若是你的业务确实须要集中过时掉某些key,又不想致使Redis发生抖动,有什么优化方案?

解决方案是,在集中过时时增长一个`随机时间`,把这些须要过时的key的时间打散便可

伪代码能够这么写:

# 在过时时间点以后的5分钟内随机过时掉
redis.expireat(key, expire_time + random(300))

这样Redis在处理过时时,不会由于集中删除key致使压力过大,阻塞主线程。

另外,除了业务使用须要注意此问题以外,还能够经过运维手段来及时发现这种状况。

作法是咱们须要把Redis的各项运行数据监控起来,执行info能够拿到全部的运行数据,在这里咱们须要重点关注expired_keys这一项,它表明整个实例到目前为止,累计删除过时key的数量。

咱们须要对这个指标监控,当在很短期内这个指标出现突增时,须要及时报警出来,而后与业务报慢的时间点对比分析,确认时间是否一致,若是一致,则能够认为确实是由于这个缘由致使的延迟增大。

实例内存达到上限

有时咱们把Redis当作纯缓存使用,就会给实例设置一个内存上限maxmemory,而后开启LRU淘汰策略。

当实例的内存达到了maxmemory后,你会发现以后的每次写入新的数据,有可能变慢了。

致使变慢的缘由是,当Redis内存达到maxmemory后,每次写入新的数据以前,必须先踢出一部分数据,让内存维持在maxmemory之下。

这个踢出旧数据的逻辑也是须要消耗时间的,而具体耗时的长短,要取决于配置的淘汰策略:

•allkeys-lru:无论key是否设置了过时,淘汰最近最少访问的key

•volatile-lru:只淘汰最近最少访问并设置过时的key

•allkeys-random:无论key是否设置了过时,随机淘汰

•volatile-random:只随机淘汰有设置过时的key

•allkeys-ttl:无论key是否设置了过时,淘汰即将过时的key

•noeviction:不淘汰任何key,满容后再写入直接报错

•allkeys-lfu:无论key是否设置了过时,淘汰访问频率最低的key(4.0+支持)

•volatile-lfu:只淘汰访问频率最低的过时key(4.0+支持)

备注: allkeys-xxx表示从 全部的键值中淘汰数据,而 volatile-xxx表示从设置了 过时键的键值中淘汰数据。

具体使用哪一种策略,须要根据业务场景来决定。

咱们最常使用的通常是allkeys-lruvolatile-lru策略,它们的处理逻辑是,每次从实例中随机取出一批key(可配置),而后淘汰一个最少访问的key,以后把剩下的key暂存到一个池子中,继续随机取出一批key,并与以前池子中的key比较,再淘汰一个最少访问的key。以此循环,直到内存降到maxmemory之下。

若是使用的是allkeys-randomvolatile-random策略,那么就会快不少,由于是随机淘汰,那么就少了比较key访问频率时间的消耗了,随机拿出一批key后直接淘汰便可,所以这个策略要比上面的LRU策略执行快一些。

但以上这些逻辑都是在访问Redis时,真正命令执行以前执行的,也就是它会影响咱们访问Redis时执行的命令。

另外,若是此时Redis实例中有存储大key,那么在淘汰大key释放内存时,这个耗时会更加久,延迟更大,这须要咱们格外注意。

若是你的业务访问量很是大,而且必须设置maxmemory限制实例的内存上限,同时面临淘汰key致使延迟增大的的状况,要想缓解这种状况,除了上面说的避免存储大key、使用随机淘汰策略以外,也能够考虑拆分实例的方法来缓解,拆分实例能够把一个实例淘汰key的压力分摊到多个实例上,能够在必定程度下降延迟。

fork耗时严重

若是你的Redis开启了自动生成RDB和AOF重写功能,那么有可能在后台生成RDB和AOF重写时致使Redis的访问延迟增大,而等这些任务执行完毕后,延迟状况消失。

遇到这种状况,通常就是执行生成RDB和AOF重写任务致使的。

生成RDB和AOF都须要父进程fork出一个子进程进行数据的持久化,在fork执行过程当中,父进程须要拷贝**内存页表**给子进程,若是整个实例内存占用很大,那么须要拷贝的内存页表会比较耗时,此过程会消耗大量的CPU资源,在完成fork以前,整个实例会被阻塞住,没法处理任何请求,若是此时CPU资源紧张,那么fork的时间会更长,甚至达到秒级。这会严重影响Redis的性能

咱们能够执行info命令,查看最后一次fork执行的耗时latest_fork_usec,单位微秒。这个时间就是整个实例阻塞没法处理请求的时间。

除了由于备份的缘由生成RDB以外,在主从节点第一次创建数据同步时,主节点也会生成RDB文件给从节点进行一次全量同步,这时也会对Redis产生性能影响。

要想避免这种状况,咱们须要规划好数据备份的周期,建议在从节点上执行备份,并且最好放在**低峰期**执行若是对于丢失数据不敏感的业务,那么不建议开启AOF和AOF重写功能。

另外,fork的耗时也与系统有关,若是把Redis部署在虚拟机上,那么这个时间也会增大。因此使用Redis时建议部署在物理机上,下降fork的影响。

绑定CPU

不少时候,咱们在部署服务时,为了提升性能,下降程序在使用多个CPU时上下文切换的性能损耗,通常会采用进程绑定CPU的操做。

但在使用Redis时,咱们不建议这么干,缘由以下:

绑定CPU的Redis,在进行数据持久化时,fork出的子进程,子进程会继承父进程的CPU使用偏好,而此时子进程会消耗大量的CPU资源进行数据持久化, 子进程会与主进程发生CPU争抢,这也会致使主进程的CPU资源不足访问延迟增大。

因此在部署Redis进程时,若是须要开启RDB和AOF重写机制,必定不能进行CPU绑定操做!

开启AOF

上面提到了,当执行AOF文件重写时会由于fork执行耗时致使Redis延迟增大,除了这个以外,若是开启AOF机制,设置的策略不合理,也会致使性能问题。

开启AOF后,Redis会把写入的命令实时写入到文件中,但写入文件的过程是先写入内存,等内存中的数据超过必定阈值或达到必定时间后,内存中的内容才会被真正写入到磁盘中。

AOF为了保证文件写入磁盘的安全性,提供了3种刷盘机制:

•appendfsync always:每次写入都刷盘,对性能影响最大,占用磁盘IO比较高,数据安全性最高

•appendfsync everysec:1秒刷一次盘,对性能影响相对较小,节点宕机时最多丢失1秒的数据

•appendfsync no:按照操做系统的机制刷盘,对性能影响最小,数据安全性低,节点宕机丢失数据取决于操做系统刷盘机制

当使用第一种机制appendfsync always时,Redis每处理一次写命令,都会把这个命令写入磁盘,并且这个操做是在主线程中执行的

内存中的的数据写入磁盘,这个会加剧磁盘的IO负担,操做磁盘成本要比操做内存的代价大得多。若是写入量很大,那么每次更新都会写入磁盘,此时机器的磁盘IO就会很是高,拖慢Redis的性能,所以咱们不建议使用这种机制。

与第一种机制对比,appendfsync everysec会每隔1秒刷盘,而appendfsync no取决于操做系统的刷盘时间,安全性不高。所以咱们推荐使用appendfsync everysec这种方式,在最坏的状况下,只会丢失1秒的数据,但它能保持较好的访问性能。

固然,对于有些业务场景,对丢失数据并不敏感,也能够不开启AOF。

使用Swap

若是你发现Redis忽然变得很是慢,每次访问的耗时都达到了几百毫秒甚至秒级,那此时就检查Redis是否使用到了Swap,这种状况下Redis基本上已经没法提供高性能的服务。

咱们知道,操做系统提供了Swap机制,目的是为了当内存不足时,能够把一部份内存中的数据换到磁盘上,以达到对内存使用的缓冲。

但当内存中的数据被换到磁盘上后,访问这些数据就须要从磁盘中读取,这个速度要比内存慢太多!

尤为是针对Redis这种高性能的内存数据库来讲,若是Redis中的内存被换到磁盘上,对于Redis这种性能极其敏感的数据库,这个操做时间是没法接受的

咱们须要检查机器的内存使用状况,确认是否确实是由于内存不足致使使用到了Swap。

若是确实使用到了Swap,要及时整理内存空间,释放出足够的内存供Redis使用,而后释放Redis的Swap,让Redis从新使用内存。

释放Redis的Swap过程一般要重启实例,为了不重启实例对业务的影响,通常先进行主从切换,而后释放旧主节点的Swap,从新启动服务,待数据同步完成后,再切换回主节点便可。

可见,当Redis使用到Swap后,此时的Redis的高性能基本被废掉,因此咱们须要提早预防这种状况。

咱们须要对Redis机器的内存和Swap使用状况进行监控,在内存不足和使用到Swap时及时报警出来,及时进行相应的处理

网卡负载太高

若是以上产生性能问题的场景,你都规避掉了,并且Redis也稳定运行了很长时间,但在某个时间点以后开始,访问Redis开始变慢了,并且一直持续到如今,这种状况是什么缘由致使的?

以前咱们就遇到这种问题,特色就是从某个时间点以后就开始变慢,而且一直持续。这时你须要检查一下机器的网卡流量,是否存在网卡流量被跑满的状况。

网卡负载太高,在网络层和TCP层就会出现数据发送延迟、数据丢包等状况。Redis的高性能除了内存以外,就在于网络IO,请求量突增会致使网卡负载变高。

若是出现这种状况,你须要排查这个机器上的哪一个Redis实例的流量过大占满了网络带宽,而后确认流量突增是否属于业务正常状况,若是属于那就须要及时扩容或迁移实例,避免这个机器的其余实例受到影响。

运维层面,咱们须要对机器的各项指标增长监控,包括网络流量,在达到阈值时提早报警,及时与业务确认并扩容。

总结

以上咱们总结了Redis中常见的可能致使延迟增大甚至阻塞的场景,这其中既涉及到了业务的使用问题,也涉及到Redis的运维问题。

可见,要想保证Redis高性能的运行,其中涉及到CPU、内存、网络,甚至磁盘的方方面面,其中还包括操做系统的相关特性的使用。

做为开发人员,咱们须要了解Redis的运行机制,例如各个命令的执行时间复杂度、数据过时策略、数据淘汰策略等,使用合理的命令,并结合业务场景进行优化。

做为DBA运维人员,须要了解数据持久化、操做系统fork原理、Swap机制等,并对Redis的容量进行合理规划,预留足够的机器资源,对机器作好完善的监控,才能保证Redis的稳定运行。

image

相关文章
相关标签/搜索