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 文件都是由子进程来完成的,不会阻塞主线程。
- 对于从库来说,它在接收了 RDB 文件后,需要使用 FLUSHDB 命令清空当前数据库,造成阻塞
- 从库在清空当前数据库后,还需要把 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 请求延迟增加
解决方案:
- 一个 Redis 实例对应绑一个物理核(把一个物理核的 2 个逻辑核都用上,缓解CPU竞争)
taskset -c 0,12 ./redis-server
- 优化Redis源码,把子进程和后台线程绑到不同CPU上
18 响应延迟
基线性能:一个系统在低压力、无干扰下的基本性能,这个性能只由当前的软硬件配置决定。
Redis基线性能测试:
./redis-cli --intrinsic-latency 120
打印 120 秒内监测到的最大延迟
注意:为了避免网络对基线性能的影响,刚刚说的这个命令需要在服务器端直接运行。
结论:如果 Redis 运行时延迟是其基线性能的 2 倍及以上,就可以认定 Redis 变慢了。
慢查询命令
慢查询命令,就是指在 Redis 中执行速度慢的命令,这会导致 Redis 延迟增加,和命令操作的复杂度有关。
排查方法:
- Redis 日志
- latency monitor 工具
处理方法:
- 用其他高效命令代替。比如说,如果你需要返回一个 SET 中的所有成员时,不要使用 SMEMBERS 命令,而是要使用 SSCAN 多次迭代返回,避免一次返回大量数据,造成线程阻塞。
- 当你需要执行排序、交集、并集操作时,可以在客户端完成,而不要用 SORT、SUNION、SINTER 这些命令,以免拖慢 Redis 实例。
- 注意生成环境不要用KEYS命令,它会遍历存储的键值对,延迟高。
过期KEY操作
过期 key 的自动删除机制,是回收内存空间的常用机制,会引起 Redis 操作阻塞,导致性能变慢。
Redis默认每100毫秒删除一些过期key:
- 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数(默认20)的 key,并将其中过期的 key 全部删除;
- 如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。
(触发该条件后会一直执行删除操作,导致Redis变慢)
算法2触发方式:
频繁使用带有相同时间参数的 EXPIREAT 命令设置过期 key,导致在同一时间大量key过期。
解决方案:加随机数
小结
排查和解决 Redis 变慢问题的方法:
- 从慢查询命令开始排查,并且根据业务需求替换慢查询命令;
- 排查过期 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点:
- 获取 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 上。
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 为了保存数据实际申请使用的空间。
经验:
- mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了 50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了。
- 小于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:查看碎片率的情况;
碎片率阈值:判断是否要进行碎片清理了;
内存碎片自动清理:提高内存实际利用率。