在 Redis 的实际应用中,有一个非常严重的问题,就是 Redis 突然变慢了。举个例子,在秒杀场景下,一旦 Redis 变慢了,大量的用户下单请求就会被拖慢,也就是说,用户提交了下单申请,确没有得到响应,这会给用户带来非常糟糕的体验,甚至可能会导致用户流失。
而且,在实际的生产环境中,Redis 往往只是业务系统中的一个环节,(例如作为缓存或是数据库)。一旦 Redis 上的请求延迟增加,就可能引起业务系统中的一串儿“连锁反应”。
举个小例子: 应用服务器(App Server)要完成一个事务操作,包括在 Mysql 上执行一个写事务,在 Redis 上插入一个标记位,并通过一个第三方服务给用户发送一条完成的消息。 这三个操作需保证原子性,所以如果此时 Redis 的延迟增加,就会拖累 App Server 端整个事务的执行了。
这个时候,切忌“急病乱投医”。如果没有一套行之有效的应对方案,大多数时候我们只能各种尝试,做无用功。在前面,我们已经学校了会导致 Redis 变慢的潜在阻塞点以及相应的解决方案,即线程异步机制和 CPU 核绑定。除此之外,还有一些因素会导致 Redis 变慢。
如何判断 Redis 是不是真的变慢了,一个最直接的方法就是查看 Redis 的响应延迟, 这种方法是看 Redis 延迟的绝对值。
大部分的时候,Redis 延迟很低,单在某些时刻,有些 Redis 实例会出现很高的响应延迟,甚至能达到几秒到十几秒,,不过持续时间不长,这也叫延迟 “毛刺”。当你返现 Redis 命令的执行时间突然就增长到了几秒,基本就可以认定是 Redis 变慢了。
但是在不同的软硬件环境下, Redis 本身的绝对性能并不相同。比如,在有的环境中,当延迟为 1ms 时,就判定 Redis 变慢了,但是当你的硬件配置高,那么,在你的运行环境,可能延迟 0.2ms 的时候,你就可以认定 Redis 变慢了。
第二种判断 Redis 是否变慢的方法:基于当前环境下的 Redis 基线性能做判断。
所谓基线性能,就是一个系统在低压力、无干扰下的基本性能,这个性能只由当前的软硬件配置决定。
怎么确定基线性能呢?
实际上,从 2.8.7 版本开始,redis-cli
命令提供了 -intrinsic-latency
选项,可以用来监测和统计测试期间内的最大延迟,这个延迟可以作为 Redis 的基线性能。其中,测试时长可以用 --intrinsic-latency
选项的参数来指定。
举个例子,比如说,我们运行下面的命令,该命令会打印 120 秒内监测到的最大延迟。可以看到,这里的最大延迟是 327 微妙,也就是基线性能为 327 微妙。一般情况下,运行 120 就足够监测到最大延迟了,所以,我们可以把参数设置为 120。
redis-cli --intrinsic-latency 120
Max latency so far: 1 microseconds.
Max latency so far: 32 microseconds.
Max latency so far: 105 microseconds.
Max latency so far: 108 microseconds.
Max latency so far: 115 microseconds.
Max latency so far: 250 microseconds.
Max latency so far: 258 microseconds.
Max latency so far: 302 microseconds.
Max latency so far: 327 microseconds.
3560345553 total runs (avg latency: 0.0337 microseconds / 33.70 nanoseconds per run).
Worst run took 9702x longer than the average latency.
需要注意的是,基线性能和当前操作系统、硬件配置相关。因此,我们可以把基线性能和 Redis 运行时的延迟结合起来,再进一步判断 Redis 性能是否变慢了。
一般来说,你要把运行时延迟和基线性能进行对比,如果你观察到 Redis 运行时延迟是其基线性能的 2 倍及以上,就可以认定 Redis 变慢了。
判断基线性能这一点,对于在虚拟化环境下运行的 Redis 来说,非常重要。这是因为,在虚拟化环境(如虚拟机或容器)中,由于增加了虚拟化软件层,与物理机相比,虚拟机或容器本身就会引起一定的性能开销,所以基线性能可能会高一些。下面的测试结果,显示的就是某一个虚拟机上运行 Redis 时测的基线性能。
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.
可以看到,由于虚拟化软件本身的开销,此时的基线性能已经达到 9.871ms。如果该 Redis 实例的运行延迟为 10 ms,这并不能算作性能变慢,因此此时,运行时延迟只比基线性能增加了 1.3% 。如果,你不了解基线性能,一看到较高的运行时延迟,就有可能误判 Redis 变慢了。
不过,我们通常是通过客户端和网络访问 Redis 服务,为了避免网络对基线性能的影响,刚刚的命令是在服务器端直接运行的,也就是说,我们只考虑服务器端软硬件环境的影响。
如果想要了解网络对 Redis 性能的影响,一个简单的方法是用 iPerf
这样的工具,测量从 Redis 客户端到服务端的网络延迟。如果这个延迟有急事号码甚至是几百毫秒,就说明 Redis 运行的网络环境中很可能有大流量的其他应用承诺需在运行,导致网络拥塞了。这个时候,你需要协调网络运行,调整网络的流量分配了。
经历上一步,你已经能确定 Redis 是否变慢了。一旦发现变慢了,接下来,就要开始查找原因并解决这个问题了。你要基于自己对 Redis 本身的工作原理的理解,并且结合和它交互的操作系统、存储及网络等外部系统关键机制,在借助一些辅助工具来定位原因,并制定行之有效的解决方案。
下图是第一节绘制的 Redis 架构图。你可以重点关注下图上新增的红色模块,也就是 Redis 自身的操作特性、文件系统和操作系统,它们是影响 Redis 性能的三大要素。
接下来,将从这三大要素入手,结合实际的应用场景,依次介绍从不同要素触发排查和解决问题的实践经验。
首先我们来学习下 Redis 提供的键值对命令操作对延迟性能的影响。重点介绍两类关键操作:慢查询命令和过期 key 操作。
慢查询命令,是指在 Redis 中执行速度慢的命令,这会导致 Redis 延迟增加。
Redis 提供的命令操作很多,并不是所有命令都慢,这个操作的复杂度有关。所以,要知道 Redis 的不同命令的复杂度。
比如说,Value类型为 String 时,GET/SET 操作主要就是操作 Redis 的哈希表索引,这个操作的复杂度是固定的,为 O(1)
。但是当 Value 类型为 Set 时,SORT、SUNION/SMEMBERS 操作的复杂度分别为 O(N+M*log(M))
和 O(N)
。其中,N 为 Set 中元素的个数,M 为 SORT 操作返回元素的个数,这个复杂度就增加了很多。Redis 官网中对每个命令的复杂度都有介绍,你可以直接查询。
如何排查这个问题呢? 下面给你排查建议和解决方法,这也是本章第一个方法。
当你发现 Redis 性能变慢时,可以通过 Redis 日志,或者是 latency monitor 工具,查询变慢的请求,根据请求对应的具体命令以及官方文档,确认下是否采用了复杂度高的慢查询命令。
如果的确有大量的慢查询,有两种处理方式:
当然,如果业务逻辑就是要求使用慢查询命令,你得考虑采用性能更好的 CPU,更快的完成查询命令,避免慢查询的影响。
还有一个比较容易忽略的慢查询命令,就是 KEYS。它用于返回和输入模式匹配的所有 key,例如,以下命令返回所有包含 “name
” 字符串的 keys。
KEYS *names*
因为 KEYS 命令会遍历存储的键值对,所以操作延迟高。如果你不了解它的实现而使用了它,就会导致 Redis 性能变慢。所以,KEYS 命令一般不建议用于生产环境中。
接下来,看下 过期 key 的自动删除机制。 它是 Redis 用来回收内存空间的常用机制,应用广泛,本身就会引起阻塞,导致性能变慢,所以你必须要知道该机制对性能的影响。
Redis 键值对的 key 可以设置过期时间。默认情况下,Redis 每 100 毫秒会删除一些过期 key,具体算法如下:
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
个数的 key,并将其中过期的 key 全部删除。ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
是 Redis 的一个参数,模式是 20,那么,一秒内基本有 200 个过期 key 会被删除。这一策略对清楚过期 key 释放内存空间很有帮助。如果每秒删除 200 个过期key,并不会对 Redis 造成太多影响。
但是,如果触发了上面这个算法的第二条,Redis 会一直删除以释放内存空间。注意,删除操作是阻塞的(Redis 4.0 后可以采用异步线程机制来减少阻塞影响
)。所以,一旦该条件触发,Redis 的线程就会被一直执行删除,这样一来,就没有办法正常服务其他的键值操作了,Redis 会变慢。
那么,第二条是如何触发的呢? 其中一个重要来源,就是频繁使用带有相同时间参数的 EXPIREAT 命令设置过期 KEY,这会导致在同一秒内有大量的 key 同时过期。
排查方法:检查业务代码在使用 EXPIREAT 设置 key 过期时间时,是否使用了相同的 unix 时间戳
,有没有使用 EXPIRE 命令给批量 key 设置相同的过期秒数。
解决办法:首先要根据实际业务的使用需求,决定 EXPIREAT 和 EXPIRE 的过期时间参数。其实,如果一批 KEY 的确是同时过期,你可以在 EXPIREAT 和 EXPIRE 的过期时间参数上,加上一个一定大小范围的随机数,这样,既保证了 key 在一个邻近的范围内被删除,又避免了同时过期造成的压力。
但是,如果在排查时,发现 Redis 没有执行大量的慢查询命令,也没有同时删除大量过期 key,那就说明,你要关注影响性能的其他机制了,也就是文件系统和操作系统。
Redis 会持久化报错数据到磁盘,这个过程要依赖于文件系统来完成,所以,文件系统将数据写回磁盘的机制,会直接影响到 Redis 持久化的效率。而且,在持久化过程中,Redis 还在接收其他的请求,持久化效率的高低会影响到 Redis 处理请求的性能。
另一方面,Redis 是内存数据库,内存操作非常频繁,所以,操作系统的内存机制会直接影响到 Redis 的处理效率。比如说,Redis 内存不够了,操作系统会启动 swap 机制,这就会直接拖慢 Redis。
为了保证数据可靠性,Redis 会采用 AOF 日志或 RDB 快照。其中,AOF 日志提供了三种写回策略: no、everysec、always。这三种策略依赖文件系统的两个系统调用完成:write 和 fsync。
当写回策略配置为 everysec、always 时,Redis 需要调用 fsync 把日志写回磁盘。但是,这两种写回策略的具体执行情况是不一样的。
在使用 everysec 时,Redis 会允许丢失 1 秒的操作记录,所以 Redis 主线程并不需要确保每个操作记录日志都写回磁盘。而且,fsync 执行时间很长,如果在主线程中执行 fsync 就容易阻塞主线程。所以,当写回策略为 everysec 时,Redis 会使用后台子线程异步完成 fsync 的操作。
而对于 always 来说,Redis 需要确保每个操作记录都写回磁盘,如果用后台子线程异步完成,主线程是无法及时地知道每个操作是否完成了,这就不符合 always 策略的要求了。所以,always 策略是在主线程中完成的。
另外,使用 AOF 日志时,为了避免日志不断增大,Redis 会执行 AOF 重写,生成体量缩小的新 AOF 日志文件。 AOF 重写本身需要的时间很长,也容易阻塞主线,所以 Redis 使用子进程来进行 AOF 重写。
但是,这里有一个潜在的风险带你:AOF 重写会对磁盘进行大量 IO 操作,同时,fsync 又需要等到数据写道磁盘后才能返回,所以,当 AOF 重写的压力比较大时,就会导致 fsync 被阻塞。虽然 fsync 是由后台子线程负责执行的,但是,主线程会监控 fsync 的执行进度。
当主线程使用后台子线程执行了一次 fsync,需要再次把新接收的操作记录写回磁盘时,如果主线程发现上一次的 fsync 没有执行完,那么它就会阻塞。所以,如果后台子线程执行 fsync 频繁阻塞的话(比如 AOF 重写占用了大量的磁盘 IO 带宽),主线程也会阻塞,导致 Redis 性能变慢。
画一张图来展示下磁盘压力小和压力大的情况下,fsync 后台子线程和主线程收到的影响。
好了,说到这里,你已经了解了,由于 fsync 后台子线程和 AOF 重写子进程的存在,主 IO 线程一般不会被阻塞。但是,如果在重写日志时,AOF 重写子进程的写入量比较大,fsync 线程也会被阻塞,进而进而阻塞主线程,导致延迟增加。
首先,你可以检查下 Redis 配置文件中的 appendsync 配置项,该配置项的取值表明了 Redis 实例使用的是哪种 AOF 写回策略,如下所示:
如果是 everysec 或 always 配置,请先确认下业务方对数据可靠性的要求,明确是否需要每一秒或每一个操作都记日志。有的业务方不了解 AOF 的机制,和可能直接使用数据可靠性登记最高的 always 配置了。其实在有些场景中(例如,Redis 用于缓存),数据丢了还可以从后端数据库中获取,并不需要很高的数据可靠性。
如果业务对延迟非常敏感,单允许一定量的数据丢失,那么可以把配置项 no-appendfsync-on-rewrite 设置为 yes,如下:
no-appendfsync-on-rewrite yes
这个配置项设置为 yes 时,表示在 AOF 重写时,不进行 fsync操作。也就是说,Redis 把实例命令写到内存后,不调用后台线程进行 fsync 操作,就可以直接返回。当日,如果此时宕机,就会导致数据丢失。反之,如果这个配置项为 no(也是默认配置),在 AOF 重写时,Redis 实例仍然会调用后台线程进行 fsync 操作,这就会给实例带来阻塞。
如果的确需要高性能,同时也需要高可靠,建议你考虑采用高速的固态硬盘作为 AOF 日志的写入设备。
高速固态盘的带宽和并发度比传统机械硬盘要高出 10 倍以上。在 AOF 重写和 fsync 后台线程同时执行时,固态硬盘可以提供较为充足的磁盘 IO 资源,让 AOF 重写和 fsync 后台线程的磁盘 IO 资源竞减少,从而降低对 Redis 性能的营销。
如果 Redis 的 AOF 日志策略为 no,或者没有采用 AOF 模式,还有什么问题会导致性能变慢吗?
操作系统的内存 swap。
内存 swap 是操作系统里将内存和磁盘间来回换入和换出的机制,涉及到磁盘的读写,所以,一旦触发 swap,无论是被换入数据的进程,还是被换出数据的进程,其性能都会受到慢速磁盘读写的影响。
Redis 是内存数据库,内存使用量大,如果没有控制内存的使用量,或者和其他内存需求大的应用一起运行了,就可能受到 swap 的影响,导致性能变慢。
正常情况下,Redis 的操作都是直接通过访问内存完成,一旦 swap 触发了,Redis 的请求操作需要等待磁盘数据读写完成才行。而且,和刚才说的 AOF 日志文件读写时使用 fsync 线程不同,swap 触发后影响的是 Redis 的主 IO 线程,这会极大的增加 Redis 的响应时间。
通常,触发 swap 的原因主要是物理机器内存不足,对于 Redis 而言,有两种场景情况:
针对这个问题,需要增加机器的内存或者使用 Redis 集群。
操作系统本身会在后台记录每个进程的 swap 使用情况,即有多少数据量发生了 swap。
可以先通过如下命令查看 Redis 的进程号,这里是 93
$ ./redis-cli info | grep process_id
process_id: 93
然后,进入 Redis 所在机器的 /proc 目录下该进程目录中:
$ cd /proc/93
最后,运行下面的命令,查看该 Redis 进程的使用情况。在这儿,我只截取了部分结果:
$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
每一行 Size 表示的是 Redis 实例所用的一块内存大小,而 Size 下方的 Swap 和它相对应,表示这块 Size 大小的内存区域有多少已经被换出到磁盘上了。如果这两个值相等,就表示这块内存区域已经完成被换出到磁盘了。
作为内存数据库,Redis 本身就会使用很多大小不一的内存块,所以,你可以看到有很多Size 行,有的很小只有 4KB,有的很大有 462044 kB 。不同内存块被换出到磁盘上的大小也不一样,例如上面结果中的第一个 4KB 内存块,它下发的 Swap 也是 4KB,这表明这个内存块已经被换出了;另外,462044 kB 这个内存块也被换出了 462008 kB,差不多有 462 MB。
这里有个重要的地方需要说明下,当出现
百 MB
,甚至GB
级别的 swap 大小时,说明此时的 Redis 实例的内存压力很大,很有可能会变慢。所以 swap 的大小是排查 Redis 性能变慢是否由 swap 引起的重要指标。
一旦发生内存 swap,最直接的解决办法就是增加机器内存。如果该实例在一个 Redis 切片集群中,可以增加 Redis 集群实例的个数,来分摊每个实例服务的数据量,进而减少每个实例所需的内存量。
如果 Redis 实例和其他操作大量文件的程序(例如数据分析程序)共享机器,你可以将 Redis 实例迁移到单独的机器上运行,以满足它的内存需求。如果该实例正好是 Redis 主从集群的主库,而从库的内存很大,也可以考虑进行主从切换,把2大内存的从库变成主库。
除了内存 swap,还有一个和内存相关的因素,即内存大页机制(Transparent Huge Page,THP),也会影响 Redis 性能。
Linux 内核从 2.6.38 开始支持大页内存,该机制支持 2MB 大小的内存也分配,而常规的内存也分配是按 4 KB 的粒度来执行的。
虽然大页内存可以给 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
为了方便你应用,我梳理了一个包含 9 个检查点的 CheckList,希望在你遇到 Redis 性能变慢时,按照这些步骤注意检查,高效地解决问题:
no-appendfsync-no-rewrite
设置为 yes,避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。如果既需要高性能有需要高可靠,最好使用高速固态盘作为 AOF 日志的写入盘。CPU Socket
上。为了保证 Redis 高性能,我们需要给 Redis 充足的计算、内存和 IO 资源,给它提供一个“安静”的环境。