16 阻塞式操作

影响 Redis 性能的 5 大方面的潜在因素:

  • Redis 内部的阻塞式操作;
  • CPU 核和 NUMA 架构的影响;
  • Redis 关键系统配置;
  • Redis 内存碎片;
  • Redis 缓冲区。

实例阻塞点

  • 客户端:网络IO,键值对增删改查,数据库操作;
  • 磁盘:生成RDB快照,记录AOF日志,AOF日志重写;
  • 主从节点:主库生成、传输RDB文件,从库接收RDB文件、清空数据库、加载RDB文件;
  • 切片集群实例:向其他实例传输哈希槽信息,数据迁移

客户端阻塞点

  • 网络IO:Redis采用IO多路复用机制,网络IO不是阻塞点
  • 键值对操作:复杂度高O(N)的增删改查操作肯定会阻塞redis。

    • 集合全量查询和聚合操作
    • 集合自身删除操作(释放内存后系统会插入空闲内存链表用于管理,内存过大操作时间会增加)
  • 清空数据库:删除和释放所有键值对

磁盘交互阻塞点

AOF日志回写:
Redis 直接记录 AOF 日志时,会根据不同的写回策略对数据做落盘保存。一个同步写磁盘的操作的耗时大约是 1~2ms,如果有大量的写操作需要记录在 AOF 日志中,并同步写回的话,就会阻塞主线程了。

主从节点阻塞点

在主从集群中,主库需要生成 RDB 文件,并传输给从库。主库在复制的过程中,创建和传输 RDB 文件都是由子进程来完成的,不会阻塞主线程。

  1. 对于从库来说,它在接收了 RDB 文件后,需要使用 FLUSHDB 命令清空当前数据库,造成阻塞
  2. 从库在清空当前数据库后,还需要把 RDB 文件加载到内存,这个过程的快慢和 RDB 文件的大小密切相关,RDB 文件越大,加载过程越慢,造成阻塞

集群交互阻塞

如果你使用了 Redis Cluster 方案,而且同时正好迁移的是 bigkey 的话,就会造成主线程的阻塞,因为 Redis Cluster 使用了同步迁移。

异步执行

Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由它们负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行。

主线程通过一个链表形式的任务队列和子线程进行交互。

写入 AOF 日志

当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到任务队列中。后台子线程读取任务后,开始自行写入 AOF 日志,这样主线程就不用一直等待 AOF 日志写完了。

惰性删除lazy free

当收到键值对删除和清空数据库的操作时,主线程会把这个操作封装成一个任务,放入到任务队列中,然后给客户端返回一个完成信息,表明删除已经完成。

但实际上,这个时候删除还没有执行,等到后台子线程从任务队列中读取任务后,才开始实际删除键值对,并释放相应的内存空间。

此时,删除或清空操作不会阻塞主线程,这就避免了对主线程的性能影响。

小结

会导致 Redis 性能受损的 5 大阻塞点,包括集合全量查询和聚合操作、bigkey 删除、清空数据库、AOF 日志同步写,以及从库加载 RDB 文件。

关键路径操作:不能被异步执行的操作。

  • 读操作是典型的关键路径操作,包括集合全量查询和聚合操作
  • 从库加载RDB操作
  • 写操作是否在关键路径,需要看使用方是否需要确认写入已经完成

集合全量查询和聚合操作、从库加载 RDB 文件是在关键路径上,无法使用异步操作来完成。对于这两个阻塞点,我也给你两个小建议。

  • 集合全量查询和聚合操作:可以使用 SCAN 命令,分批读取数据,再在客户端进行聚合计算;
  • 从库加载 RDB 文件:把主库的数据量大小控制在 2~4GB 左右,以保证 RDB 文件能以较快的速度加载。

17 CPU结构

一个 CPU 处理器中一般有多个运行核心,我们把一个运行核心称为一个物理核,每个物理核都可以运行应用程序。

主流架构

每个物理核都拥有私有的一级缓存(Level 1 cache,简称 L1 cache),包括一级指令缓存和一级数据缓存,以及私有的二级缓存(Level 2 cache,简称 L2 cache)。
L1 和 L2 缓存的大小只有KB级别。

不同的物理核还会共享一个共同的三级缓存(Level 3 cache,简称为 L3 cache)。
L3有几 MB 到几十 MB。

另外,现在主流的 CPU 处理器中,每个物理核通常都会运行两个超线程,也叫作逻辑核。同一个物理核的逻辑核会共享使用 L1、L2 缓存。

服务器上通常有多个 CPU 处理器(CPU Socket),每个处理器有自己的物理核(包括 L1、L2 缓存),L3 缓存,以及连接的内存,同时,不同处理器间通过总线连接。

远端内存访问:
应用在一个Socket上运行并把数据存入内存,当被调度到另一个 Socket 上运行再进行内存访问时,就需要访问之前 Socket 上连接的内存,称为远端内存访问。
远端内存访问会增加应用程序的延迟。

在多 CPU 架构下,一个应用程序访问所在 Socket 的本地内存和访问远端内存的延迟并不一致,所以,我们也把这个架构称为非统一内存访问架构(Non-Uniform Memory Access,NUMA 架构)。

多核影响

在 CPU 多核的环境下,通过绑定 Redis 实例和 CPU 核,可以有效降低 Redis 的尾延迟。

在一个 CPU 核上运行时,应用程序需要记录自身使用的软硬件资源信息(例如栈指针、CPU 核的寄存器值等),我们把这些信息称为运行时信息。

同时,应用程序访问最频繁的指令和数据还会被缓存到 L1、L2 缓存上,以便提升执行速度。

上下文切换 context switch:
在 CPU 多核的环境中,一个线程先在一个 CPU 核上运行,之后又切换到另一个 CPU 核上运行,这时就会发生 context switch。

Redis 主线程的运行时信息需要被重新加载到另一个 CPU 核上,而且,此时,另一个 CPU 核上的 L1、L2 缓存中,并没有 Redis 实例之前运行时频繁访问的指令和数据,所以,这些指令和数据都需要重新从 L3 缓存,甚至是内存中加载。

我们可以使用 taskset 命令把一个程序绑定在一个核上运行。

taskset -c 0 ./redis-server

把 Redis 实例绑在了 0 号核上,其中,“-c”选项用于设置要绑定的核编号。

我们最好把网络中断程序和 Redis 实例绑在同一个 CPU Socket 上。

不过,需要注意的是,在 CPU 的 NUMA 架构下,对 CPU 核的编号规则,并不是先把一个 CPU Socket 中的所有逻辑核编完,再对下一个 CPU Socket 中的逻辑核编码,而是先给每个 CPU Socket 中每个物理核的第一个逻辑核依次编号,再给每个 CPU Socket 中的物理核的第二个逻辑核依次编号。

lscpu

Architecture: x86_64
...
NUMA node0 CPU(s): 0-5,12-17
NUMA node1 CPU(s): 6-11,18-23
...

绑核风险:
当我们把 Redis 实例绑到一个 CPU 逻辑核上时,就会导致子进程、后台线程和 Redis 主线程竞争 CPU 资源,一旦子进程或后台线程占用 CPU 时,主线程就会被阻塞,导致 Redis 请求延迟增加

解决方案:

  1. 一个 Redis 实例对应绑一个物理核(把一个物理核的 2 个逻辑核都用上,缓解CPU竞争)
taskset -c 0,12 ./redis-server
  1. 优化Redis源码,把子进程和后台线程绑到不同CPU上

18 响应延迟

基线性能:一个系统在低压力、无干扰下的基本性能,这个性能只由当前的软硬件配置决定。

Redis基线性能测试:

./redis-cli --intrinsic-latency 120

打印 120 秒内监测到的最大延迟
注意:为了避免网络对基线性能的影响,刚刚说的这个命令需要在服务器端直接运行。

结论:如果 Redis 运行时延迟是其基线性能的 2 倍及以上,就可以认定 Redis 变慢了。

慢查询命令

慢查询命令,就是指在 Redis 中执行速度慢的命令,这会导致 Redis 延迟增加,和命令操作的复杂度有关。

排查方法:

  1. Redis 日志
  2. latency monitor 工具

处理方法:

  1. 用其他高效命令代替。比如说,如果你需要返回一个 SET 中的所有成员时,不要使用 SMEMBERS 命令,而是要使用 SSCAN 多次迭代返回,避免一次返回大量数据,造成线程阻塞。
  2. 当你需要执行排序、交集、并集操作时,可以在客户端完成,而不要用 SORT、SUNION、SINTER 这些命令,以免拖慢 Redis 实例。
  3. 注意生成环境不要用KEYS命令,它会遍历存储的键值对,延迟高。

过期KEY操作

过期 key 的自动删除机制,是回收内存空间的常用机制,会引起 Redis 操作阻塞,导致性能变慢。

Redis默认每100毫秒删除一些过期key:

  1. 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数(默认20)的 key,并将其中过期的 key 全部删除;
  2. 如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。
    (触发该条件后会一直执行删除操作,导致Redis变慢)

算法2触发方式:
频繁使用带有相同时间参数的 EXPIREAT 命令设置过期 key,导致在同一时间大量key过期。

解决方案:加随机数

小结

排查和解决 Redis 变慢问题的方法:

  1. 从慢查询命令开始排查,并且根据业务需求替换慢查询命令;
  2. 排查过期 key 的时间设置,并根据实际使用需求,设置不同的过期时间。

问题:有哪些其他命令可以代替 KEYS 命令,实现同样的功能呢?
如果想要获取整个实例的所有key,建议使用SCAN命令代替。客户端通过执行SCAN $cursor COUNT $count可以得到一批key以及下一个游标$cursor,然后把这个$cursor当作SCAN的参数,再次执行,以此往复,直到返回的$cursor为0时,就把整个实例中的所有key遍历出来了。

但是SCAN可能会得到重复的key(Rehash时,旧表已遍历过的key会映射到新表没有遍历过的位置)。

19 文件系统和操作系统

Redis 会持久化保存数据到磁盘,这个过程要依赖文件系统来完成,所以,文件系统将数据写回磁盘的机制,会直接影响到 Redis 持久化的效率。在持久化的过程中,Redis 也还在接收其他请求,持久化的效率高低又会影响到 Redis 处理请求的性能。

另一方面,Redis 是内存数据库,内存操作非常频繁,所以,操作系统的内存机制会直接影响到 Redis 的处理效率。比如Redis 的内存不够用了,操作系统会启动 swap 机制,这就会直接拖慢 Redis。

文件系统:AOF模式

AOF 日志提供了三种日志写回策略:no、everysec、always。

写回策略依赖文件系统的两个系统调用完成,也就是 write 和 fsync:

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

no:调用write写日志文件,由操作系统周期性的将日志写回磁盘
everysec:允许丢失一秒的操作记录,使用后台子线程完成fysnc操作
always:不使用后台子线程执行

另外,AOF 重写生成体量缩小的新的 AOF 日志文件,需要的时间很长,也容易阻塞 Redis 主线程,所以,Redis 使用子进程来进行 AOF 重写。

风险点:

  • fsync需要等到数据写到磁盘才能返回,AOF重写会进行大量IO,可能阻塞fsync。
  • 主线程会监控fsync进度,如果发现上一次还没执行完,主线程也会阻塞,导致Redis性能下降。

配置:
如果业务应用对延迟非常敏感,但同时允许一定量的数据丢失,那么,可以把配置项 no-appendfsync-on-rewrite 设置为 yes

no-appendfsync-on-rewrite yes

建议:使用高速固态硬盘

操作系统:内存swap

内存 swap 是操作系统里将内存数据在内存和磁盘间来回换入和换出的机制。
swap 触发后影响的是 Redis 主 IO 线程,这会极大地增加 Redis 的响应时间。

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

解决思路:增加内存或使用集群。

查看swap情况:

# 1.查看进程ID
redis-cli info | grep process_id
process_id: 5332

# 2.进入进程目录
cd /proc/5332

# 3.查看进程使用情况
cat smaps | egrep '^(Swap|Size)'

注意:当出现百 MB,甚至 GB 级别的 swap 大小时,就表明,此时,Redis 实例的内存压力很大,很有可能会变慢。

操作系统:内存大页

内存大页机制(Transparent Huge Page, THP),也会影响 Redis 性能,该机制支持2MB大小的内存分配,常规内存分配是4KB。

缺点:RDB使用写时复制机制,有数据要被修改时,会先拷贝一份再修改。当修改或新写数据较多时,内存大页将导致大量拷贝,影响Redis性能。

检查内存大页:

cat /sys/kernel/mm/transparent_hugepage/enabled

启动:always 禁止:never

生产环境建议:关闭大页机制

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

小结

检查Redis性能9点:

  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 上。

20 内存分配

问题:做了数据删除,使用 top 命令查看时,为什么 Redis 还是占用了很多内存呢?
原因:数据删除后,Redis 释放的内存空间会由内存分配器管理,并不会立即返回给操作系统。所以,操作系统仍然会记录着给 Redis 分配了大量内存。

内存碎片

内因:内存分配策略
外因:键值对大小不一样;删改操作;

内存分配策略

Redis 可以使用 libc、jemalloc、tcmalloc 多种内存分配器来分配内存,默认使用 jemalloc。

jemalloc 的分配策略之一,是按照一系列固定的大小划分内存空间。当程序申请的内存最接近某个固定值时,jemalloc 会给它分配相应大小的空间。

如果 Redis 每次向分配器申请的内存空间大小不一样,这种分配方式就会有形成碎片的风险。

键值对大小

内存分配器只能按固定大小分配内存,所以,分配的内存空间一般都会比申请的空间大一些,不会完全一致,这本身就会造成一定的碎片,降低内存空间存储效率。

修改删除操作

键值对被修改和删除,会导致空间的扩容和释放。

  • 如果修改后的键值对变大或变小了,就需要占用额外的空间或者释放不用的空间
  • 删除的键值对不再需要内存空间了,会把空间释放出来,形成空闲空间

判断内存碎片大小

使用INFO命令:

INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
…
mem_fragmentation_ratio:1.86

mem_fragmentation_ratio 的指标,它表示的就是 Redis 当前的内存碎片率。

mem_fragmentation_ratio = used_memory_rss / used_memory
used_memory_rss 是操作系统实际分配给 Redis 的物理内存空间,里面就包含了碎片;
used_memory 是 Redis 为了保存数据实际申请使用的空间。

经验:

  1. mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了 50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了。
  2. 小于1,说明没有足够的物理内存,发生swap了。

清理内存碎片

从 4.0-RC3 版本以后,Redis 自身提供了一种内存碎片自动清理的方法。

代价:
碎片清理是有代价的,操作系统需要把多份数据拷贝到新位置,把原有空间释放出来,这会带来时间开销。因为 Redis 是单线程,在数据拷贝时,Redis 只能等着,这就导致 Redis 无法及时处理请求,性能就会降低。

解决方案:
可以通过设置参数,来控制碎片清理的开始和结束时机,以及占用的 CPU 比例,从而减少碎片清理对 Redis 本身请求处理的性能影响。

Redis 需要启用自动内存碎片清理,可以把 activedefrag 配置项设置为 yes:

config set activedefrag yes

配置自动清理条件(同时满足):

  • active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理;
  • active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理。

配置CPU占比:

  • active-defrag-cycle-min 25: 表示自动清理过程所用 CPU 时间的比例不低于 25%,保证清理能正常开展;
  • active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于 75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高。

小结

info memory:查看碎片率的情况;
碎片率阈值:判断是否要进行碎片清理了;
内存碎片自动清理:提高内存实际利用率。


IT小马
1.2k 声望166 粉丝

Php - Go - Vue - 云原生