• redis变慢原因记录


    有时候会遇到这种场景:

    当调用程序的某个接口时,发现这个接口响应速度很慢,首先要排查慢在哪个环节?如下图:

    排查环节大致有这么几个:

    1. 后端业务代码,是不是在某个地方出现了长时间的循环、等待临界资源等等。这需要审查代码了。
    2. 网络原因,部署业务服务的服务器与redis、mysql等等服务之间的网络出现问题,网络数据包传输存在高延迟、丢包等等情况。并且出现这种情况,不应该是某一个接口这样,应该是所有需要用到redis的接口都会出现缓慢的情况,这种情况就联系网络运维的同事协助解决网络问题了。
    3. 问题出在redis上,这里重点关注的是问题出在redis的情况。下面详细说明。

    Redis真的变慢了吗?

    如何知道redis真的慢了?

    基准性能测试,基准性能测试可以让我们了解我们使用的redis在redis所部署的服务器上的基准性能,也就是他的最大响应延迟和平均延迟大概是个什么情况,这里要在自己使用的服务器上进行测试,因为不同的配置,redis的性能是不一样的。

    例如:在A机器上,1ms可能就可以认为redis变慢了,但是在B机器上,2ms可能就是正常速度。

    基准性能测试

    为了避免网络带来的影响,可以直接在redis服务器上测试响应延迟,使用: redis-cli --latency
    在这里插入图片描述

    min最小延时

    max最大延时

    avg平均延时

    单位都是ms

    –latency统计的是ping的耗时,也就是一直ping,统计出来的耗时。因为我这里是在redis本地连接的,所以没有网络传输时间,并且这个统计也是不包括建立时间的。

    如果观察到,这个实例运行延迟是正常情况下的2倍以上,那么可以认为这个redis的确是慢了。

    redis变慢的原因分析

    redis提供了慢日志记录的,可以使用:slowlog get 10来获取最近的10条慢记录。

    redis是如何定义慢操作的?

    127.0.0.1:6379> config get slowlog-log-slower-than
    1) "slowlog-log-slower-than"
    2) "10000"
    

    默认是10000微秒。

    可以通过命令:config set slowlog-log-slower-than 0来设置这个时间。

    设置为0表示所有命令都记录到慢日志。

    设置为负数,表示禁用慢日志。

    使用了复杂度过高的命令

    经常使用时间复杂度大于O(n)的命令,例如sort、sunion、zunionstore

    这种情况变慢的原因就是,redis在操作内存数据的时候,时间复杂度太高,长时间占用了CPU资源导致。

    出现这种情况的现象就是redis的ops并不高,但是redis的CPU使用率却很高。

    针对这种情况,可以选择将对数据费时间的操作在客户端做。

    还有就是使用了时间复杂度为O(n)的命令,但是这个n非常大。并且返回的数据量还很大。

    这种情况变慢的原因就是,n很大,导致CPU资源被占用,并且返回的数据量很大,那么也就需要花费更多的时间在数据协议的组装和网络传输过程中。

    针对这种情况,可以选择将这个数据进行拆分成多个小的数据,尽量保证n不要很大,并且每次获取的数据量尽量少。

    操作BigKey

    如果在慢日志查询发现,并不是上面的情况,而是set/del这种简单的命令出现在慢日志中,那么就要怀疑是否存在bigkey。

    redis 也提供了查询实例中bigkey的命令redis-cli --bigkeys,这个会统计string,list,set,zset,hash 这几个常见数据类型中每种类型里的最大的 key。 这里的最大不一定是内存最大,也可能是拥有元素最多的key。

    但是这个查出来的只是最大的key,不一定是bigkey,至于多大算bigkey,推荐10kB。可以根据自己的经验进行定义bigkey。

    这个命令的原理就是在内部scan命令,遍历整个实例中的所有key,然后针对key的类属性,分别执行strlen、llen、hlen、scard、zcard命令,这个命令会导致redis的ops突增。

    redis在写出数据时,需要为新的数据分配内存。那么在删除数据时,它会释放相应的内存空间。

    如果一个key的val非常大,那么redis在分配内存时,就会比较耗时。

    同样的,当删除这个key,释放的内存也就是很大,也会比较耗时。

    如果是bigkey导致的redis变慢,那么可以选择使用unlink命令替代del,但是需要是redis4.0以上的版本。这个命令就是把释放key内存的操作交给后台线程去处理。

    如果使用的是redis6.0以上的版本,可以开启laze-free机制,执行del,也是一样的道理,交给后台线程去处理。

    在这里插入图片描述

    默认是关闭的(lazyfree-lazy-user-del no),有关laze-free选项可以网上查询资料,或者直接看注释也行。

    但是还是不建议在实例中存入bigkey,因为在很多场景下还会有其它的性能问题。

    集中过期

    如果在慢日志中查询也没有发现明显的很慢的操作,但是在使用redis时,会在某个时间点突然出现延时,这可能就是集中过期导致的。

    集中过期指的是大量的Key同时在某个时间过期了,这个时候,redis可能会出现延时变大的效果。

    要了解这个原因,首先要知道redis的过期删除策略。

    redis有三种常见的过期删除策略:

    • 定时删除
    • 惰性删除
    • 定期删除
    定时删除:在设置key的过期时间时,同时创建一个定时事件,等到达时间,由事件处理器执行删除key的操作。
    

    优点:对于内存很友好,过期很快就会被删除。

    缺点:如果在某个时间点,大量的key同时过期,那么就会占用一部分CPU资源,如果CPU资源紧张,那么会对服务器的响应时间和吞吐量造成影响,也就是会造成redis的延迟变大

    惰性删除:不主动去删除过期key,每次查询的key的时候,都检测一下这个key是否过期,如果过期,那么则删除key。
    

    优点:对于CPU很友好,每次只有访问key的时候才回去检测是否过期,执行删除操作。

    缺点:对于内存不友好,如果有大量的key过期,而没来得及的删除,就会占用大量的内存,造成了内存的浪费。在内存严重不足的情况下,redis又会触发内存淘汰策略,在特定的策略下,也是有可能造成redis的延迟变大的。下面会讲内存不足有关得内存淘汰策略。

    定期删除:每隔一段时间,【随机】从数据库中取出一定数量的key检查,并删除其中过期的key。
    

    优点:通过限制删除操作执行的时长和频率,来减少操作对CPU的影响,同时也可以删除一部分过期的key。

    缺点:执行的太频繁,就会和定时删除一样,对CPU不友好,执行的太不频繁,又会和惰性删除一样,对过期的key删除不及时。

    redis中定期删除是这样实现的:

    默认每秒10次检查一次数据库,随机选择20个key判断是否过期,删除其中过期的kye,如果这20个key中,过期的超过了5个,就继续随机选20个key,一直到20个key中不超过5个过期,或者时间超过了25ms,就退出定期删除。这个操作是在主线程中执行的。如果大量的key都过期了,那么这个删除操作就会使得redis延迟变大。

    1s执行10次,也就是每100ms执行1次,如果25ms都在执行删除操作,那么cpu对于用户的响应也就是75ms,也就是1/4的时间都在处理和用户无关的事情。

    并且删除过期key的操作是不会出现在慢日志中的。慢日志记录的是一个命令真正操作内存数据耗费的时间。所以排查得从代码里面排查,看是否使用了expireat等关键字。

    如果是此类情况,那么可以通过以下两种方法规避:

    1.给集中过期得key增加一个随机过期时间,例如设置4s+random().nextInt(10)。

    2.开启lazy-free,这样删除过期得key就会放到后台线程做了。

    实例内存达到上限

    当redis内存达到了上限,也会导致redis延迟变大。因为redis达到内存上限以后,redis必须先要淘汰一部分数据,腾出空间,再讲新数据写入。

    淘汰数据也是需要时间的,具体淘汰哪些数据,就取决于配置的淘汰策略了。

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

    如果淘汰的是bigkey,删除bigkey释放内存时,耗费时间也是久一些的。

    如果是这种情况,有4种建议方案:

    1. 避免存储bigkey,就可以减少因释放内存而占用的时间。
    2. 淘汰策略改为随机,随机淘汰比LRU要快,但是也要视具体业务而定。
    3. 分片集群,将淘汰key的压力分摊到多个实例上。
    4. redis4.0以上版本,开启lazy-free机制,把淘汰释放内存的操作放到后台线程种执行。(lazyfree-lazy-eviction=yes)

    fork耗时严重

    为了保证Redis数据的持久化,我们可能会开启后台RDB和AOF rewrite功能。但是,redis操作延迟变大有可能会发生在RDB和AOF Rewrite期间,如果真的是这样,那么要了解一下什么情况下RDB和AOF Rewrite会导致redis操作延迟变大。

    后台RDB和AOF Rewrite期间,主进程会创建子进程,期间是调用了系统提供的fork函数。fork函数在执行过程中,会将主进程的页表拷贝一份给子进程。
    在这里插入图片描述

    如果这个redis实例很大,那么页表就会很大,这个时候fork就会导致消耗大量CPU资源,并且在fork期间,整个redis都会被阻塞。

    如果此时CPU资源本来就很紧张,此时如果fork很耗时,那么就会导致redis响应延迟变大。

    如果在fork完成后,客户端又执行了set/del命令并且操作的是bigkey,此时又会进一步导致redis延迟增加,因为在后台进程对redis进行rdb持久化/AOF Rewrite时,如果修改了实例中的数据,那么会发生写时复制,原则是谁写谁复制,也就是说,主进程会复制一份需要修改的数据,然后在副本上进行修改。

    如果此时需要复制的数据比较大,是一个bigkey,那么就到导致复制延迟增加,进而导致redis延时增加。

    这种情况如何排查?

    在redis执行info命令,查看latest_fork_usec项,单位是微秒。

    redis在数据持久化生成RDB过程、AOFrewrite、主从节点第一次同步数据、从节点断开后恢复连接,都有可能会fork子进程。

    优化方案:

    1. 控制实例大小,尽量在10G以上。
    2. RDB备份尽量控制在ops低的期间执行。如果对数据丢失不敏感的话,可以关闭aof和aofwrite。
    3. 调大repl-backlog-size参数,这个参数就是为了尽量减少主从全量复制的概率。
    4. redis尽量不要部署在虚拟机上,因为虚拟机上的fork操作会比在物理机上更加耗时。

    开启了内存大页

    程序向操作系统申请内存是按照内存页进行申请的,linux中,常规的内存页大小是4KB。

    linux内核从2.6.38开始,支持了内存大页机制,这个机制允许应用程序以2MB大小为单位,向系统申请内存。

    申请的内存单位变大了,那么耗时也就会变得更长。

    那么在redis中,在fork子进程后,如果主进程接到了写请求,那么就会采用写时复制的方式来操作内存数据。

    在这里插入图片描述

    主进程一旦有数据需要修改,那么就会先把这块内存数据拷贝出来,再去修改数据,并且拷贝的不是单纯的具体某一条数据,而是以页为单位进行拷贝的,也就是说,如果开启了内存大页,那么拷贝的至少是一页,也就是2MB。

    这样带来的好处就是,父进程的任何写操作,都不会影响到子进程持久化操作。

    极端情况写,如果在子进程持久化的时候,多个写操作都发生了修改,并且都不在一个页里面,那么每次都要向操作系统申请内存,并且开了大页,申请的时间也会变长,进而导致写请求延迟增加,影响redis性能。

    解决这个问题:

    只需要关闭机器的内存大页即可。

    查看:

    [root@192 ~]# cat /sys/kernel/mm/transparent_hugepage/enabled 
    [always] madvise never
    

    关闭:

    [root@192 ~]# echo never /sys/kernel/mm/transparent_hugepage/enabled
    

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

    开启AOF

    上面分析AOF Rewrite对redis性能的影响,关注点都是在fork上。但是其实数据持久化方面,还有影响redis性能的因素,这次来看一下。

    当redis开启aof后,工作原理如下:

    redis每次收到写命令,先执行这个命令,然后再把这个命令,写到aof中。

    //redis.conf
    appendonly     yes					//表示开启AOF持久化,默认是关闭的
    appendfilename "appendonly.aof"		//AOF持久化文件的名称
    

    再具体一点就是:

    1. Redis执行完写操作命令,将命令追加到server.aof_buf缓冲区。
    2. 再通过write()系统调用,将aof_buf缓冲的写入到aof文件,此时只是写到了内核缓冲page cache,等待内核将数据写入磁盘。
    3. 内核决定何时将page cache的数据写到磁盘。

    在这里插入图片描述

    redis提供了三种刷盘策略:

    always:每次执行完写操作后立马刷盘。

    no:每次执行完写操作,只是将数据写到page cache。具体什么时候刷盘,由内核决定。

    everysec:每次执行完写操作,也是将数据写到page cache,由后台线程每隔1s执行一次刷盘操作。

    首先第一种策略,always,每次执行一次写操作,都会把这个命令写到磁盘中,必定会加重redis写负担,会导致redis延迟增加。

    第二种策略,no,这种redis每次写操作,只会写到page cache中,什么时候刷盘,由操作系统决定,这种方案对redis的性能影响最小,但是当redis宕机时候,会丢失一部分数据,并且数据量的大小也是不确定的,出于安全性考虑,一般不适用这种配置。如果对数据丢失无所谓,那么可以采用这种配置。

    第三种策略,everysec,这种是个折中的方案,每次存到page cache,然后每秒刷盘一次,这样即使丢失数据也就是丢失一秒的数据。并且也不会影响每次写操作的效率。但是这个方案并不像看上去那么好,因为这种方案还是会存在redis延迟变大的情况发生,甚至阻塞整个redis。

    考虑一下,如果在某个时候,redis后台线程在执行AOF Rewrite,并且此时磁盘的IO负载已经很高了,此时如果触发了redis刷盘策略,这个时候会不会阻塞主线程的概率呢?如下图:

    在这里插入图片描述

    当磁盘IO负载很大的时候,主线程依旧接收写请求,主线程还需要通过系统调用write将数据写到文件中,但是此时,后台子线程由于磁盘IO负载过高,导致fsync阻塞,响应延迟变大,此时将会导致主线程在执行write系统调用时也被阻塞住,知道后台线程执行完fsync后,主线程执行write才能成功返回。

    所以,即使AOF配置了everysec,在磁盘压力过大的时候,也会出现性能问题。

    解决方法:redis提供了一个配置项,当子进程AOF期间,可以让后台子线程不执行刷盘操作。如下图,改为yes即可,上面的注释也有详细说明。

    在这里插入图片描述

    缺点就是,如果在AOFrewrite期间,实例发生了宕机,那么此时会丢失更多的数据。

    当然,如果占用磁盘IO的是其它应用程序,那就比较简单了,将这个程序迁移到其它机器上,或者提供磁盘的IO能力,例如将磁盘缓存SSD磁盘。

    绑定CPU

    绑定CPU是可以提供性能的,因为降低了redis在多个CPU之间的上下文切换带来的性能影响。

    但是,redis在绑定CPU是有很多讲究的,如果不了解redis的运行原理,随意绑定CPU,可能会导致相反的效果。

    现代服务器一般都有多个CPU,每个CPU又包含多个物理核心,每个物理核心又分为多个逻辑核心。每个物理核心下的逻辑核心又是共用L1/L2 Cache的。

    在这里插入图片描述

    而redis Server除了主线程服务客户端的请求之外,还会创建子进程、子线程。

    子进程用于数据持久化。

    子线程用于执行一些耗时的操作,例如异步释放fd、异步AOF刷盘、异步lazy-free等等。

    假设把redis进程绑定到一个CPU的逻辑核心上,那么当redis持久化的时候,fork出来的子进程会继承父进程的 CPU 使用偏好。

    而此时的子进程会消耗大量的 CPU 资源进行数据持久化(把实例数据全部扫描出来需要耗费CPU),这就会导致子进程会与主进程发生 CPU 争抢,进而影响到主进程服务客户端请求,访问延迟变大。

    优化方案:如果要绑定CPU,那么应该让redis进程绑定在同一个物理核心的多个逻辑核心上。

    但是这只是在一定程度上解决主线程、子进程、子线程对于CPU资源的竞争。子进程、子线程还是会竞争这些CPU。

    官方redis6.0版本提供了一个方案,就是给主线程、后台线程、后台RDB进程、AOR Rewrite进程都绑定固定的CPU逻辑核心。

    使用了Swap

    如果redis突然变得非常慢,每次操作耗时可能都几百毫秒甚至秒级,那可能就要检查一下redis是否使用了swap了,如果真的是这种情况,redis基本上是没有办法再提供高性能服务了。

    什么是swap?

    如果对操作系统有了解的话,应该知道,操作系统为了缓解内存不足对应用的影响,允许把一部分内存中的数据换出到磁盘上,等要使用的时候再换进来。从内存中换出到磁盘上的区域就是Swap。

    那么问题就在于,频繁的换入换出,也就是要频繁读写磁盘,这个速度相比于访问内存,是很慢的。尤其是对redis这种对性能要求极高、性能及其敏感的数据库来说。

    如何排查?

    首先查看redis进程id,下面的1556就是进程id

    [root@192 bin]# ps -ef|grep redis
    root       1556      1  0 21:52 ?        00:00:00 /usr/local/bin/redis-server 127.0.0.1:6379
    root       1564   1478  0 21:52 pts/0    00:00:00 grep --color=auto redis
    

    然后进入机器的/proc目录

    [root@192 bin]# cd /proc/1556
    

    查看redis进程的使用情况

    [root@192 1556]# cat smaps | egrep '^(Swap|Size)'
    Size:               1840 kB
    Swap:                  0 kB
    Size:                  8 kB
    Swap:                  0 kB
    Size:                 48 kB
    Swap:                  0 kB
    Size:               2224 kB
    Swap:                  0 kB
    

    每一行 Size 表示的是 Redis 实例所用的一块内存大小,而 Size 下方的 Swap和它相对应,表示这块 Size 大小的内存区域有多少已经被换出到磁盘上了。如果这两个值相等,就表示这块内存区域已经完全被换出到磁盘了。

    如果有几百MB,甚至GB的数据从内存换到了磁盘,这时候redis的性能就会急剧下降了。

    这种情况下的解决方案就比较有限了。

    只能想办法增加机器的内存。

    碎片整理

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

    内存碎片会降低 Redis 的内存使用率,我们可以通过执行 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 版本,它正好提供了自动碎片整理的功能,可以通过配置开启碎片自动整理。

    但是开启自动碎片整理,也可能导致redis性能下降。

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

    网络带宽过载

    Redis 稳定运行了很长时间,但在某个时间点之后开始,操作 Redis 突然开始变慢了,而且一直持续下去,这种情况又是什么原因导致?

    此时你需要排查一下 Redis 机器的网络带宽是否过载,是否存在某个实例把整个机器的网路带宽占满的情况。

    网络带宽过载的情况下,服务器在 TCP 层和网络层就会出现数据包发送延迟、丢包等情况。

    Redis 的高性能,除了操作内存之外,就在于网络 IO 了,如果网络 IO 存在瓶颈,那么也会严重影响 Redis 的性能。

    如果确实出现这种情况,你需要及时确认占满网络带宽 Redis 实例,如果属于正常的业务访问,那就需要及时扩容或迁移实例了,避免因为这个实例流量过大,影响这个机器的其他实例。

    运维层面,你需要对 Redis 机器的各项指标增加监控,包括网络流量,在网络流量达到一定阈值时提前报警,及时确认和扩容

    源,产生更多的耗时,从而影响到客户端的请求。

    网络带宽过载

    Redis 稳定运行了很长时间,但在某个时间点之后开始,操作 Redis 突然开始变慢了,而且一直持续下去,这种情况又是什么原因导致?

    此时你需要排查一下 Redis 机器的网络带宽是否过载,是否存在某个实例把整个机器的网路带宽占满的情况。

    网络带宽过载的情况下,服务器在 TCP 层和网络层就会出现数据包发送延迟、丢包等情况。

    Redis 的高性能,除了操作内存之外,就在于网络 IO 了,如果网络 IO 存在瓶颈,那么也会严重影响 Redis 的性能。

    如果确实出现这种情况,你需要及时确认占满网络带宽 Redis 实例,如果属于正常的业务访问,那就需要及时扩容或迁移实例了,避免因为这个实例流量过大,影响这个机器的其他实例。

    运维层面,你需要对 Redis 机器的各项指标增加监控,包括网络流量,在网络流量达到一定阈值时提前报警,及时确认和扩容。

  • 相关阅读:
    Spring 框架中都用到了哪些设计模式:单例模式、策略模式、代理模式
    树查找(暑假每日一题 18)
    redis源码解析
    【C++多线程那些事儿】多线程的执行顺序如你预期吗?
    MySQL面试知识点总结(持续更新)
    小程序如何反编译
    《LeetCode力扣练习》代码随想录——链表(两两交换链表中的节点---Java)
    Vue+Electron打包桌面应用(从零到一完整教程)
    一文带你快速搭建框架(最全MyBatis笔记)
    树的存储结构
  • 原文地址:https://blog.csdn.net/weixin_42496727/article/details/126963244