title | category | tag | |
---|---|---|---|
Redis 常见阻塞原因总结 |
数据库 |
|
本文整理完善自:https://mp.weixin.qq.com/s/0Nqfq_eQrUb12QH6eBbHXA ,作者:阿Q说代码
使用 O(n)
命令可能会导致阻塞,例如keys *
、hgetall
、lrange
、smembers
、zrange
、sinter
、sunion
命令。这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大耗时也会越大从而导致客户端阻塞。
不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。有遍历的需求可以使用 hscan
、sscan
、zscan
代替。
Redis 提供了两个命令来生成 RDB 快照文件:
save
: 同步保存操作,会阻塞 Redis 主线程;bgsave
: fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
默认情况下,Redis 默认配置会使用 bgsave
命令。如果手动使用 save
命令生成 RDB 快照文件的话,就会阻塞主线程。
Redis AOF 持久化机制是在执行完命令之后再记录日志,这和关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复)不同。
为什么是在执行完命令之后记录日志呢?
- 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
- 在命令执行完之后再记录,不会阻塞当前的命令执行。
这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过):
- 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
- 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf
中,然后再根据 appendfsync
配置来决定何时将其同步到硬盘中的 AOF 文件。
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( fsync
策略),它们分别是:
appendfsync always #每次有数据修改发生时都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度
appendfsync everysec #每秒钟调用fsync函数同步一次AOF文件
appendfsync no #让操作系统决定何时进行同步,一般为30秒一次
当调用 fsync 函数同步 AOF 文件时,需要等待,直到写入完成。当磁盘压力太大的时候,主线程可能会被阻塞。
- fork 出一条子线程来将文件重写,在执行
BGREWRITEAOF
命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子线程创建新 AOF 文件期间,记录服务器执行的所有写命令。 - 当子线程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。
- 最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。
阻塞就是出现在第 2 步的过程中,将缓冲区中新数据写到新文件的过程中会产生阻塞。
相关阅读:Redis AOF重写阻塞问题分析。
如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。
大 key 造成的阻塞问题如下:
- 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
- 引发网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
- 阻塞工作线程:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
当我们在使用 Redis 自带的 --bigkeys
参数查找大 key 时,最好选择在从节点上执行该命令,因为主节点上执行时,会阻塞主节点。
-
我们还可以使用 SCAN 命令来查找大 key;
-
通过分析 RDB 文件来找出 big key,这种方案的前提是 Redis 采用的是 RDB 持久化。网上有现成的工具:
-
- redis-rdb-tools:Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具
- rdb_bigkeys:Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。
删除操作的本质是要释放键值对占用的内存空间。
释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序。
所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。
删除大 key 时建议采用分批次删除和异步删除的方式进行。
清空数据库和上面 bigkey 删除也是同样道理,flushdb
、flushall
也涉及到删除和释放所有的键值对,也是 Redis 的阻塞点。
Redis 集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,需要人工介入。
在扩缩容的时候,需要进行数据迁移。而 Redis 为了保证迁移的一致性,迁移所有操作都是同步操作。
执行迁移时,两端的 Redis 均会进入时长不等的阻塞状态,对于小 Key,该时间可以忽略不计,但如果一旦 Key 的内存使用过大,严重的时候会触发集群内的故障转移,造成不必要的切换。