Redis 是否真的变慢
Redis 真的变慢了吗?需要知道当前 Redis 的响应延迟 + 当前机器配置下的基线性能。
当前机器配置下的基线性能可以在启动 redis client 时配置参数,./redis-cli --intrinsic-latency 120。该命令会打印 120s 内检测到的最大延迟,可以以该最大延迟作为基线性能。
当然也有可能是网络导致基线性能受到影响,此时可以通过 iPerf 等工具来测试网络延迟。
-1.webp)
Redis 自身特性问题
- 慢查询命令,如交并集、排序等耗时操作;
- 过期 key 的删除,不过 4.0 开始异步删除进行缓解。前者将耗时操作放在客户端或者更换更高效的命令,后者需要查看是否设置大量 key 在同一时间过期。
O(n) 命令
Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如:
KEYS *:会返回所有符合规则的 key。HGETALL:会返回一个 Hash 中所有的键值对。LRANGE:会返回 List 中指定范围内的元素。SMEMBERS:返回 Set 中的所有元素。SINTER/SUNION/SDIFF:计算多个 Set 的交集/并集/差集。- ……
由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长,从而导致客户端阻塞。不过,这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 HSCAN、SSCAN、ZSCAN 代替。
除了这些 O(n)时间复杂度的命令可能会导致阻塞之外,还有一些时间复杂度可能在 O(N) 以上的命令,例如:
ZRANGE/ZREVRANGE:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。- ……
SAVE 创建 RDB 快照
Redis 提供了两个命令来生成 RDB 快照文件:
save: 同步保存操作,会阻塞 Redis 主线程;bgsave: fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
默认情况下,Redis 默认配置会使用 bgsave 命令。如果手动使用 save 命令生成 RDB 快照文件的话,就会阻塞主线程。
AOF
AOF 日志记录阻塞
Redis AOF 持久化机制是在执行完命令之后再记录日志,这和关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复)不同。

为什么是在执行完命令之后记录日志呢?
- 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
- 在命令执行完之后再记录,不会阻塞当前的命令执行。
这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过):
- 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
- 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。
AOF 刷盘阻塞
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的 AOF 文件。
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式(fsync 策略),它们分别是:
appendfsync always:主线程调用write执行写操作后,后台线程(aof_fsync线程)立即会调用fsync函数同步 AOF 文件(刷盘),fsync完成后线程返回,这样会严重降低 Redis 的性能(write+fsync)。appendfsync everysec:主线程调用write执行写操作后立即返回,由后台线程(aof_fsync线程)每秒钟调用fsync函数(系统调用)同步一次 AOF 文件(write+fsync,fsync间隔为 1 秒)appendfsync no:主线程调用write执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write但不fsync,fsync的时机由操作系统决定)。
当后台线程(aof_fsync 线程)调用 fsync 函数同步 AOF 文件时,需要等待,直到写入完成。当磁盘压力太大的时候,会导致 fsync 操作发生阻塞,主线程调用 write 函数时也会被阻塞。fsync 完成后,主线程执行 write 才能成功返回。
关于 AOF 工作流程的详细介绍可以查看:Redis 持久化机制详解,有助于理解 AOF 刷盘阻塞。
AOF 重写阻塞
- fork 出一条子线程来将文件重写,在执行
BGREWRITEAOF命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子线程创建新 AOF 文件期间,记录服务器执行的所有写命令。 - 当子线程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。
- 最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。
阻塞就是出现在第 2 步的过程中,将缓冲区中新数据写到新文件的过程中会产生阻塞。
AOF 重写:只能子进程,否则阻塞主线程。但是重写时会进行大量 IO 操作,同时 fsync 需要等到数据写到磁盘后才能返回,所以当 IO 压力大,fsync 会被阻塞。主线程会监控 fsync 执行,当 fsync 还没写完,而新接受记录要写回磁盘时,就会阻塞。
一般来说不需要非常可靠的数据,不一定需要 everysec 或者 always。如果业务应用对于延迟很敏感,但是允许一定量数据丢失,可以把配置项 no-appendfsync-on-rewrite 设置为 yes,表示 AOF 重写时,不进行 fsync 操作。即 Redis 实例把写命令写到内存后,不调用后台线程进行 fsync 操作,就可以直接返回了,但是可能宕机导致数据丢失。
为了高性能,推荐选择高速的固态硬盘作为 AOF 日志的写入设备。
相关阅读:Redis AOF 重写阻塞问题分析。
大 Key
如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:
- string 类型的 value 超过 1MB
- 复合类型(列表、哈希、集合、有序集合等)的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。
大 key 造成的阻塞问题如下:
- 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
- 引发网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
- 阻塞工作线程:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
删除大 key
删除操作的本质是要释放键值对占用的内存空间。
释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序。
所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。
删除大 key 时建议采用分批次删除和异步删除的方式进行。
清空数据库
清空数据库和上面 bigkey 删除也是同样道理,flushdb、flushall 也涉及到删除和释放所有的键值对,也是 Redis 的阻塞点。
集群扩容
Redis 集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,需要人工介入。
在扩缩容的时候,需要进行数据迁移。而 Redis 为了保证迁移的一致性,迁移所有操作都是同步操作。
执行迁移时,两端的 Redis 均会进入时长不等的阻塞状态,对于小 Key,该时间可以忽略不计,但如果一旦 Key 的内存使用过大,严重的时候会触发集群内的故障转移,造成不必要的切换。
操作系统:Swap
如果 Redis 实例用了很多内存,或机器上存在其他应用导致 Redis 内存不够用,那么可能会导致将一部分内存 swap 到磁盘上,频繁换入换出导致延迟。
什么是 Swap? Swap 直译过来是交换的意思,Linux 中的 Swap 常被称为内存交换或者交换分区。类似于 Windows 中的虚拟内存,就是当内存不足的时候,把一部分硬盘空间虚拟成内存使用,从而解决内存容量不足的情况。因此,Swap 分区的作用就是牺牲硬盘,增加内存,解决 VPS 内存不够用或者爆满的问题。
Swap 对于 Redis 来说是非常致命的,Redis 保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把 Redis 使用的部分内存换出硬盘,由于内存与硬盘的读写速度差几个数量级,会导致发生交换后的 Redis 性能急剧下降。
识别 Redis 发生 Swap 的检查方法如下:
1、查询 Redis 进程号
redis-cli -p 6383 info server | grep process_id
process_id: 4476
2、根据进程号查询内存交换信息
cat /proc/4476/smaps | grep Swap
Swap: 0kB
Swap: 0kB
Swap: 4kB
Swap: 0kB
Swap: 0kB
.....
如果交换量都是 0KB 或者个别的是 4KB,则正常。
预防内存交换的方法:
- 保证机器充足的可用内存
- 确保所有 Redis 实例设置最大可用内存(maxmemory),防止极端情况 Redis 内存不可控的增长
- 降低系统使用 swap 优先级,如
echo 10 > /proc/sys/vm/swappiness
操作系统:内存大页
虽然内存大页可以在分配相同内存量时减少分配次数,但是持久化时采用 copy on write 技术,会拷贝一整个内存页的内容,就算上面只有一点点数据。生产中不建议使用内存大页。
除了内存 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/enabledCPU 竞争
Redis 是典型的 CPU 密集型应用,不建议和其他多核 CPU 密集型服务部署在一起。当其他进程过度消耗 CPU 时,将严重影响 Redis 的吞吐量。
可以通过 redis-cli --stat 获取当前 Redis 使用情况。通过 top 命令获取进程对 CPU 的利用率等信息通过 info commandstats 统计信息分析出命令不合理开销时间,查看是否是因为高算法复杂度或者过度的内存优化问题。
网络问题
连接拒绝、网络延迟,网卡软中断等网络问题也可能会导致 Redis 阻塞。
其他问题
- 操作 bigkey,大量 key 集中过期
- 客户端使用短连接
- 数据量大时的 AOF 和 RDB 的 fork 耗时。
参考
- Redis 阻塞的 6 大类场景分析与总结:https://mp.weixin.qq.com/s/eaZCEtTjTuEmXfUubVHjew
- Redis 开发与运维笔记-Redis 的噩梦-阻塞:https://mp.weixin.qq.com/s/TDbpz9oLH6ifVv6ewqgSgA