如何判断redis是不是真的变慢了

(1)一个最直接的方法,就是查看redis的响应延时

  • 大部分时候,redis延迟很低,但是在某些时刻,有些redis实例会出现很高的响应延时,甚至能达到几秒到十几秒,不过持续时间不长,这也叫延迟“毛刺”。当你发现Redis 命令的执行时间突然就增长到了几秒,基本就可以认定 Redis 变慢了。
  • 在不同的软硬件环境下,Redis 本身的绝对性能并不相同。比如,在我的环境中,当延迟为 1ms 时,我判定 Redis 变慢了,但是你的硬件配置高,那么,在你的运行环境下,可能延迟是 0.2ms 的时候,你就可以认定 Redis 变慢了。

(2)第二个方法是,基于当前环境下的redis基线性能做判断

  • 所谓基线性能,就是一个系统在低压力、无干扰下的基本性能,这个性能只由当前的软硬件配置决定
  • 我们可以把基线性能和redis运行时的延迟结合起来,再进一步判断redis性能是否变慢了。 一般来说,要把运行时延迟和基线性能进行对比,如果redis运行时延迟是其基线性能的2倍以上,就可以认定redis变慢了
  • 判断基线性能这一点,对于在虚拟化环境下运行的redis来说,非常重要。这是因为,在虚拟化环境(比如虚拟机、容器)中,增加了虚拟化软件层,与物理机相比,虚拟机或容器本身就会引入一定的性能开销,所以基线性能会高一些

那具体怎么确定基线性能呢?

  • 自redis2.8.7 版本开始,redis-cli 命令提供了–intrinsic-latency 选项,可以用来检测和统计计测试期间内的最大延迟,这个延迟可以作为 Redis 的基线性能。其中,测试时长可以用–intrinsic-latency 选项的参数来指定,一般指定为120s。
  • 我们通常是通过客户端和网络访问redis服务,为了避免为了对基线性能的影响,这个命令需要在服务端直接运行,也就是说,我们只考虑服务器端软硬件环境的影响。
  • 如果想了解网络对redis性能的影响,一个简单的方法是用 iPerf 这样的工具,测量从Redis 客户端到服务器端的网络延迟。如果这个延迟有几十毫秒甚至是几百毫秒,就说明,Redis 运行的网络环境中很可能有大流量的其他应用程序在运行,导致网络拥塞了。这个时候,就需要协调网络运维,调整网络的流量分配了。

看个例子:

  • 如下为物理机的基线性能,最大延迟是 119 微秒,也就是基线性能为 119 微秒
$ ./redis-cli --intrinsic-latency 120  #打印 120 秒内监测到的最大延迟
Max latency so far: 17 microseconds.
Max latency so far: 44 microseconds.
Max latency so far: 94 microseconds.
Max latency so far: 110 microseconds.
Max latency so far: 119 microseconds.
36481658 total runs (avg latency: 3.2893 microseconds / 3289.32 nanoseconds pe
  • 如下为虚拟机测试的基线性能。可以看到,由于虚拟化软件本身的开销,此时的基线性能已经达到了 9.871ms。如果该Redis 实例的运行时延迟为 10ms,这并不能算作性能变慢,因为此时,运行时延迟只比基线性能增加了 1.3%。
$ ./redis-cli --intrinsic-latency 120
Max latency so far: 692 microseconds.
Max latency so far: 915 microseconds.
Max latency so far: 2193 microseconds.
Max latency so far: 9343 microseconds.
Max latency so far: 9871 microseconds.

哪些因素会导致redis性能变慢呢?

如下图红色部分,一般来讲,redis自身特性、文件系统和操作系统,都有可能导致redis性能下降

在这里插入图片描述

redis自身特性的影响

主要由两种命令会对延迟性能有影响:慢查询命令和过期key操作

慢查询命令

慢查询命令,就是指在redis中执行速度慢的命令,这会导致redis延迟增加。redis提供的命令很多,但并不是所有的命令都慢,这和命令操作的复杂度有关。

那怎么找出哪些命令慢呢?可以通过redis日志,或者latency monitor 工具,查询变慢的请求,根据请求对应的具体命令和官方文档,确定是否采用了复杂度高的慢查询命令。

如果的确有大量的慢查询命令,有两种处理方法:

  • 用其他高效命令代替。比如,如果你需要返回一个set中的所有成员时,不要使用SMEMBERS 命令,而是要使用SSCAN多次迭代返回,避免一次返回大量数据,造成线程阻塞
  • 当你需要执行排序、交集、并集操作时,可以在客户端完成,而不要用 SORT、SUNION、SINTER 这些命令,以免拖慢 Redis 实例。

当然,如果业务逻辑就是要求使用慢查询命令,那就要考虑采用性能更好的CPU,更快的完成查询命令,避免慢查询的影响。

还有一个慢查询命令需要关注,就是keys,因为keys命令需要遍历存储的键值对,所以操作延时高。所以,KEYS命令一般不被建议用在生产环境中

过期key操作

过期key的自动删除机制,是redis用来回收内存空间的常用机制,应用广泛,本身就会引起redis操作阻塞,导致性能变慢。

redis键值对的key可以设置过期时间。默认情况下,redis每100ms就会删除一些过期key,具体的算法如下:

  • 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP个数的key,并将其中过期的key全部删除
  • 如果超过25%的key过期了,则重复删除的过程,直到过期key的比例降到25%以下。

ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 是 Redis 的一个参数,默认是 200,也就是一秒内基本有200个过期的key会被删除。这一策略对清除过期key、释放内存空间很有帮助,如果每秒钟删除200个过期的key,并不会对redis造成太大影响。

但是,如果触发了上面这个算法的第2条,redis就会一直删除以释放内存空间。注意,删除操作是阻塞的(redis4.0后可以用异步线程机制来减少阻塞影响)。所以,一旦该条件触发,redis的线程就会一直执行删除,这一一来,就没办法正常服务其他的键值操作了,就会进一步引起其他简直操作的延时增加,redis就会变慢。

那么,算法的第2条是怎么被触发的呢?其中一个重要来源,就是频繁使用带有相同时间参数的EXPIPEAT命令设置过期key,这就会导致,在同一秒内有大量的key同时过期。

那怎么解决呢?

  • 先根据实际需求,决定EXPIPEAT的过期时间参数
  • 其次,如果一批key确时需要同时过期,可以在EXPIPEAT的过期时间参数上,加上一个大小范围内的随机数。这样,既保证了key在一个邻近时间范围内被删除,又避免了同时过期造成的压力

如果即没有大量的慢查询也没有删除大量过期key,那有可能是文件系统和操作系统因素了

  • redis会持久化数据保存到磁盘,这个过程要依赖文件系统来完成,所以,文件系统将数据写回磁盘的机制,会直接影响到redis。而且,在持久化的过程中,redis也还在接收其他请求,持久化的效率高低又会影响到redis处理请求的性能
  • 另一方面,redis是内存数据库,内存操作十分频繁,所以操作系统的内存机制会直接影响到redis的处理效率。比如,如果redis的内存不够用了,操作系统会启动swap机制,这就会直接拖慢redis

文件系统:AOF机制

为了保证数据的可靠性,redis会采用AOF日志或者RDB快照。其中,AOF日志提供了三种回写日志策略:no、everysec、always。这三种写回策略依赖文件系统的两个系统调用完成,也就是write和fsync

  • write只要把日志记录写到内核缓冲区,就可以返回了,并不需要等待日志实际写回到磁盘
  • fsync需要把日志记录写回到磁盘后才能返回,时间较长

在这里插入图片描述
当写回策略配置为 everysec 和 always 时,Redis 需要调用 fsync 把日志写回磁盘。但是,这两种写回策略的具体执行情况还不太一样。

  • 在使用everysec时,redis允许丢失一秒的操作记录,所以,redis主线程并不需要确保每个操作记录日志都写回磁盘。而且,fsync的执行时间很长,如果是在redis主线程中执行fsync,就容易阻塞主线程。所以,当写回策略配置为everysec时,redis会使用后台的子线程异步完成fsync操作
  • 对于always来说,redis需要确保每个操作记录日志都写回磁盘,如果用后台子线程异步完成,主线程就无法及时的知道每个操作是否已经完成了,这就不符合always策略的要求了。所以,always策略并不使用后台子线程来执行

另外,在使用AOF日志是,为了避免日志文件不断增大,redis会执行AOF重写,生成体量缩小的新的AOF日志文件。

  • AOF重写本身需要的时间很长,也容易阻塞redis主线程,所以,redis使用子线程来进行AOF重写
  • 但是,AOF重写会对磁盘进行大量IO操作,同时,fsyn又需要等到数据写到磁盘后才能返回,所以,当AOF重写的压力比较大时,就会导致fsync被阻塞。虽然fsync是由后台子线程复制执行时,但是主线程会监控fsync的执行进度
  • 当主线程使用后台子线程执行了一次fsync,需要再次把新接收到的操作记录写回磁盘时,如果主线程发现上一次的 fsync 还没有执行完,那么它就会阻塞。所以,如果后台子线程执行的 fsync 频繁阻塞的话(比如 AOF 重写占用了大量的磁盘 IO 带宽),主线程也会阻塞,导致 Redis 性能变慢。

在这里插入图片描述

  • 也就是说,由于fsync后台子线程和AOF重写子进程的存在,主IO线程一般不会被阻塞。但是,如果在重写日志时,AOF重写子进程的写入了比较大,fsync线程也会被阻塞,进而阻塞主线程,导致延迟增加。

怎么排查呢?

  • 可以检查下 Redis 配置文件中的 appendfsync 配置项,该配置项的取值表明了redis实例使用的是哪种AOF写回策略。 如果AOF写回策略使用了everysec或always配置。请先确认下业务方对数据可靠性的要求,明确是否确实需要每一秒或每一个操作都记日志。
appendfsync no
appendfsync evenysec
appendfsync always
  • 如果业务应用对延迟非常敏感,但同时允许一定量的数据丢失,那么,可以把配置项noappendfsync-on-rewrite 设置为 yes
    • yes表示redis写命令到内存之后,不调用后台线程进行fsync操作,就可以直接返回。缺点是宕机会导致数据丢失
    • no(默认项)表示在AOF重写是,redis实例会调用后台线程进行 fsync 操作,这就会给实例带来阻塞。
no-appendfsync-on-rewrite yes
  • 如果的确需要高性能,同时也需要高可靠数据保证,建议采用高速的固态硬盘作为 AOF 日志的写入设备。

操作系统

swap

  • 内存swap是操作系统里将内存数据和磁盘来回换入换出的机制,设计到磁盘的读写。所以,一旦触发swap,无论是被换入数据的进程,还是被换出数据的进程,其性能都会受到慢速磁盘读写的影响。
  • redis是内存数据库,内存使用量大,如果没有控制好内存的使用量,或者和其他内核需求大的应用一起运行,就可能受到swap的影响,而导致性能变慢:正常情况下,redis的操作是直接通过访问内存就能完成,一旦swap被触发了,redis的请求需要等待磁盘数据读写完成才行,这会影响到redis的主IO线程,极大的增加redis的响应时间。

那么,什么时候会触发 swap 呢?通常,触发wap的原因主要是物理机器内存不足,对于redis而言,主要有两种情况:

  • redis实例自身使用了大量的内存,导致物理机制的可用内存不足
  • 和redis实例在同一台机器上运行的其他进程,在进行大量的文件读写操作。文件读写操作本身会占用系统内存,这会导致分配给redis实例的内存量变少,进而触发redis发送swap

怎么判断是不是有内存交换?

  • 操作系统本身会在后台记录每个进程的swap使用情况。所以,需要先找出redis的进程号,下面是5332
$ redis-cli info | grep process_id
process_id: 5332
  • 然后,进入 Redis 所在机器的 /proc 目录下的该进程目录中:
$ cd /proc/5332
  • 最后,运行下面的命令,查看该 Redis 进程的使用情况
    • 下面每一行Size表示redis实例所用的一块内存大小,而Size下面的Swap和它相对应,表示这块Size大小的内存区域有多少已经被换出到磁盘上了。如果这两个值相等,就表示这块内存区域已经完全被换出到磁盘了。
    • 作为内存数据库,redis本身就会使用很多大小不一的内存块。表示这块内存区域已经完全被换出到磁盘了。上的大小也不一样,例如下面的结果中的第一个 4KB 内存块,它下方的 Swap 也是 4KB,这表示这个内存块已经被换出了;另外,462044KB 这个内存块也被换出了 462008KB,差不多有 462MB。
    • 注意,当出现百MB,甚至GB级别的swap大小是,就表明,此时redis实例的内存压力很大,很有可能会变慢。所以,swap 的大小是排查Redis 性能变慢是否由 swap 引起的重要指标。
$cat smaps | egrep '^(Swap|Size)'
Size: 584 kB
Swap: 0 kB
Size: 4 kB
Swap: 4 kB
Size: 4 kB
Swap: 0 kB
Size: 462044 kB
Swap: 462008 kB
Size: 21392 kB
Swap: 0 kB

怎么解决呢?增加机器的内存或者使用 Redis 集群

  • 一旦发生内存swap,就直接的解决方法就是增加机器内存。如果该实例在一个redis切片集群中,可以增加redis集群的实例个数,来分摊每个实例服务的数据量,进而减少每个实例所需要的内存量
  • 如果redis实例和其他操作大量文件的进程共享机器,可以把redis实例迁移到单独的机器上运行。如果该实例正好是redis主从集群中的主库,而从库的内存很大,也可以考虑主从切换,把大内存的从库变成主库,由它来处理客户端请求

内存大页

除了内存 swap,还有一个和内存相关的因素,即内存大页机制(Transparent Huge Page, THP),也会影响 Redis 性能。

Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的

很多人都觉得:“Redis 是内存数据库,内存大页不正好可以满足 Redis 的需求吗?而且在分配相同的内存量时,内存大页还能减少分配次数,不也是对 Redis 友好吗?”

其实,系统的设计通常是一个取舍过程,我们称之为 trade-off。很多机制通常都是优势和劣势并存的。Redis 使用内存大页就是一个典型的例子。

  • 虽然内存大页可以给redis带来内存分配方面的收益,但是,redis为了提供数据的可靠性,需要将数据做持久化保存,这个写入过程由额外的线程执行。所以,此时,Redis 主线程仍然可以接收客户端写请求,客户端的写请求可能会修改正在进行持久化的数据。这一过程中,redis就会采用写时复制机制,也就是说,一旦有数据要被修改,redis并不会直接修改内存中的数据,而是将这些数据拷贝一份,然后再进行修改
  • 如果采用了内存大页,那么,即使客户端请求只修改了100B的数据,redis也需要拷贝2MB的大页。相反,如果是常规内存页机制,只用拷贝4KB。
  • 也就是说,当客户端请求修改或者新写入的数据较多时,内存大页机制将导致大量的拷贝,这就会影响redis的正常访存操作,最终导致性能变慢

怎么解决呢?关闭内存大页即可(实际生产环境中部署中不要使用内存大页)

  • 首先,我们要先排查下内存大页。方法是在 Redis 实例运行的机器上执行如下命令:
cat /sys/kernel/mm/transparent_hugepage/enabled
  • 如果执行结果是 always,就表明内存大页机制被启动了;如果是 never,就表示,内存大页机制被禁止。
  • 关闭则运行下面命令
echo never /sys/kernel/mm/transparent_hugepage/enabled

总结

redis性能变慢怎么办?步骤如下:

  • 获取 Redis 实例在当前环境下的基线性能。
  • 是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。
  • 是否对过期 key 设置了相同的过期时间?对于批量删除的 key,可以在每个 key 的过期时间上加一个随机数,避免同时删除。
  • 是否存在 bigkey? 对于 bigkey 的删除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是 Redis 4.0 以前的版本,可以使用SCAN 命令迭代删除;对于 bigkey 的集合查询和聚合操作,可以使用 SCAN 命令在客户端完成。
  • Redis AOF 配置级别是什么?业务层面是否的确需要这一可靠性级别?如果我们需要高性能,同时也允许数据丢失,可以将配置项 no-appendfsync-on-rewrite 设置为yes,避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。当然, 如果既需要高性能又需要高可靠性,最好使用高速固态盘作为 AOF 日志的写入盘。
  • Redis 实例的内存使用是否过大?发生 swap 了吗?如果是的话,就增加机器内存,或者是使用 Redis 集群,分摊单机 Redis 的键值对数量和内存压力。同时,要避免出现Redis 和其他内存需求大的应用共享机器的情况。
  • 在 Redis 实例的运行环境中,是否启用了透明大页机制?如果是的话,直接关闭内存大页机制就行了。
  • 是否运行了 Redis 主从集群?如果是的话,把主库实例的数据量大小控制在 2~4GB,以免主从复制时,从库因加载大的 RDB 文件而阻塞。
  • 是否使用了多核 CPU 或 NUMA 架构的机器运行 Redis 实例?使用多核 CPU 时,可以给 Redis 实例绑定物理核;使用 NUMA 架构时,注意把 Redis 实例和网络中断处理程序运行在同一个 CPU Socket 上。
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐