Redis常见阻塞原因总结

本文整理完善自: https://mp.weixin.qq.com/s/0Nqfq_eQrUb12QH6eBbHXA ,作者:阿 Q 说代码

这篇文章会详细总结一下可能导致 Redis 阻塞的情况,这些情况也是影响 Redis 性能的关键因素,使用 Redis 的时候应该格外注意!

O(n) 命令 #

Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如:

  • KEYS *:会返回所有符合规则的 key。
  • HGETALL:会返回一个 Hash 中所有的键值对。
  • LRANGE:会返回 List 中指定范围内的元素。
  • SMEMBERS:返回 Set 中的所有元素。
  • SINTER/SUNION/SDIFF:计算多个 Set 的交集/并集/差集。
  • ……

由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长,从而导致客户端阻塞。不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 HSCANSSCANZSCAN 代替。

除了这些 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 记录日志不会对命令进行语法检查;
  • 在命令执行完之后再记录,不会阻塞当前的命令执行。

这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过):

  • 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
  • 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)

AOF 刷盘阻塞 #

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的 AOF 文件。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( fsync策略),它们分别是:

  1. appendfsync always:主线程调用 write 执行写操作后,后台线程( aof_fsync 线程)立即会调用 fsync 函数同步 AOF 文件(刷盘),fsync 完成后线程返回,这样会严重降低 Redis 的性能(write + fsync)。
  2. appendfsync everysec:主线程调用 write 执行写操作后立即返回,由后台线程( aof_fsync 线程)每秒钟调用 fsync 函数(系统调用)同步一次 AOF 文件(write+fsyncfsync间隔为 1 秒)
  3. appendfsync no:主线程调用 write 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write但不fsyncfsync 的时机由操作系统决定)。

当后台线程( aof_fsync 线程)调用 fsync 函数同步 AOF 文件时,需要等待,直到写入完成。当磁盘压力太大的时候,会导致 fsync 操作发生阻塞,主线程调用 write 函数时也会被阻塞。fsync 完成后,主线程执行 write 才能成功返回。

关于 AOF 工作流程的详细介绍可以查看: Redis 持久化机制详解,有助于理解 AOF 刷盘阻塞。

AOF 重写阻塞 #

  1. fork 出一条子线程来将文件重写,在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子线程创建新 AOF 文件期间,记录服务器执行的所有写命令。
  2. 当子线程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。
  3. 最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。

阻塞就是出现在第 2 步的过程中,将缓冲区中新数据写到新文件的过程中会产生阻塞

相关阅读: 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 自带的 --bigkeys 参数查找大 key 时,最好选择在从节点上执行该命令,因为主节点上执行时,会阻塞主节点。

  • 我们还可以使用 SCAN 命令来查找大 key;

  • 通过分析 RDB 文件来找出 big key,这种方案的前提是 Redis 采用的是 RDB 持久化。网上有现成的工具:

    • redis-rdb-tools:Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具
    • rdb_bigkeys:Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。

删除大 key #

删除操作的本质是要释放键值对占用的内存空间。

释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序。

所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。

删除大 key 时建议采用分批次删除和异步删除的方式进行。

清空数据库 #

清空数据库和上面 bigkey 删除也是同样道理,flushdbflushall 也涉及到删除和释放所有的键值对,也是 Redis 的阻塞点。

集群扩容 #

Redis 集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,需要人工介入。

在扩缩容的时候,需要进行数据迁移。而 Redis 为了保证迁移的一致性,迁移所有操作都是同步操作。

执行迁移时,两端的 Redis 均会进入时长不等的阻塞状态,对于小 Key,该时间可以忽略不计,但如果一旦 Key 的内存使用过大,严重的时候会触发集群内的故障转移,造成不必要的切换。

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

CPU 竞争 #

Redis 是典型的 CPU 密集型应用,不建议和其他多核 CPU 密集型服务部署在一起。当其他进程过度消耗 CPU 时,将严重影响 Redis 的吞吐量。

可以通过redis-cli --stat获取当前 Redis 使用情况。通过top命令获取进程对 CPU 的利用率等信息 通过info commandstats统计信息分析出命令不合理开销时间,查看是否是因为高算法复杂度或者过度的内存优化问题。

网络问题 #

连接拒绝、网络延迟,网卡软中断等网络问题也可能会导致 Redis 阻塞。

参考 #