Bootstrap

Redis变慢原因

目录

 

转载

第一步:确定Redis是否真的变慢了

第二步:查看slowlog慢日志

原因1:使用复杂度过高的命令

原因2:操作bigkey

原因3:集中过期

原因4:实例内存达到上限

原因5:fork耗时严重

原因6:开启内存大页

原因7:开启AOF

原因8:绑定CPU

原因9:使用Swap

原因10:碎片整理

原因11:网络带宽过载


转载

Redis为什么变慢了?一文讲透如何排查Redis性能问题 | 万字长文

第一步:确定Redis是否真的变慢了

响应变慢,需要思考下面几个问题:

①应用服务器与Redis服务器之间的网络存在问题,存在网络线路质量不佳,网络数据包在传输时存在延迟、丢包等情况;

②假如确实是Redis响应慢了,衡量标准是什么?

这里需要用到【基准性能】,即Redis在一台负载正常的机器上,其最大的响应延迟和平均响应延迟分别是怎样的?

只有了解了Redis在生产环境服务器上的基准性能,才能评价当其延迟达到什么程度时,才认为Redis确实变慢了。

因此,我们直接在生产环境下的Redis服务器测试实例的响应延迟情况,执行以下命令:

redis-cli -h IP地址 -p 端口 --intrinsic-latency 60

拿到生产环境的【基准性能】后,再去相同配置的服务器上,测试一个正常Redis实例的【基准性能】,如果生产环境的【基准性能】运行延迟是正常Redis基准性能的 2倍以上,即可认为这个Redis实例确实变慢了;

第二步:查看slowlog慢日志

根据自身情况,配置慢日志参数,例如:

# 命令执行耗时超过 5 毫秒,记录慢日志

CONFIG SET slowlog-log-slower-than 5000

# 只保留最近 500 条慢日志

CONFIG SET slowlog-max-len 500

然后通过SLOWLOG get X来查看最近记录的慢日志;

原因1:使用复杂度过高的命令

该原因具体又分成2种场景:

①经常使用O(N)以上复杂度的命令,如SORT、SUNION、ZUNIONSTORE聚合类命令,此类命令在操作内存数据时,时间复杂度过高,要花费更多的 CPU 资源;

②使用O(N)复杂度的命令,其N值非常大,由于N值比较大,Redis一次性需要返回给客户端的数据过多,更多时间花费在数据协议的组装和网络传输过程中;

对应的解决方法:

①尽量不使用O(N)以上复杂度过高的命令,对于数据的聚合操作,放在客户端做;

②执行O(N)命令保证N尽量的小(推荐 N <= 300),每次获取尽量少的数据,让Redis可以及时处理返回;

原因2:操作bigkey

如果在slowlog中显示SET/DEL占据大量时间,那么Redis实例可能写入bigkey了;

原因在于:Redis在写入数据时需要为新的数据分配内存,Redis在删除数据时会释放对应的内存空间;如果一个key写入/删除的value非常大,那Redis在分配内存/释放内存时就会比较耗时,这种类型的key就是bigkey;

这里借助如下指令来查看bigkey的情况:

redis-cli -h IP地址 -p 端口号 --bigkeys -i 0.01

在查看期间需要注意2个情况:

①对线上实例进行bigkey扫描时,Redis的OPS会突增,因此为了降低扫描过程中对Redis影响,需控制一下扫描的频率,指定-i 参数,该参数表示扫描过程中每次扫描后休息的时间间隔,单位是秒;

bigkeys内部执行的是SCAN命令,遍历整个实例中所有的key,然后针对key的类型,分别执行STRLEN、LLEN、HLEN、SCARD、ZCARD 命令,来获取String类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数;

②扫描结果中,对于容器类型(List、Hash、Set、ZSet)的key,只能扫描出元素最多的key。但一个key的元素多,不一定表示占用内存也多,你还需要根据业务情况,进一步评估内存占用情况;

针对bigkeys的解决方案有如下几个:

①业务应用尽量避免写入bigkey;

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

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

总而言之,还是尽量避免写入bigkey,除了上述原因外,当你在集群部署时,槽位迁移也会因为bigkeys导致移动时间变成;

原因3:集中过期

如果规律性的发生延迟,则需要考虑大量key集中过期的情况;这里先解释下Redis处理过期数据的策略:被动过期、主动过期;

被动过期:只有当访问某个key时,才判断这个key是否已过期,如果已过期,则从实例中删除;

主动过期:Redis 内部维护了一个定时任务,默认每隔100毫秒(1秒10次)就会从全局的过期哈希表中随机取出20个key,然后删除其中过期的key,如果过期key的比例超过了25%,则继续重复此过程,直到过期key的比例下降到25%以下,或者这次任务的执行耗时超过了25毫秒,才会退出循环;

这个主动过期key的定时任务,是在Redis主线程中执行的,因此,如果在执行主动过期的过程中,出现了需要大量删除过期 key的情况,那么此时应用程序在访问Redis时,必须要等待这个过期任务执行结束,Redis才可以服务这个客户端请求,此时就会出现,应用访问Redis延时变大,如果此时需要过期删除的是一个bigkey,那么这个耗时会更久,且这个操作延迟的命令并不会记录在慢日志中;

慢日志只记录一个命令真正操作内存数据的耗时,而Redis主动删除过期key的逻辑,是Redis主动进行,在用户发送del命令之前执行的,所以,慢日志中没有操作耗时的命令,但应用程序却感知到了延迟变大,时间都花费在了删除过期key上;

可以在开发的工程中搜索expireat/pexpireat关键字,如果确实存在集中过期key的逻辑存在,但这种逻辑又是业务所必须的,那该如何处理呢?

一般有3种方案来规避这个问题:

①集中过期key增加一个随机过期时间,把集中过期的时间打散,降低Redis清理过期key的压力;

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

③运维层面,把Redis各项运行状态数据监控起来,在Redis上执行INFO命令就可以拿到这个实例所有的运行状态数据,其中expired_keys代表整个实例到目前为止,累计删除过期 key的数量,当这个指标在很短时间内出现了突增,需要与业务应用报慢的时间点进行对比分析,确认时间是否一致,如果一致则可以确认确实是因为集中过期key导致的延迟变大;

原因4:实例内存达到上限

如果Redis实例设置了内存上限maxmemory,那么也有可能导致 Redis变慢,这是因为我们在设置maxmemory同时,必须设置一个数据淘汰策略,这是因为:当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);

原因5:fork耗时严重

如果操作Redis延迟变大都发生在Redis后台RDB和AOF的 rewrite期间,那我们有理由怀疑是fork导致Redis延时增大,为了增强我们怀疑,需要在Redis上执行INFO命令,查看latest_fork_usec配置,该配置单位是微秒,表示主进程在fork子进程期间,整个实例阻塞无法处理客户端请求的时间,如果耗时很久,则说明fork导致整个Redis实例都处于不可用的状态;

那为什么fork会导致Redis变慢?

当Redis开启了后台RDB和AOF rewrite后,在执行时它们都需要主进程调用操作系统提供的fork函数来创建出一个子进程进行数据的持久化,而fork在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个Redis实例很大,那么这个拷贝的过程也会比较耗时。而且fork过程还会消耗大量的CPU资源,在完成fork之前,整个Redis实例会被阻塞住,无法处理任何客户端请求;

此时如果CPU资源本来就很紧张,那fork的耗时会更长,甚至达到秒级,严重影响Redis性能;

此外,除了数据持久化会生成RDB之外,当主从节点第一次建立数据同步时,主节点也创建子进程生成RDB,然后发给从节点进行一次全量同步,所以,这个过程也会对Redis产生性能影响。

要想避免这种情况,可以采取以下方案进行优化:

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

②合理配置数据持久化策略:在slave节点执行RDB备份,推荐在低峰期执行,而对于丢失数据不敏感的业务,可以关闭AOF和AOF rewrite;

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

④降低主从库全量同步的概率:适当调大repl-backlog-size参数,避免主从全量同步;

原因6:开启内存大页

除了fork耗时导致Redis响应延时变大之外,这里还有一个方面也会导致性能问题,这就是操作系统是否开启了内存大页机制;

这里有个问题,为什么会从fork引申到操作系统的内存大页机制?

应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的内存页大小是4KB,到了Linux 2.6.38开始支持内存大页机制,该机制允许应用程序以2MB大小为单位,向操作系统申请内存,虽然应用程序每次向操作系统申请的内存单位变大了,但申请内存的耗时也相应的变长了

当Redis在执行后台RDB和AOF rewrite时,采用fork子进程的方式来处理。主进程fork子进程后,此时的主进程依旧是可以接收写请求的,而进来的写请求,会采用Copy On Write方式操作内存数据,即主进程一旦有数据需要修改,Redis并不会直接修改现有内存中的数据,而是先将这块内存数据拷贝出来,再修改这块新内存的数据,这就是所谓的「写时复制」。

这样做的好处是,父进程有任何写操作,并不会影响子进程的数据持久化(子进程只持久化fork这一瞬间整个实例中的所有数据即可,不关心新的数据变更,因为子进程只需要一份内存快照,然后持久化到磁盘上)。

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

那如何解决这个问题?

只需要关闭内存大页机制就可以了。

你需要查看 Redis 机器是否开启了内存大页:

cat /sys/kernel/mm/transparent_hugepage/enabled

如果输出选项是 always,就表示目前开启了内存大页机制,我们需要关掉它:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

原因7:开启AOF

如果AOF配置不合理,也有可能导致性能问题,下面先说一下AOF工作流程:

①Redis执行写命令后,把这个命令写入到AOF文件内存中(write系统调用);

②Redis然后根据配置的AOF刷盘策略,把AOF内存数据刷到磁盘上(fsync系统调用);

下面继续说一下AOF刷盘策略:

①appendfsync always

主线程每次执行写操作后立即刷盘,写入到磁盘中才返回,此方案会占用比较大的磁盘 IO资源,但数据安全性最高;

但是,整个过程都是在主线程执行的,必然造成Redis写操作的代价,严重拖慢Redis性能;

②appendfsync no

主线程每次写操作只写内存就返回,内存数据什么时候刷到磁盘,交由操作系统决定,此方案对性能影响最小,但数据安全性也最低,Redis 宕机时丢失的数据取决于操作系统刷盘时机;

③appendfsync everysec

主线程每次写操作只写内存就返回,然后由后台线程每隔1秒执行一次刷盘操作(触发fsync系统调用),此方案对性能影响相对较小,但当Redis宕机时会丢失1秒的数据;

那么,目前看来,appendfsync everysec方案最完美,但该方案理论上有个重点风险

当Redis后台线程在执行AOF文件刷盘时,此时磁盘的IO负载很高,后台线程在执行刷盘操作(fsync系统调用)时就会被阻塞住;

此时主线程依旧会接收写请求,紧接着,主线程又需要把数据写到文件内存中(write 系统调用),但此时的后台子线程由于磁盘负载过高,导致fsync发生阻塞,迟迟不能返回,那主线程在执行write 系统调用时也会被阻塞住,直到后台线程fsync执行完成后,主线程执行write才能成功返回,在这个过程中,主线程依旧有阻塞的风险。

那什么情况下会导致磁盘IO负载过大?以及如何解决这个问题呢?

①子进程正在执行AOF rewrite,这个过程会占用大量的磁盘IO 资源;

解决方案:当子进程在AOF rewrite期间,可以让后台子线程不执行刷盘(不触发fsync系统调用)操作。这相当于在AOF rewrite期间,临时把 appendfsync设置为了none,配置如下:

# AOF rewrite 期间,AOF 后台子线程不进行刷盘操作

# 相当于在这期间,临时把 appendfsync 设置为了 none

no-appendfsync-on-rewrite yes

当然,开启这个配置项,在 AOF rewrite 期间,如果实例发生宕机,那么此时会丢失更多的数据;

②有其他应用程序在执行大量的写文件操作,也会占用磁盘 IO 资源;

解决方案:定位到是哪个应用程序在大量写磁盘,然后把这个应用程序迁移到其他机器上执行,避免对Redis产生影响;

原因8:绑定CPU

一些程序,在部署服务时,为了降低应用程序在多个CPU核心之间的上下文切换带来的性能损耗,会通过进程绑定CPU的方式来提高性能;

如果Redis也采用这种方式的话,则可能会存在问题,原因在于:

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

Redis Server除了主线程服务客户端请求之外,还会创建子进程、子线程。其中子进程用于数据持久化,而子线程用于执行一些比较耗时操作,如果把Redis进程只绑定了一个CPU逻辑核心上,那么当Redis在进行数据持久化时,fork出的子进程会继承父进程的CPU 使用偏好。而此时的子进程会消耗大量的CPU资源进行数据持久化,这就会导致子进程会与主进程发生CPU争抢,进而影响到主进程服务客户端请求,访问延迟变大。

那如何解决这个问题呢?

如果真的想要绑定CPU,不要让Redis进程只绑定在一个CPU逻辑核上,而是绑定在多个逻辑核心上,而且绑定的多个逻辑核心最好是同一个物理核心,这样它们还可以共用L1/L2 Cache。在一定程度上缓解主线程、子进程、后台线程在CPU资源上的竞争,毕竟这些子进程、子线程还是会在这多个逻辑核心上进行切换,存在性能损耗。

那如何再进一步优化?

可以让主线程、子进程、后台线程,分别绑定在固定的CPU核心上,不让它们来回切换,他们各自使用的CPU资源互不影响。Redis  6.0版本已经推出了这个功能,可以对主线程、后台线程、后台RDB进程、AOF rewrite进程来绑定固定的CPU逻辑核心:

# Redis Server 和 IO 线程绑定到 CPU核心 0,2,4,6

server_cpulist 0-7:2

# 后台子线程绑定到 CPU核心 1,3

bio_cpulist 1,3

# 后台 AOF rewrite 进程绑定到 CPU 核心 8,9,10,11

aof_rewrite_cpulist 8-11

# 后台 RDB 进程绑定到 CPU 核心 1,10,11

# bgsave_cpulist 1,10-1

总体来说,Redis性能已经足够优秀,除非对Redis性能有更加严苛的要求,否则不建议你绑定CPU。

原因9:使用Swap

如果Redis突然变得非常慢,每次的操作耗时都达到了几百毫秒甚至秒级,此时可以检查下Redis是否使用到了Swap;

操作系统为了缓解内存不足对应用程序的影响,允许把一部分内存中的数据换到磁盘上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域就是Swap

当内存中的数据被换到磁盘上后,Redis再访问这些数据时,就需要从磁盘上读取,访问磁盘的速度要比访问内存慢几百倍,可以通过以下方式来查看Redis进程是否使用到了Swap:

# 先找到 Redis 的进程 ID

$ ps -aux | grep redis-server

# 查看 Redis Swap 使用情况

$ cat /proc/$pid/smaps | egrep '^(Swap|Size)'

如果是几百兆甚至上GB的内存被换到了磁盘上,那么你就需要警惕了,这种情况 Redis 的性能肯定会急剧下降,此时的解决方案是:

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

②整理内存空间,释放出足够的内存供Redis使用,然后释放 Redis的Swap,让Redis重新使用内存;只不过释放Redis的Swap过程通常要重启实例,为了避免重启实例对业务的影响,一般会先进行主从切换,然后释放旧主节点的Swap,重启旧主节点实例,待从库数据同步完成后,再进行主从切换即可;

预防的办法就是对Redis机器的内存和Swap使用情况进行监控,在内存不足或使用到Swap时报警出来及时处理;

原因10:碎片整理

Redis数据都存储在内存中,当频繁修改Redis中数据时,就有可能会导致Redis产生内存碎片。内存碎片会降低Redis的内存使用率,可以通过执行INFO命令得到实例的内存碎片率:

# Memory

used_memory:5709194824

used_memory_human:5.32G

used_memory_rss:8264855552

used_memory_rss_human:7.70G

mem_fragmentation_ratio:1.45

内存碎片率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碎片整理的参数配置如下:

# 开启自动内存碎片整理(总开关)

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

原因11:网络带宽过载

如果以上产生性能问题的场景,都规避掉了,且Redis也稳定运行了很长时间,但在某个时间点之后开始,操作Redis突然开始变慢了,而且一直持续下去,这种情况又是什么原因导致?

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

网络带宽过载的情况下,服务器在TCP层和网络层就会出现数据包发送延迟、丢包等情况。如果确实出现这种情况,你需要及时确认占满网络带宽Redis实例,如果属于正常的业务访问,那就需要及时扩容或迁移实例了,避免因为这个实例流量过大,影响这个机器的其他实例;

;