半夜两点多某线上项目X的Redis出现了一波诡异的延迟尖刺P99从平时的1毫秒左右直接飙到800毫秒以上持续两三秒后又恢复如常。业务方截图过来的时候我还不太慌结果打开监控一看CPU不高、内存没满、带宽也不紧张慢查询日志里干干净净数据库连接数甚至没有明显变化。这种“什么指标都正常但就是卡”的情况基本可以断定是Redis阻塞问题。如果你也遇到过类似的场景你应该能懂那种感觉性能瓶颈排查了半天最后发现根本不是网络问题也不是硬件问题而是Redis内部或系统层面的某个瞬间“堵住了”。在单线程模型下任何一点阻塞都会被放大成全局事故而阻塞的来源又五花八门——命令、持久化、内存、操作系统、主从复制、慢客户端每一样都能给你来上一刀。这篇文章我就把Redis阻塞的几大类原因和整套排查链路掰开揉碎讲清楚适合正在处理线上延迟问题的运维和开发者参考。1. 先搞清楚Redis阻塞的“堵车逻辑”单线程模型为什么让任何慢操作都被放大1.1 Redis事件循环与命令处理的串行本质Redis核心的工作模式是单线程事件循环。主线程在aeMain循环里依次处理连接应答、命令读取、命令执行、命令回复以及时间事件过期清理、AOF刷盘、主从心跳这些后台任务。这意味着同一时刻只能干一件事当前命令没处理完后续所有命令只能排队等待。我习惯用一个“单车道收费站”的类比来解释这个问题。Redis就像一条单车道每个客户端请求都是一辆车。正常情况下一辆车几微秒就过去了但如果前面有一辆超长卡车在慢悠悠地翻仓库比如执行KEYS *或者收费站工作人员被叫去搬砖比如主进程在fork生成RDB快照那么后面所有的车都被堵在路上一动不动。车道上没有任何超车道也没有红绿灯只有一根车道在运转。阻塞的本质就是主线程在某一个瞬间被某件事占用而这个占用时间明显超出了正常范围。占用期间新到达的连接可能要排队已连接的请求得不到响应于是客户端那侧看到的就是延迟飙升、超时、甚至连接被重置。搞清楚这个底层逻辑后面所有阻塞原因都能归到同一条主线上什么情况下主线程会被莫名占用1.2 阻塞的全景地图从命令、持久化到系统层Redis官方文档其实把延迟和阻塞问题拆得很细但网上资料比较零散我这里把常见阻塞源整理成一张表方便你后面对照排查。阻塞类别常见源头典型表现首选排查工具命令执行慢命令、大Key操作、集中过期延迟尖刺、慢日志有记录SLOWLOG GET、--bigkeys持久化fork生成RDB、AOF重写、AOF刷盘全实例停顿、fork耗时日志INFO persistence内存与系统swap、内存淘汰、碎片整理、THPCPU不高但延迟高vmstat、INFO memory主从与集群全量复制、主从切换、槽位迁移延迟高峰与同步动作同步出现INFO replication网络与客户端慢客户端、输出缓冲堆积、连接风暴连接异常、缓冲区告警client list、tcpdump时间事件过期键扫描、定时任务堆积周期性延迟尖刺LATENCY LATEST很多新手排查阻塞只会看慢查询日志这是一个很大的误区。慢查询日志只能捕获到“命令执行本身耗时长”的那类阻塞而像fork阻塞、swap阻塞、网络缓冲区堆积这类问题慢日志里往往一条记录都没有。这也是为什么上面那种“指标都正常”的谜之卡顿经常把很多人卡在原地。2. 命令执行引发的阻塞大Key、慢命令与集中过期2.1 慢查询日志与耗时命令的定位命令执行阻塞是最容易被理解的一类也是最常见的一类。Redis的慢查询日志SLOWLOG就是为这类问题准备的它能记录执行耗时超过阈值的命令但前提是你提前配置了恰当的阈值。你可以先看下当前配置redis-cli CONFIG GET slowlog-log-slower-than这个值的单位是微秒默认通常是10000也就是10毫秒。如果你线上业务对延迟很敏感建议把这个阈值调低到5000甚至2000微秒否则很多“小毛刺”可能根本不会被记录下来。然后使用redis-cli SLOWLOG GET 100去拉取最近的100条慢命令重点看command字段、execution_time字段和执行时间戳。我见过多次线上问题最终定位结果都是一条命令直接干了几秒比如在某实例上执行HGETALL一个成员数过万的Hash、执行SMEMBERS一个几十万成员的Set、或者对一个大列表做LRANGE 0 -1。这些命令的复杂度是O(N)当N足够大时单线程就必然被卡住。另外一个容易被忽略的就是KEYS命令。很多人知道它慢但不知道它到底有多慢。在一个键数量达到几百万的实例上一个KEYS * 足以让整个实例不可用好几秒期间所有业务请求全部排队。虽然很多公司安全规范里早就禁止了KEYS但代码里借用KEYS做模糊匹配的情况非常普遍尤其是老项目里这种代码就像埋着的雷。2.2 大Key为什么是阻塞的头号杀手慢命令往往和大Key是绑定在一起的。判断一个Key算不算大Key业界没有统一标准但根据我的实战经验可以参考这几个阈值String类型单个value超过10KB就算需要关注了。Hash、Set、ZSet、List元素数量超过1万个或整个对象占用内存超过10MB就要纳入监控和治理。单个大Key占用内存超过50MB以上属于危险级别处理时要格外小心。大Key的阻塞风险不只是读取慢删除同样致命。在老版本Redis里删除一个大Key比如一个包含几十万个元素的列表意味着主线程要一次性释放这块内存释放过程中所有请求都被挂起。很多团队第一次踩坑就是因为清理垃圾数据时执行了一条DEL结果整个Redis卡了几百毫秒。解决删除大Key的阻塞最简单的方式是使用Redis 4.0引入的UNLINK命令redis-cli UNLINK big_key_nameUNLINK会尝试把释放操作丢给后台线程异步处理主线程只需要做一个引用计数递减不会长时间卡住。但要注意UNLINK并不是绝对无阻塞的如果对象的引用计数不是1它还是会同步释放而且释放一个特别庞大的聚合对象时后台线程同样会占CPU极端情况下也会拖累主调度。所以根本的解决思路还是治理大Key而不是等它出现了再想怎么删。查找大Key可以直接用内置的扫描工具redis-cli --bigkeys它会以SCAN方式扫描全库并统计各种类型里最大的Key不会污染主线程太多。新版本还可以用--memkeys来从内存维度统计识别那些元素不多但单个元素巨长的Key。建议在低峰期定期跑一次把扫描结果存下来做对比。2.3 集中过期藏在expire背后的CPU尖刺还有一个很隐蔽的命令层阻塞来源大量Key在同一时间点集中过期。Redis的过期键清理是周期性的默认每秒执行10次由hz配置控制每次会抽样一部分过期键并删除。如果某个瞬间有过期任务堆积主线程要在过期扫描和命令处理之间来回切换CPU开销陡增表现就是延迟在某个固定时间点出现规律性尖刺。这类问题的经典场景是业务里把缓存过期时间设成整点或同一个相对时间比如“凌晨0点所有用户标签缓存同时过期”然后到了那个点Redis就卡顿几秒到几十秒。排查时你会发现慢日志里没有几条像样的命令但系统CPU可能突然飙升。解决方案也很朴素在设置过期时间时加入随机扰动。比如统一过期时间原本是3600秒就改成3600 random(0, 300)让过期分布错开。同时可以把hz适当调高比如从10调到20让过期任务更加分散地执行但这个参数会带来一点CPU开销需要权衡。3. 持久化机制是阻塞的重灾区fork、RDB快照与AOF刷盘3.1 fork时页表复制带来的全实例停顿如果命令层排查了一圈没发现问题我建议你立刻把目光转向持久化这是线上Redis隐性阻塞的第二大源头。Redis在生成RDB快照或执行AOF重写前需要先fork一个子进程。fork这个系统调用本身在Linux里算是轻量级的但如果父进程内存很大fork复制页表的时间就不可忽略了。这里面的原理是fork需要给子进程复制父进程的页表项内存越大页表就越大复制时间越长期间父进程完全停止响应。一个常见参考数值4GB左右内存的实例fork可能需要几十毫秒十几GB的实例fork耗时可能到几百毫秒甚至超过1秒。如果你Redis里全是几GB级别的数据那每次RDB快照就等于给线上请求来了一次“人为断电”。这类问题通过慢日志是看不到的因为阻塞发生在命令执行之外。如何确认看日志和INFO输出。在Redis启动日志里如果出现类似Background saving started by pid ... fork operation may be slow就说明fork已经对主线程产生了明显影响。更精确地可以通过INFO persistence查看rdb_last_fork_usec字段redis-cli INFO persistence | grep rdb_last_fork_usec这个字段的单位是微秒代表上一次fork的耗时。如果这个数值持续大于几万到几十万也就是几十到几百毫秒整个实例的阻塞风险就非常高了。3.2 AOF刷盘策略和磁盘性能的真实影响AOF持久化模式下刷盘策略是个老生常谈但依然坑多的话题。appendfsync有三个取值always、everysec、no。三者的差异我在下面列出来刷盘策略行为阻塞风险数据丢失窗口always每次写命令执行后都调用fsync最高磁盘慢时每次写都变慢最多丢失1条命令everysec每秒后台刷盘一次较低但刷盘瞬间磁盘慢也会拖累最多丢1秒数据no完全交给操作系统最低内存中积压后统一落盘可能丢几十秒到上百秒数据很多团队为了数据安全会把appendfsync设为always但如果底层的磁盘是普通的机械盘或者虚拟化环境里的共享存储每次fsync的耗时可能达到几十毫秒。这种情况下主线程写入越频繁延迟就越夸张而且这类问题同样不会出现在慢查询日志里。当你排除掉命令层问题又看到“写入延迟和磁盘IO读写量成正相关”时基本上就是always模式在作祟了。我的建议是如果数据库本身已经能保证状态可重建appendfsync设为everysec已经能满足绝大多数场景的数据安全需求如果一定要用always请务必保证底层磁盘是SSD并且单独监控fsync耗时。3.3 大内存页与内核参数的坑除了fork本身Linux内核的某些默认配置也会放大持久化阻塞。最常见的就是透明大页Transparent Huge PagesTHP。THP本来是内核用来减少TLB miss的优化但在Redis场景下它是个彻头彻尾的坑。原因在于当Redis fork子进程后主进程仍然在继续写入触发写时复制copy-on-write时如果启用了THP内核默认按2MB的大页粒度复制内存页而不是常规的4KB小页。这就意味着只需要修改一个4KB的数据系统可能也要复制整块2MB的内存页COW的开销被放大了几百倍内存占用和CPU都会飙升。线上比较稳妥的做法是直接关闭THPecho never /sys/kernel/mm/transparent_hugepage/enabled同时为了确保重启后依然生效要把它写进系统启动脚本或crontab里。另外一个重要内核参数是vm.overcommit_memory如果它是0或者2Redis在内存偏紧时fork可能直接失败日志里会出现:Cant save in background: fork: Cannot allocate memory这会导致RDB备份失败同时主线程反复尝试fork反而加重阻塞。建议将vm.overcommit_memory设置为1允许进程申请超过物理内存的虚拟内存空间让fork更大概率成功因为fork只是复制页表不会立刻分配全部物理内存。4. 内存紧缺与操作系统层面的隐形阻塞swap、淘汰和碎片4.1 swap的恐怖延迟为什么CPU不高却卡顿这是我认为Redis所有阻塞里最阴险的一种swap。当Linux物理内存不足时会把一部分内存页交换到磁盘上Redis的数据是全部驻留在内存的一旦被swap出去访问那块数据时就会触发磁盘IO延迟从微秒级变成毫秒甚至秒级。为什么说它阴险因为CPU可能完全不高内存used_memory看起来也没满业务却时不时卡顿。其实数据已经有一部分被换到磁盘上了Redis自己并不知道它只知道访问那些数据特别慢。我遇到过一次真实情况某项目实例的物理内存是8GBRedis maxmemory设成7GB又跑了一些其他进程最后内存不够开始swap结果P99延迟从1ms涨到300ms查了三天没头绪后来用vmstat看到si和so两个字段持续非零才锁定swap。排查swap的命令组合# 查看整体swap情况 free -h # 查看具体进程swap占用Redis主进程PID在这里替换 grep VmSwap /proc/redis_pid/status # 动态观察换入换出 vmstat 1vmstat输出里的siswap in和soswap out只要持续非零就是明确的swap信号。严格来说线上Redis环境应该禁用swap或者至少设置swappiness极低。svm.swappiness建议设置成0或1让系统尽量不主动使用swap。另外把maxmemory设置得比物理内存余量小得多一些给系统预留足够空间才是根本方案。4.2 内存淘汰策略在极端场景下的卡顿Redis在达到maxmemory后会根据淘汰策略清理键常用的有allkeys-lru、allkeys-lfu、volatile-lru等。理论上淘汰一个键的耗时很低但有两个场景会让它演变成阻塞事故。第一个场景是淘汰大Key。如果所有键都比较大淘汰时需要释放大块内存释放操作本身会占用主线程时间。我曾经见过一个实例里全是大String单个1MB以上内存打满后Redis开始频繁淘汰每次淘汰都能看到几十毫秒的延迟尖刺。第二个场景是淘汰和过期同时发生。当实例到达内存上限又恰好有一批过期键集中到期Redis既要执行过期删除又要执行内存淘汰两件事叠加在单线程里处理瞬间的CPU占用会非常高。排查内存淘汰问题可以在INFO stats里看evicted_keys字段的增长情况redis-cli INFO stats | grep evicted_keys如果这个值在延迟期间快速增长基本就能确认淘汰策略是主因。优化手段包括把大Key拆小避免淘汰大对象的开销合理设置maxmemory不要把实例压到临界点运行根据业务读取特征选择更合适的淘汰策略比如allkeys-lfu在冷热分明的场景下淘汰更聪明可以减少频繁淘汰。4.3 从INFO memory判断内存健康度我每次做Redis体检都会重点看INFO memory里的几个指标used_memoryRedis实际使用的内存。used_memory_rss进程在操作系统里实际占用的物理内存。mem_fragmentation_ratio碎片率等于used_memory_rss / used_memory。maxmemory实例内存上限。碎片率超过1.5说明内存碎片明显偏多这会导致实际物理内存占用远超实际数据大小间接引发swap风险。碎片整理的参数activedefrag可以在系统空闲时开启但开启后本身会增加CPU开销建议只在碎片率持续偏高且业务低峰时启用。另一个值得关注的问题是长时间运行后内存分配器默认是jemalloc内部碎片和资源池膨胀导致used_memory明明不高但RSS却一直上涨。这种情况最直接的解法就是主动规划一次主从切换或滚动重启让进程重新获得干净的内存布局。5. 主从复制与集群动作引发的“连锁阻塞”5.1 全量复制时的fork与消耗主从复制过程中的全量同步是Redis持久化之外另一个常见的fork触发点。当从库第一次连接或者复制偏移量落后太多时主库会触发bgsave生成RDB快照然后通过网络全量传给从库。这个bgsave的fork阶段就会造成主线程暂停前面讲的fork耗时问题在这里完全适用。如果主库本身内存已经很大比如几十GB那全量同步基本等于主动制造一次秒级阻塞。更糟的情况是多从库同时触发全量同步主库要连续fork多次或者多个fork进程同时存在CPU和磁盘IO都会被拉满主库的写延迟会变得惨不忍睹。我处理过一个具体案例某个实例有3个从库某天网络抖动让两个从库同时断连接着同时开始全量复制主库在几分钟内连续fork两次每次耗时都在400毫秒以上业务侧在那一分钟里出现了明显的超时告警。后来我们限制了同一时间内只允许一个从库重连并且把repl-backlog-size调大减少了断连后落到全量复制的概率。5.2 主从切换和复制风暴对主库的冲击主从切换本身不是阻塞但切换后往往会引发复制风暴。哨兵或集群发起failover后新的主库需要和它下面的从库同步数据如果从库数量很多且没有良好的复制拓扑一个主库带着多个从库同时开始全量同步压力全部压在新主库上。这种情况下你看到的表现可能不是某一条慢命令而是整个实例延迟整体抬升。集群场景下的槽位迁移则是另一个阻塞源。Redis Cluster迁移数据时会多次执行MIGRATE命令这个命令是同步的主线程需要把一个Key序列化、发送给目标节点、等待目标节点返回后再处理下一个请求。如果待迁移的Key很大或者需要迁移的Key数量很多每个MIGRATE操作都要占用主线程业务请求在这些间隙里被拖延。我的经验是做集群扩容和槽位迁移务必选择业务低峰期并且把迁移的批量参数调小不要一次搬太多数据。同时在迁移窗口期密切观察主库的LATENCY和CPU一旦延迟尖刺明显就暂停迁移让实例喘口气。6. 网络与慢客户端阻塞不只在Redis内部6.1 慢客户端如何吃掉服务端缓冲区很多时候Redis阻塞的根源不在服务端的处理逻辑而在客户端的一侧。这里的“慢客户端”不是指慢查询命令而是指那些连接了Redis却迟迟不读取返回数据的客户端连接。Redis为每个客户端维护出站缓冲区如果某个客户端一直不读取数据服务端就必须在缓冲区里不断堆积返回结果。堆积越来越多占用的内存越来越大同时主线程在拷贝和发送数据时花费的时间也越来越长。极端情况下这些慢客户端能吞掉大量内存间接触发swap和淘汰甚至让Redis直接进入不可服务状态。用以下命令可以查看每个客户端的输出缓冲区状态redis-cli CLIENT LIST在输出里关注obl、oll、omem字段omem表示输出缓冲区占用的内存。如果某些连接的omem一直居高不下或者某个客户端连接数异常膨胀很可能就是它在拖慢整个Redis。适当调整client-output-buffer-limit参数可以控制风险但这只是止损更要从业务侧排查为什么客户端消费消息这么慢比如订阅者有阻塞的消费逻辑。6.2 Redis 6 IO多线程的真实定位很多人以为Redis 6引入的多线程IO能彻底解决阻塞问题这是一个很大的误解。Redis 6.0的io-threads只是把socket上的网络读写分散到多个IO线程但命令执行的核心逻辑仍然在单线程主线程中串行执行。也就是说它能改善高并发连接下网络读写带来的开销但如果你有一个大Key慢命令照样会把主线程卡死。所以不要把多线程IO当成万能药。它主要解决的是那种“连接数非常多但单条命令都很快”的场景比如某些网关服务建立了几万个连接网络事件的处理占用了主线程大量时间。配置时也不要盲目开太多IO线程一般2到4个就够开太多反而会因为锁竞争和上下文切换带来额外开销。7. 线上阻塞的实战排查链路从现象到根因7.1 第一步用redis-cli和内置指标确认阻塞范围我现在接到Redis延迟异常的第一反应不是翻代码而是先用工具确认阻塞范围。第一步永远是用redis-cli直接测到实例的端到端延迟。redis-cli -h host -p port --latency -i 1这个命令会每秒发一个PING持续统计延迟分布。如果你执行后看到avg正常但偶尔出现几百毫秒的毛刺说明问题就在Redis或它的宿主机上如果avg本身就很高那先检查你自己这台客户端和目标服务器之间的网络链路。另外还可以用--intrinsic-latency测一下这台机器本身的内核延迟基线redis-cli --intrinsic-latency 100如果主机本身的延迟就很高那问题可能出在宿主机CPU争抢或虚拟化层这时候再排查Redis意义也不大。7.2 第二步慢日志、命令统计与耗时分布分析确认阻塞在Redis侧之后我习惯按“命令层→持久化层→系统层”的顺序逐层排除。命令层先看慢日志redis-cli SLOWLOG GET 50如果慢日志里有大量的耗时命令那直接顺着命令去代码里找来源。如果慢日志空荡荡不要慌这很可能说明阻塞不在命令执行本身。接着看命令统计和状态计数redis-cli INFO commandstats redis-cli INFO stats重点看两个方向一是有没有哪个命令的调用次数和耗时异常高二是expired_keys和evicted_keys在延迟尖刺时间段内是否出现突增。如果expired_keys突增那就是集中过期问题如果evicted_keys突增那就是内存淘汰问题。7.3 第三步持久化与系统层的交叉验证命令层没发现问题时立刻看持久化和系统层。先用INFO persistence确认fork耗时redis-cli INFO persistence如果rdb_last_fork_usec耗时很大再把焦点放到cron里是不是正好有RDB定时保存或者从库是不是正在发起全量复制。如果需要同时评估系统层我会开三个终端分别跑# 终端1观察CPU和内存以及swap vmstat 1 # 终端2观察磁盘IO iostat -x 1 # 终端3定位Redis进程CPU和内核态占比 top -H -p redis_pid如果vmstat里si/so非零说明swap正在发生如果iostat里util接近100%说明磁盘IO瓶颈如果top里Redis的主线程CPU占用异常高且内核态占比大可能是COW写时复制或网络发送在大量消耗CPU。7.4 一套可复用的排查命令组合为了降低下次排查的记忆成本我把整套流程沉淀成一个可以直接照抄的排查序列你按顺序执行基本能覆盖90%的阻塞场景# 1. 延迟基线 redis-cli -h host -p port --latency -i 1 # 2. 慢日志 redis-cli -h host -p port SLOWLOG GET 30 # 3. 命令统计和状态计数 redis-cli -h host -p port INFO commandstats redis-cli -h host -p port INFO stats # 4. 持久化耗时 redis-cli -h host -p port INFO persistence # 5. 系统和进程指标 vmstat 1 iostat -x 1 top -H -p redis_pid # 6. 连接与客户端状态 redis-cli -h host -p port CLIENT LIST redis-cli -h host -p port INFO clients再补充一个非常实用的技巧如果要定位“间歇性毛刺”可以将latency monitor打开配置一个阈值比如100毫秒redis-cli CONFIG SET latency-monitor-threshold 100 redis-cli LATENCY LATEST这样Redis会自己记录延迟事件的类型和耗时包括fork、expire-cycle、aof-write这些内部事件。很多之前靠肉眼看日志根本定位不到的问题用LATENCY LATEST一眼就能看到阻塞发生在哪个环节。我曾经排查过一个案例就特别典型某服务每天20:05左右准时卡两秒慢日志空白CPU不高。打开latency monitor后发现事件类型是command说明确实是命令执行导致但慢日志为什么没记录后来才找到原因是某个客户端用MONITOR命令开启了全量命令流监控MONITOR本身会显著增加服务端开销导致大量命令的执行时间被拉长但慢日志只记录“命令本身计算耗时”没考虑监控带来的开销。这提醒我一件事MONITOR是排查工具也是阻塞源不要在线上长时间开用的时候必须掐秒表。8. 我的预防清单踩过坑之后的配置与习惯写到最后把多年踩坑积攒的预防措施列一份清单。如果说前面的章节是“治病”这一部分的思路就是“防病”。第一配置层面我强烈建议每条线上实例都落实下面几项禁止危险命令通过rename-command把KEYS、FLUSHALL、CONFIG等在业务侧禁掉或者只允许运维通道使用。设置maxmemory必须设置并留足系统余量不要让Redis进程无限吃内存导致swap。关闭THPecho never /sys/kernel/mm/transparent_hugepage/enabled写进开机自启。设定合理的slowlog阈值和latency monitor阈值我一般用slowlog-log-slower-than 5000latency-monitor-threshold 100。appendfsync推荐everysec并确保磁盘为SSD。如果启用了Redis 6的IO多线程线程数保守一点2到4个足够。第二开发协作层面要把“禁止大Key、禁止O(N)命令、过期时间加随机扰动”写进代码规范和评审清单。很多时候线上出问题不是Redis不行而是使用方式出了问题。比如某次事故的根源就是业务方为了取一个统计值在一个几百万元的Hash上执行了HGETALL这种代码在数据量小的时候毫无感知数据涨起来就变成定时炸弹。第三监控层面除了常规的CPU、内存、网络还要专门盯几个Redis专属指标延迟P99、慢日志条数、evicted_keys、expired_keys的速率、rdb_last_fork_usec、客户端输出缓冲区omem峰值。这些指标配合告警可以让你在阻塞还没造成大规模故障的时候就先发现问题。根据我个人的经验90%的Redis阻塞都不是Redis本身坏了而是使用方式或周边环境里那些不起眼的小问题被数据量放大成了事故。每次排查完阻塞问题我都会习惯性地把“根因修复方案如何提前发现”记下来下次再遇到类似延迟尖刺就能直奔主题少走很多弯路。希望这篇总结也能给你一些启发让你下次面对Redis卡顿的时候多一条快速定位的思路。