1 简介
Redis重视影响Redis性能的因素,如:
- 命令操作
- 系统配置
- 关键机制
- 硬件配置 ...
要尽可能避免性能异常场景,还要做好异常应对方案。影响Redis性能的潜在风险:
- Redis内部的阻塞式操作
- CPU核和NUMA架构的影响
- Redis关键系统配置
- Redis内存碎片
- Redis缓冲区
本文研究Redis内部的阻塞式操作及应对方案。
Redis的网络I/O和KV对读写都由主线程完成。若在主线程执行操作耗时太长,就会引起主线程阻塞。但Redis既有服务客户端请求的键值对增删改查操作,也有保证可靠性的持久化操作,还有主从复制时的数据同步操作。哪些会引起阻塞?
2 Redis阻塞风险点
Redis要和不同对象交互,有不同操作:
- 客户端:网络IO,KV对CRUD操作,DB操作
- 磁盘:生成RDB快照,记录AOF日志,AOF日志重写
- 主从节点:主库生成、传输RDB文件,从库接收RDB文件、清空数据库、加载RDB文件
- 分片集群实例:向其他实例传输哈希槽信息,数据迁移
2.1 客户端交互
Redis使用I/O多路复用,避免主线程一直处在等待网络连接或请求到来的状态,所以,网络I/O并非导致Redis阻塞因素。
2.1.1 集合全量查询和聚合操作
KV对的crud操作是Redis和客户端主要交互,也是Redis主线程执行的主要任务。复杂度高crud操作势必阻塞Redis。
最基本标准:看操作复杂度是否O(N)。Redis涉及集合的操作复杂度通常O(N):
- 集合元素全量查询操作,如HGETALL、SMEMBERS
- 集合的聚合统计操作,如交、并差集
2.1.2 删除大key
集合自身的删除也可能阻塞。
Q:不就直接数据删除,咋阻塞主线程?
A:删除本质是释放KV对占用内存空间。
释放内存只是第一步,为高效管理内存,应用程序释放内存时,os要把释放掉的内存块插入一个空闲内存块的链表,以便后续管理和再分配。这过程耗时,且会阻塞当前释放内存的应用程序。 所以,若突然释放大量内存,空闲内存块链表操作时间就会增加,导致Redis主线程阻塞。
啥时释放大量内存?
最常见于删除含大量元素的集合,即删除bigkey。不同元素数量集执行删除耗时:
| 集合类型 | 10万(8字节) | 100万(8字节) | 10万(128字节) | 100万(128字节) |
| Hash | 50ms | 962ms | 91ms | 1980ms |
| List | 25ms | 133ms | 29ms | 283ms |
| Set | 42ms | 821ms | 75ms | 1347ms |
| Sorted Set | 53ms | 809ms | 61ms | 991ms |
- 当元素数量从10w到100w,集合类型删除时间增长幅度从5倍上升到近20倍
- 集合元素越大,删除耗时越长
- 当删除有100w个元素的集合时,最大删除时间绝对值已达1.98s(Hash类型)。Redis响应时间一般在微秒级,这不可避免严重阻塞主线程
2.1.3 清空数据库
Redis数据库级操作:清空数据库,如FLUSHDB、FLUSHALL也是重大阻塞风险,涉及删除、释放所有KV对。
2.2 磁盘交互阻塞
2.2.1 AOF日志同步写
磁盘I/O费时费力,Redis开发者早就设计为:
- 子进程生成RDB
- AOF日志重写
都由子进程负责执行,慢速的磁盘I/O就不阻塞主线程。
但Redis直接记录AOF日志时,会根据不同写回策略对数据做落盘保存。 一个同步写盘操作耗时大约1~2ms,若大量写操作需记录在AOF日志,并同步写回,就会阻塞主线程。
2.3 主从节点交互阻塞
2.3.1 从库加载RDB文件
主从集群中的主库需:
- 生成RDB文件
- 并传输给从库
主库在复制过程,创建、传输RDB都由子进程完成,不阻塞主线程。
但从库,接收RDB文件后,需用FLUSHDB命令清空当前数据库,恰好撞车三大阻塞点。从库清空当前数据库后,还要把RDB文件载入内存,RDB文件越大,加载越慢。
2.3.2 分片集群实例交互阻塞
- 部署Redis Cluster时,每个Redis实例上分配的哈希槽信息,需在不同实例间传递 不过,哈希槽信息量不大
- 当需负载均衡或有实例数变化时,数据会在不同实例间迁移 而数据迁移是渐进式执行
所以,一般这两类操作对Redis主线程阻塞影响不大。
但若使用Redis Cluster,且同时正好迁移大key,就会阻塞主线程,因Redis Cluster使用的同步迁移。 当无大key时,分片集群的各实例在进行交互时一般不会阻塞主线程。
在主线程中执行以上操作,势必导致主线程长时间无法服务其它请求。 为避免阻塞式操作,Redis提供异步线程机制:Redis会启动一些子线程,把一些任务移交子线程,让它们在后台处理。使用异步线程机制执行操作,可以避免阻塞主线程。
以上这些阻塞式操作可以被异步执行吗?
3 可异步执行的阻塞点
在分析阻塞式操作的异步执行的可行性前,先了解异步执行对操作的要求。
若一个操作能被异步执行,说明它不是Redis主线程关键路径上的操作。
3.1 关键路径操作
客户端把请求发给Redis后,等Redis返回数据结果:
- 主线程接收到操作1后,由于操作1无需给客户端返回具体数据,所以,主线程可将其移交给后台子线程处理,同时只需给客户端返回“OK”。 操作1就不属关键路径操作,因其不用给客户端返回具体数据,所以可由后台子线程异步执行
- 子线程执行操作1时,客户端又向Redis实例发送操作2,而此时,客户端需使用操作2返回的具体数据结果。若操作2不返回结果,则客户端将一直处等待状态。 该操作需把结果返给客户端,所以是关键路径操作,主线程须立即执行完该操作。
那Redis的写操作(如SET,HSET,SADD)属于关键路径吗?这需要客户端根据业务需要区分:
- 若客户端依赖操作返回值的不同而处理不同业务逻辑,则HSET、SADD算关键路径,而SET操作不算关键路径
因为HSET和SADD操作,若field或member不存在,Redis返回1,否则返0。而SET操作返回的结果都是OK - 若客户端不关心返回值,只关心数据是否写成功,则SET/HSET/SADD都不算关键路径,多次执行这些命令都是幂等的,这时可放到异步线程
- 若Redis设置maxmemory,但未设置淘汰策略,这三个操作也都算关键路径
因为若Redis内存超过maxmemory,再写入数据时,Redis返回的结果是OOM error,这种情况下,客户端需要感知有错误发生才行
3.2 各阻塞点分析
3.2.1 集合全量查询和聚合操作
Redis读都是关键路径操作,因为客户端发起读请求后,就会等待返回读取数据,再处理后续。所以,涉及读操作,无法异步!
推荐使用SCAN命令,分批读取数据,再在客户端进行聚合计算。
3.2.2 删除操作
无需给客户端返具体数据,不算关键路径操作。
“大K删除”、“清空数据库”同理,都可用后台子线程异步执行。
3.2.3 AOF日志同步写
为保证数据可靠性,Redis实例需保证AOF日志中的操作记录已落盘,这操作虽需实例等待,但不会返回具体数据结果给实例。所以,可使用一个子线程执行。
3.2.4 从库加载RDB文件
从库想对客户端提供数据存取服务,须将RDB文件加载完成。所以,这也属于关键路径操作,须让从库的主线程执行。把主库数据量大小控制在2~4GB左右,以保证RDB文件能以较快的速度加载。
综上,可使用Redis异步子线程机制实现大K删除,清空数据库及AOF日志同步写。
本文已收录在Github,关注我,紧跟本系列专栏文章,咱们下篇再续!
- 🚀 魔都架构师 | 全网30W技术追随者
- 🔧 大厂分布式系统/数据中台实战专家
- 🏆 主导交易系统百万级流量调优 & 车联网平台架构
- 🧠 AIGC应用开发先行者 | 区块链落地实践者
- 🌍 以技术驱动创新,我们的征途是改变世界!
- 👉 实战干货:编程严选网