18 | 波动的响应延迟:如何应对变慢的Redis?(上)
1、前言
Redis 突然变慢,不仅影响用户体验,而且会影响数据库等。
包括在 MySQL 上执行一个写事务,在 Redis 上插入一个标记位,并通过一个第三方服务给用户发送一条完成消息。
这三个操作都需要保证事务原子性,所以,如果此时 Redis 的延迟增加,就会拖累 AppServer 端整个事务的执行。这个事务一直完成不了,又会导致 MySQL 上写事务占用的资源无法释放,进而导致访问 MySQL 的其他请求被阻塞。很明显,Redis 变慢会带来严重的连锁反应。
从问题认定、系统性排查、应对方案这 3 个方面分析。
2、Redis真的变慢了么?
1)、最直接方法:查看 Redis 的响应延迟。
某些时刻,执行时间突然增长到几秒-->变慢
2)、基于当前环境下的 Redis 基线性能做判断,只能在服务器端直接运行,不然有其他因素
#打印 120 秒内监测到的最大延迟
-redis-cli --intrinsic-latency 120
Max latency so far: 692 microseconds.
Max latency so far: 915 microseconds.
3、如何应对 Redis 变慢?
Redis 自身的操作特性、文件系统和操作系统,它们是影响 Redis 性能的三大要素。
4、Redis 自身操作特性的影响
1)、慢查询命令--时间复杂度是王道
a、用其他高效命令替代,比如SSCAN替换SMEMSMEMBERS
b、需要执行排序、交集、并集操作时,可以在客户端完成,而不要用 SORT、SUNION、SINTER 这些命令,以免拖慢 Redis 实例。
c、容易忽略的慢查询命令KEYS,因为 KEYS 命令需要遍历存储的键值对,所以操作延时高,生产环境不建议用
-KEYS *name*
1) "lastname"
2) "firstname"
2)、过期Key操作
过期 key 的自动删除机制。它是 Redis 用来回收内存空间的常用机制,应用广泛,本身就会引起 Redis 操作阻塞,导致性能变慢。
Redis 键值对的 key 可以设置过期时间。默认情况下,Redis 每 100 毫秒会删除一些过期key,具体的算法如下:
1. 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数的 key,并将其中过期的key 全部删除;
2. 如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 是 Redis 的一个参数,默认是 20,那么,一秒内基本有 200 个过期 key 会被删除。这一策略对清除过期 key、释放内存空间很有帮助。如果每秒钟删除 200 个过期 key,并不会对 Redis 造成太大影响。
如果触发了上面这个算法的第二条,Redis 就会一直删除以释放内存空间。注意,删除操作是阻塞的。
怎么触发第二条?频繁使用带有相同时间参数的 EXPIREAT 命令设置过期 key,这就会导致,在同一秒内有大量的 key 同时过期。
解决方案:尽量让Key不在同一时间过期,如果一批 key 的确是同时过期,你还可以在EXPIREAT 和 EXPIRE 的过期时间参数上,加上一个一定大小范围内的随机数,这样,既保证了 key 在一个邻近时间范围内被删除,又避免了同时过期造成的压力。
5、小结
首先介绍了 Redis 性能变慢带来的重要影响,希望你能充分重视这个问题。我重点介绍了判断 Redis 变慢的方法:一个是看响应延迟,一个是看基线性能。
同时,还给了你两种排查和解决 Redis 变慢这个问题的方法:
1. 从慢查询命令开始排查,并且根据业务需求替换慢查询命令;
2. 排查过期 key 的时间设置,并根据实际使用需求,设置不同的过期时间。
性能诊断通常是一件困难的事,所以一定不能毫无目标地“乱找”。这节课的内容,就是排查和解决 Redis 性能变慢的章法,你一定要按照章法逐一排查,这样才可能尽快地找出原因。
当然,要真正把 Redis 用好,除了要了解 Redis 本身的原理,还要了解和 Redis 交互的各底层系统的关键机制,包括操作系统和文件系统。通常情况下,一些难以排查的问题是 Redis 的用法或设置和底层系统的工作机制不协调导致的。
19 | 波动的响应延迟:如何应对变慢的Redis?(下)
1、前言
文件系统、操作系统对 Redis 性能的影响
一方面:Redis 持久化保存数据到磁盘,依赖于文件系统,这个写会机制会影响 Redis 持久化效率,在持久化过程中,还会接收新的请求,持久化效率高低又会影响 Redis 处理请求的效率。
另一方面:Redis 是内存数据库,内存操作非常频繁,所以,操作系统的内存机制会直接影响到 Redis 的处理效率。比如说,如果 Redis 的内存不够用了,操作系统会启动 swap 机制,这就会直接拖慢 Redis。
2、文件系统:AOF 模式
AOF日志提供了三种日志写回策略:no、everysec、always。
这三种写回策略依赖文件系统的两个系统调用完成,也就是 write 和 fsync。
write:只要把日志记录写到内核缓冲区,就可以返回了,并不需要等待日志实际写回到磁盘;
fsync:需要把日志记录写回到磁盘后才能返回,时间较长。
写回策略是 everysec、always 时,Redis 需要调用 fsync 把日志写回磁盘。但是,这两种写回策略的具体执行情况还不太一样。
everysec 允许1s数据丢失,Redis 主线程并不需要确保每个操作记录日志都写回磁盘,而且 fsync 写回磁盘时间很长,容易阻塞主线程,所以 Redis 采用后台子线程异步完成 fsync 的操作。
always Redis 需要确保每个操作记录日志都写回磁盘,如果用后台子线程异步完成,主线程就无法及时地知道每个操作是否已经完成,这就不符合 always 策略要求。所以,always 策略并不使用后台子线程来执行。
另外,为了防止 AOF 日志变大,会进行 AOF 日志重写,重写采用的是子线程异步,
潜在风险点:重写会对磁盘IO进行大量操作,同时,fsync 又需要等到数据写到磁盘后才能返回,所以,当 AOF 重写的压力比较大时,就会导致 fsync 被阻塞。虽然 fsync 是由后台子线程负责执行的,但是,主线程会监控 fsync 的执行进度。
当主线程使用后台子线程执行了一次 fsync,需要再次把新接收的操作记录写回磁盘时,如果主线程发现上一次的 fsync 还没有执行完,那么它就会阻塞。所以,如果后台子线程执行的 fsync 频繁阻塞的话(比如 AOF 重写占用了大量的磁盘 IO 带宽),主线程也会阻塞,导致 Redis 性能变慢。
由于 fsync 后台子线程和 AOF 重写子进程的存在,主IO 线程一般不会被阻塞。但是,如果在重写日志时,AOF 重写子进程的写入量比较大,fsync 线程也会被阻塞,进而阻塞主线程,导致延迟增加。
关于 AOF 模式问题的排查跟解决建议
1)、首先,你检查 Redis 配置文件中的 appendfsync 配置项,查询 Redis 实例使用的 AOF 日志写回策略。
2)、如果 AOF 写回策略使用了 everysec 或 always 配置,先确认业务方对数据可靠性要求,明确是否需要每一秒或每一个操作都记日志。是否设置为always?
3)、如果业务应用对延迟非常敏感,但同时允许一定量的数据丢失,那么,可以把配置项 no�appendfsync-on-rewrite 设置为 yes。表示在 AOF 重写时,不进行 fsync 操作。
当然这个设置会导致,如果此时实例发生宕机,就会导致数据丢失。
反之,如果这个配置项设置为 no(也是默认配置),在 AOF 重写时,Redis 实例仍然会调用后台线程进行 fsync 操作,这就会给实例带来阻塞。
4)、如果的确需要高性能,同时也需要高可靠数据保证,建议考虑采用高速的固态硬盘作为 AOF 日志的写入设备(高速比传统的带宽高十倍以上)。
3、操作系统:swap
Redis 的 AOF 日志配置只是 no,或者就没有采用 AOF 模式,
1)、潜在瓶颈:操作系统的内存 swap。
内存 swap 是操作系统里将内存数据在内存和磁盘间来回换入和换出的机制,涉及到磁盘的读写,所以,一旦触发 swap,无论是被换入数据的进程,还是被换出数据的进程,其性能都会受到慢速磁盘读写的影响。
Redis 是内存数据库,内存使用量大,如果没有控制好内存的使用量,或者和其他内存需求大的应用一起运行了,就可能受到 swap 的影响,而导致性能变慢。
swap 触发后影响的是 Redis 主 IO 线程,这会极大地增加 Redis 的响应时间。
2)、什么时候出发 swap ?----触发 swap 的原因主要是物理机器内存不足,对于Redis,两种。
1)、Redis 实例自身使用了大量的内存,导致物理机器的可用内存不足;
2)、和 Redis 实例在同一台机器上运行的其他进程,在进行大量的文件读写操作。文件读写本身会占用系统内存,这会导致分配给 Redis 实例的内存量变少,进而触发 Redis 发生swap。
3)、swap 解决思路:增加机器的内存或者使用 Redis 集群(考虑主从切换一下,大内存变主库)。
4、操作系统:内存页大
除了内存 swap,还有一个和内存相关的因素,即内存大页机制(Transparent Huge Page, THP),也会影响 Redis 性能。
Linux 内核支持2KB大小内存页分配,常规4KB。
trade-off :
1)、内存大页可以给 Redis 带来内存分配方面的收益;
2)、持久化 Rdb 时修改数据,采用写时复制技术,保证正在修改的数据也被持久化。
一旦有数据要被修改,Redis 并不会直接修改内存中的数据,而是将这些数据拷贝一份,然后再进行修改。
如果采用大内存页,即使客户端请求只修改 100B 的数据,Redis 也需要拷贝 2MB 的大页。相反,如果是常规内存页机制,只用拷贝 4KB。
所以,当客户端请求修改或新写入数据较多时,内存大页机制将导致大量的拷贝,这就会影响Redis 正常的访存操作,最终导致性能变慢。
解决:关闭大内存页
#执行下面命令,返回always,说明启动了大内存页,如果是nvner说明关闭。
-cat /sys/kernel/mm/transparent_hugepage/enabled
always
在 Redis 实例部署之前,执行下面命令,关闭大内存页
-echo never /sys/kernel/mm/transparent_hugepage/enabled
5、小结
梳理了一个包含 9 个检查点的 Checklist,遇到 Redis性能变慢时,按照这些步骤逐一检查,高效地解决问题:
1)、 获取 Redis 实例在当前环境下的基线性能。
2)、是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。
3)、是否对过期 key 设置了相同的过期时间?对于批量删除的 key,可以在每个 key 的过期时间上加一个随机数,避免同时删除。
4)、是否存在 bigkey? 对于 bigkey 的删除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是 Redis 4.0 以前的版本,可以使用 SCAN 命令迭代删除;对于 bigkey 的集合查询和聚合操作,可以使用 SCAN 命令在客户端完成。
5)、Redis AOF 配置级别是什么?业务层面是否的确需要这一可靠性级别?如果我们需要高性能,同时也允许数据丢失,可以将配置项 no-appendfsync-on-rewrite 设置为 yes,避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。当然, 如果既需要高性能又需要高可靠性,最好使用高速固态盘作为 AOF 日志的写入盘。
6)、Redis 实例的内存使用是否过大?发生 swap 了吗?如果是的话,就增加机器内存,或者是使用 Redis 集群,分摊单机 Redis 的键值对数量和内存压力。同时,要避免出现 Redis 和其他内存需求大的应用共享机器的情况。
7)、在 Redis 实例的运行环境中,是否启用了透明大页机制?如果是的话,直接关闭内存大页机制就行了。
8)、是否运行了 Redis 主从集群?如果是的话,把主库实例的数据量大小控制在 2~4GB,以免主从复制时,从库因加载大的 RDB 文件而阻塞。
9)、是否使用了多核 CPU 或 NUMA 架构的机器运行 Redis 实例?使用多核 CPU 时,可以给 Redis 实例绑定物理核;使用 NUMA 架构时,注意把 Redis 实例和网络中断处理程序运行在同一个 CPU Socket 上(但是不同核)。
除此之外,检查 Redis 所在的机器上有没有一些其他占内存、磁盘 IO 和网络 IO 的程序,比如说数据库程序或者数据采集程序。如果有的话,我建议你将这些程序迁移到其他机器上运行。
保证要给 Redis 充足的计算、内存和 IO 资源。