☰
Redis缓冲区内存膨胀与连接超限排查调优指南
2026/10/27 16:31:34 网站建设 项目流程

1. 缓冲区为什么是Redis里最容易“背刺”你的一块内存

晚高峰刚过一半,监控平台上某业务系统的Redis实例内存使用率从四十多个百分点一路爬到八十以上,可数据量没涨、key数量没变、慢查询列表也干干净净。与此同时,客户端连接数却在往下掉,实例日志里反复出现输出缓冲区超限、连接被强制关闭的提示。这种“内存涨了、连接掉了、数据却没多”的组合,排查时特别容易把人带偏——先去怀疑大key,再去看内存碎片率,接着又翻持久化配置,兜一大圈才回到真正的原因:缓冲区在偷偷吃内存。

Redis缓冲区、客户端输入缓冲区、客户端输出缓冲区、主从复制缓冲区、repl_backlog_buffer、client-output-buffer-limit,这几个词平时在文档里看着不起眼,一旦在线上出问题,表现往往非常吓人:实例内存被顶到接近上限、写入开始报错、从库反复触发全量同步、订阅客户端被踢下线。更麻烦的是,缓冲区占用的内存不像普通键值那样能靠淘汰策略释放,maxmemory 到了也拿它没办法,只能从连接和配置层面动手。

这篇笔记适合两类人看:一类是已经把 Redis 用起来、但还没认真研究过缓冲区细节的开发者,另一类是被“内存莫名上涨”“从库老是全量同步”折腾过的运维同学。下面我会把几类缓冲区拆开讲清楚:它们分别在什么条件下膨胀、膨胀之后会引发什么连锁反应、排查时看哪几个指标、参数到底怎么估。命令和配置都是可以直接复制去试的,踩过的坑也会一并写出来。

1.1 一次内存曲线抬头带来的三个疑问

先还原一下现场。某业务系统的缓存实例在晚高峰出现内存快速上涨,运维同学第一反应是查大key,用扫描工具跑了一遍,最大的 key 也就几百 KB,占比很低;第二反应是查慢查询,慢日志里只有零星几条,耗时不长;第三反应是看持久化,AOF 重写刚做完不久,也不像是重写缓冲区的问题。三条路都走不通,问题就卡住了。

真正把方向掰回来的是三个疑问。第一个疑问:used_memory涨了,但used_memory_dataset涨幅很小,多出来的内存到底记在哪个账上?第二个疑问:为什么连接数在下降,而且下降的时机和内存上涨的时机高度重合?第三个疑问:maxmemory明明设了,为什么没有触发淘汰,反而开始拒绝写入?

这三个疑问指向同一个答案:内存被客户端连接相关的缓冲区占用了,而连接因为缓冲区超限被服务端主动断开。连接断开之后,它占用的缓冲区内存会被释放,所以内存曲线呈现出“涨上去、掉下来、再涨上去”的锯齿状,看起来像抖动,实际是同一批慢客户端在反复重连、反复超限。把这个逻辑串起来之后,后面的排查就顺了。

1.2 Redis里到底有几块缓冲区,各自归谁管

先把概念理清楚。Redis 里叫“缓冲区”的东西不止一个,它们归属不同、触发条件不同、限制参数也不同,混在一起讲很容易乱。下面这张表是我自己整理的口径,排查时按这张表对号入座基本不会跑偏。

缓冲区类型归属主要触发场景默认限制排查入口
客户端输入缓冲区每个客户端连接一份客户端发送大命令、命令发送速度超过处理速度client-query-buffer-limit,默认 1gbclient list的qbuf、qbuf-free
客户端输出缓冲区每个客户端连接一份返回结果过大、客户端读取过慢、Pub/Sub 消费慢client-output-buffer-limit分类配置client list的obl、oll、omem
复制缓冲区主库为每个从库各分配一份全量同步期间积累写命令归入replica类输出缓冲区限制client list中flags=S的连接
复制积压缓冲区主库全局一份,环形结构主从断连后的增量同步repl-backlog-size,默认 1mbinfo replication的偏移量差值
AOF 缓冲区全局一份每次写命令追加到 AOF 前的暂存无独立上限,受刷盘策略影响info persistence、aof_buf相关指标

这五块里,前两块是“每个连接一份”,最容易随着连接数放大;第三块和第四块跟主从架构绑定,单机部署可以暂时不看;第五块跟持久化绑定,出问题的表现通常是写延迟抖动而不是内存暴涨。后面的章节会按这个顺序展开,先从最常见的客户端输入缓冲区讲起。

注意:client-output-buffer-limit在较新版本里把从库类别写作replica,老版本写作slave,两种写法功能一致,改配置时别因为写法不同以为参数没生效。

2. 客户端输入缓冲区:命令还没执行,内存先被占住了

输入缓冲区是 Redis 为每个客户端连接准备的一块内存,用来暂存“已经从 socket 读进来、但还没被解析执行”的命令数据。打个比方,它像是餐馆前台的点单台:客人把菜名报上来,服务员先记在单子上,后厨再按单子做。正常情况下单子写完就撕掉,占用很小;但如果客人一口气报了一百道菜,或者后厨出菜速度突然变慢,点单台就会堆满单子。

这块内存的特点是:它统计在used_memory里,但不属于数据键,淘汰策略管不到它;它的上限由client-query-buffer-limit控制,默认是 1gb,看着很宽松,实际上一旦有连接顶到这个值,说明客户端行为已经严重异常了。更隐蔽的是,输入缓冲区在执行完命令后不会立刻缩回去,Redis 会保留一段时间的峰值容量,所以你在client list里看到的qbuf可能比当前正在执行的命令大不少,这是正常的,不要一看到qbuf有几 KB 就紧张。

2.1 输入缓冲区是怎么长出来的

从一次网络读取说起。Redis 的事件循环发现某个连接的 socket 可读,就会把数据读进输入缓冲区,然后尝试按 RESP 协议解析出完整命令。解析出一条命令就执行一条,执行完继续解析缓冲区里剩下的数据,直到解析不出完整命令为止。如果客户端发过来的数据不完整,比如一条命令只发了一半,剩下的半条就会留在缓冲区里等下一次读取。

这个机制本身没问题,问题出在两个地方。第一,客户端发送速度超过服务端处理速度时,缓冲区里的数据会越积越多,因为读进来的速度比消费掉的速度快。第二,单条命令本身就特别大时,缓冲区需要一次性容纳整条命令才能开始解析,比如一个几 MB 的SET命令,或者一个参数特别多的MSET,在解析完成之前全部躺在输入缓冲区里。

还有一个容易被忽略的细节:Redis 在读取数据时是按批读的,一次事件循环可能读进来多条命令。如果客户端用了 pipeline 一次性发了几万条命令,这一批数据会全部进入输入缓冲区,然后逐条执行。pipeline 本身是提升吞吐的好手段,但批次大小失控时,输入缓冲区就会被瞬间顶高。

2.2 哪些操作最容易把它撑爆

按我实际遇到的情况排个序,最容易把输入缓冲区撑大的操作有这么几类。

第一类是超大 value 的写入命令。单条命令的参数字节数直接决定输入缓冲区的峰值,一个 10MB 的 value 写入,输入缓冲区至少要能容纳 10MB 左右的数据才能完成解析。如果业务里存在这种大对象,又恰好并发写入,几个连接同时来,输入缓冲区占用就会非常可观。

第二类是失控的 pipeline。有些客户端为了追求吞吐,把几千甚至几万条命令攒成一批发出去,服务端一次读取就要吃下整批数据,输入缓冲区峰值跟着批次大小走。批次越大,峰值越高,而且这批命令执行时间也长,期间缓冲区一直占着。

第三类是MONITOR命令。这个命令会让 Redis 把执行的每一条命令都推送给监控连接,它影响的不只是输出缓冲区,监控连接本身如果读取慢,输出缓冲区也会堆积,反过来又会拖慢整体。线上实例我一般建议直接禁用或者严格限制使用时长。

第四类是协议异常或客户端 bug。比如某些客户端在连接复用、断线重连时把半条命令留在了缓冲区里,服务端一直等后续数据,这块内存就一直占着。这种情况从client list里能看到qbuf很大但cmd字段是空的或者一直不变。

2.3 用client list把qbuf和qbuf-free读明白

排查输入缓冲区,最直接的工具就是client list。输出字段比较多,刚开始看容易眼花,重点盯住这几个:id、addr、name、age、idle、qbuf、qbuf-free、cmd。

qbuf表示当前输入缓冲区已分配的总容量,单位是字节;qbuf-free表示这块缓冲区里还空着多少容量。两者相加大致就是缓冲区当前的分配上限。判断有没有异常,不能只看qbuf的绝对值,要结合cmd和idle一起看:如果某个连接的qbuf持续很大,cmd一直停在某条命令上,idle又很小,说明它一直在发数据但处理不完,或者命令本身太大。

# 按 qbuf 倒序,找出输入缓冲区占用最大的前十个连接 redis-cli -h <实例地址> -p <端口> client list \ | awk '{ for (i = 1; i <= NF; i++) { if ($i ~ /^qbuf=/) { split($i, a, "="); qbuf = a[2] } if ($i ~ /^id=/) { split($i, b, "="); id = b[2] } if ($i ~ /^cmd=/) { split($i, c, "="); cmd = c[2] } if ($i ~ /^addr=/) { split($i, d, "="); addr = d[2] } } print qbuf, id, addr, cmd }' \ | sort -rn | head -n 10

除了client list,info clients里还有两个很有用的指标:client_recent_max_input_buffer和client_recent_max_output_buffer。前者记录的是最近一段时间内所有连接中出现过的最大输入缓冲区,后者对应输出缓冲区。这两个值不需要你一个个连接去翻,直接看汇总就能判断“最近有没有出现过异常大的缓冲区”,做监控告警非常合适。

如果发现某个连接确实异常,可以用client kill精确干掉它,而不是重启实例。命令写法上,按 ID 杀最稳:

# 按连接 ID 精确关闭 redis-cli -h <实例地址> -p <端口> client kill id <连接ID>

2.4 处理套路与配置建议

输入缓冲区的治理思路,说到底是两句话:把单条命令变小,把批次控制住。

client-query-buffer-limit默认 1gb,我一般不建议直接调小到很小的值,因为它限制的是单条命令解析所需的峰值容量,调得太小会误伤正常的批量写入。比较稳妥的做法是先通过监控确认峰值水平,再把上限设成“正常峰值的一点五到两倍”,比如业务里最大的单条命令在 2MB 左右,上限设成 8mb 到 16mb 就够用,既留了余量,又能在异常时及时把连接掐掉,避免一块内存被长期占着。

配置可以动态改,不用重启:

# 动态调整输入缓冲区上限(示例值,按实际业务调整) redis-cli -h <实例地址> -p <端口> config set client-query-buffer-limit 16mb

pipeline 批次这块,我自己的经验是单批控制在 500 到 1000 条以内,同时单批总字节数控制在一个明确的上限,比如 1MB。只限条数不限字节是不够的,因为一条大命令抵得上一千条小命令。客户端封装的时候,两个维度都要卡。

注意:输入缓冲区达到上限后,Redis 会记录类似Closing client that reached max query buffer length的日志并关闭该连接,客户端侧看到的是连接被重置。排查时不要只盯着客户端日志,服务端这份日志才是根因。

3. 客户端输出缓冲区:慢消费者拖垮快服务端

如果说输入缓冲区是“客人点单太快”,那输出缓冲区就是“后厨出菜太快、客人吃得太慢”。Redis 把命令执行结果写进输出缓冲区,等待 socket 可写时再发出去。服务端处理一条命令可能只要几十微秒,但网络发送受限于客户端读取速度、网络带宽、客户端处理逻辑,一旦发不出去,结果就堆在输出缓冲区里。

这块内存的危险程度比输入缓冲区更高,原因是它没有统一的 1gb 上限,而是按连接类别分别配置,normal类默认甚至是完全不限制。很多实例出问题,就是因为普通客户端这一类配的是0 0 0,也就是硬限制、软限制、软限制时长全部为零,等于不设防。等到业务里出现一个超大返回值的查询,输出缓冲区可以一路涨到把实例内存顶穿。

3.1 三类输出缓冲区与各自的默认阈值

Redis 把客户端按用途分成三类,分别配置输出缓冲区上限。配置项的格式是client-output-buffer-limit <类别> <硬限制> <软限制> <软限制秒数>,硬限制和软限制的单位都是 mb 或 gb,写 0 表示不限制。

类别适用连接常见默认值说明
normal普通读写客户端0 0 0默认不限制,风险最高,建议显式配置
replica(旧版写slave)主从复制连接256mb 64mb 60全量同步期间积累写命令
pubsub发布订阅客户端32mb 8mb 60订阅者消费慢时容易堆积

三类里我最担心的是normal。replica和pubsub至少有默认值兜底,normal一旦不设限,任何一条返回大量数据的命令都可能成为导火索。比如误用了全量查询类的命令、某个接口一次性返回了几十万条记录、某个导出任务直接扫了一个大集合,结果全部写进输出缓冲区等网络发送。

3.2 软限制与硬限制的触发逻辑

这两个概念经常被混淆,实际行为差别很大。

硬限制是“一碰就死”。输出缓冲区一旦超过硬限制的字节数,Redis 立即把这个连接标记为待关闭,日志里会出现scheduled to be closed ASAP for overcoming of output buffer limits这样的提示,连接被断开,缓冲区内存释放。

软限制是“持续超标才死”。输出缓冲区超过软限制之后,Redis 不会立刻动手,而是开始计时,只有持续超过软限制达到配置的秒数,才会关闭连接。这个设计是为了容忍短时间的突发,比如一次批量返回导致的瞬间高峰,只要很快回落就不会被误杀。

理解了这个逻辑,配置就有章法了。硬限制设成“绝对不能超过的红线”,软限制设成“可以短暂触碰但要尽快回落的水位”,软限制秒数设成“能容忍的持续时间”。比如normal 512mb 128mb 30,意思是:任何时候超过 512mb 直接断,超过 128mb 且持续 30 秒也断,中间这段突发是允许的。

3.3 参数怎么定:从业务侧估一个合理值

拍脑袋配参数是排查时最常见的二次伤害。我的做法是从业务侧倒推,具体分三步。

第一步,确认单次返回的最大数据量。翻一遍业务里的查询接口,找出返回结果最大的那类请求,估算它的序列化后字节数。假设最大的单次返回在 2MB 左右,那么单连接的输出缓冲区至少要能容纳这个量级,否则正常请求都会被误杀。

第二步,确认单实例的并发连接规模。如果实例上有 2000 个活跃连接,理论上不可能所有连接同时返回 2MB,但要有兜底。按“正常峰值连接的百分之五同时返回最大结果”估算,2000 × 5% × 2MB ≈ 200MB。这个数字是粗略的,但比拍脑袋强。

第三步,结合实例总内存定上限。输出缓冲区的总和不能吃掉太多内存预算,一般控制在实例内存的 10% 到 20% 以内比较安全。如果实例是 8GB,那所有缓冲区加起来控制在 1GB 左右,单个连接的硬限制设成 256mb 到 512mb 是合理区间。

综合下来,一个比较通用的起点是:

# 普通客户端:硬限制 512mb,软限制 128mb,持续 30 秒触发 # 从库连接:硬限制 512mb,软限制 128mb,持续 60 秒触发 # 订阅客户端:硬限制 64mb,软限制 16mb,持续 60 秒触发 redis-cli -h <实例地址> -p <端口> config set client-output-buffer-limit "normal 512mb 128mb 30" redis-cli -h <实例地址> -p <端口> config set client-output-buffer-limit "replica 512mb 128mb 60" redis-cli -h <实例地址> -p <端口> config set client-output-buffer-limit "pubsub 64mb 16mb 60"

注意config set这个参数的值是一个完整字符串,三类要分三次设,每次只写一类,不要试图一次写三段,格式会解析失败。设完之后用config get client-output-buffer-limit确认,再写进配置文件,避免重启后丢失。

3.4 线上排查演练:obl、oll、omem怎么用

client list里跟输出缓冲区相关的字段有三个:obl、oll、omem。obl是输出缓冲区的字节长度,oll是输出缓冲区里链表对象的个数,omem是这两者占用的总内存。看的时候重点看omem,它才是真正计入used_memory的数字。

# 找出输出缓冲区内存占用最大的前十个连接 redis-cli -h <实例地址> -p <端口> client list \ | awk '{ for (i = 1; i <= NF; i++) { if ($i ~ /^omem=/) { split($i, a, "="); omem = a[2] } if ($i ~ /^id=/) { split($i, b, "="); id = b[2] } if ($i ~ /^cmd=/) { split($i, c, "="); cmd = c[2] } if ($i ~ /^addr=/) { split($i, d, "="); addr = d[2] } } print omem, id, addr, cmd }' \ | sort -rn | head -n 10

实测下来,omem排在前面的连接,cmd字段往往能直接告诉你问题出在哪。如果cmd是一条全量查询类的命令,那就是返回值太大;如果cmd是subscribe或者psubscribe,那就是订阅消费慢;如果cmd是monitor,那基本可以确定是监控连接在拖后腿。

还有一个辅助手段是看实例层面的汇总。info clients里的client_recent_max_output_buffer记录了最近出现过的最大输出缓冲区,如果这个值一直在涨,说明有连接反复顶高,即使当前已经被断开,问题还会复现。

3.5 客户端限流与连接治理

光靠服务端断开连接是治标不治本。连接被断开后,客户端通常会重连、重试,同一个慢查询再来一次,缓冲区再涨一次,形成循环。真正的解法在客户端侧。

第一,限制单次返回的数据量。查询接口加明确的分页上限,禁止无上限的全量拉取。需要导出大数据量时走离线任务,不要在在线实例上做。

第二,订阅客户端要独立。Pub/Sub 的消费逻辑如果和普通业务混在同一个连接池里,一个慢消费者会影响整个池子。我倾向于给订阅单独分配连接,并且订阅客户端只做轻量转发,真正的处理逻辑放到下游异步做。

第三,客户端读取要跟上。有些客户端框架在收到响应后处理逻辑很重,导致 socket 读取变慢,服务端发送不出去。这种情况要么把处理逻辑异步化,要么给这类客户端单独限流。

第四,对慢客户端做识别和隔离。可以通过定期采样client list,把omem持续偏高的连接标记出来,结合业务侧确认是哪个调用方,然后针对性治理。这个动作做成定时任务,比出事之后临时排查效率高得多。

注意:normal类不设限是很多实例的默认状态,也是最容易被忽略的风险点。上线新实例时,把三类限制都显式写进配置文件,别依赖默认值。

4. 主从复制缓冲区:一次全量同步背后的内存账

主从架构下,缓冲区的故事更复杂一些。主库要给每个从库维护一份复制缓冲区,用来在全量同步期间积累写命令;同时主库还有一个全局的复制积压缓冲区,用来支持断连后的增量同步。这两个东西名字很像,作用完全不同,混着讲很容易把排查方向带错。

先说结论:复制缓冲区影响的是“全量同步期间的内存峰值”,复制积压缓冲区影响的是“断连后能不能增量同步”。前者出问题的表现是从库连接被断开、全量同步反复重来,后者出问题的表现是从库每次都做全量同步、主库压力周期性飙升。

4.1 replication buffer 与 repl_backlog_buffer 不是一回事

复制缓冲区是主库为每个从库单独分配的,从库连接在主库上表现为一个特殊客户端,client list里能看到它的flags带S。全量同步时,主库先 fork 出子进程生成 RDB,这期间新进来的写命令不能丢,就暂存在这个复制缓冲区里,等 RDB 传完、从库加载完,再把缓冲区里的命令补发过去。这个缓冲区的大小受replica类输出缓冲区限制约束,超过限制主库会断开从库连接,全量同步前功尽弃,从库重连后又要从头来一遍。

复制积压缓冲区是主库全局唯一的环形缓冲区,所有从库共享。主库执行写命令时,除了发给从库,还会往这个环形缓冲区里写一份。从库断连重连后,会带着自己最后处理到的偏移量来找主库,如果这个偏移量还在环形缓冲区覆盖的范围内,主库就把缺失的部分补发过去,这就是增量同步;如果偏移量已经被新数据覆盖了,就只能退化成全量同步。

用一个类比:复制缓冲区像是给每个学生单独准备的补课讲义,全量同步期间落下的课先记下来,回头单独补;复制积压缓冲区像是教室后面那块公共黑板,所有学生都能看,但黑板容量有限,写满了就把最早的擦掉,来晚的学生就看不到前面的内容了。

4.2 repl-backlog-size 到底该给多大

这个参数默认 1mb,很多实例一直没改过。1mb 在写入量小的场景够用,但在写入频繁的实例上,从库只要稍微抖动一下,偏移量就超出覆盖范围,直接触发全量同步。

估算方法不复杂,核心是三个变量:主库的峰值写入速率、能容忍的最长断连时间、安全系数。

所需积压缓冲区 ≥ 主库峰值写入速率(字节/秒) × 最长断连时间(秒) × 安全系数

举个具体例子。某业务主库峰值写入速率大约是 2MB/s,网络抖动或者从库重启,从断开到恢复平均需要 60 秒,安全系数取 2,那么所需容量是 2MB × 60 × 2 = 240MB,配置成 256mb 比较合适。

如果峰值写入速率不好估,可以用另一个口径:看info replication里master_repl_offset的增长速度。隔一段时间采样两次,算出每秒增长的字节数,这个就是实际的写入速率,比拍脑袋准得多。

# 采样两次 master_repl_offset,间隔 10 秒,估算写入速率 redis-cli -h <主库地址> -p <端口> info replication | grep master_repl_offset sleep 10 redis-cli -h <主库地址> -p <端口> info replication | grep master_repl_offset

需要注意的是,积压缓冲区配得越大,占用的内存越多,而且这部分内存是实打实常驻的。我的经验是不要一步配到几个 G,先从 64mb 或 128mb 起步,观察一段时间全量同步的发生频率,再决定要不要往上加。配得过大反而会掩盖网络或从库本身的稳定性问题。

4.3 复制缓冲区溢出后的连锁反应

复制缓冲区溢出的表现很有辨识度:从库反复处于同步中状态,主库的 fork 次数明显增多,rdb_bgsave_in_progress频繁为 1,磁盘上 RDB 文件反复生成,网络带宽周期性打满。业务侧感受到的是主库周期性抖动,写延迟出现尖刺。

原因链条是这样的:全量同步期间,主库 fork 生成 RDB,同时把新写入的命令攒在复制缓冲区里。如果 RDB 生成时间长、传输慢,或者这期间写入量特别大,缓冲区就会顶到replica类的硬限制,主库断开从库连接。从库重连后发现自己需要的偏移量已经不在积压缓冲区里了,只能再请求一次全量同步,于是同样的过程再来一遍,形成循环。

这个循环一旦形成,主库的负载会持续偏高,因为 fork 和 RDB 传输本身就要消耗 CPU、内存和带宽。更糟的是,如果从库一直同步不上,业务读请求全部压到主库,压力进一步放大。

处理这个问题的顺序是:先确认是不是缓冲区限制导致断开,再看全量同步为什么慢,最后决定是调大缓冲区还是优化同步过程。只调大缓冲区而不解决同步慢的问题,相当于把症状压下去,根因还在。

4.4 实操:从info replication读出蛛丝马迹

排查主从复制问题,info replication是主要入口。重点看几个偏移量字段:master_repl_offset是主库当前的写偏移量,slave_repl_offset是从库已经处理到的偏移量,slave_read_repl_offset是从库从主库读到的偏移量。

三个值的关系能说明很多问题。如果master_repl_offset和slave_read_repl_offset差距很大,说明网络传输跟不上,或者主库发送被阻塞;如果slave_read_repl_offset和slave_repl_offset差距很大,说明从库收到了但处理不过来,通常是从库自身负载高或者执行了慢命令。

# 主库侧:确认从库连接状态和偏移量 redis-cli -h <主库地址> -p <端口> info replication # 从库侧:确认同步状态、偏移量、是否处于全量同步 redis-cli -h <从库地址> -p <端口> info replication

判断积压缓冲区是否够用,核心是看两个偏移量的差值有没有超过repl-backlog-size。info replication里有一个repl_backlog_size字段,配合偏移量差值就能算出余量。如果差值经常逼近上限,说明缓冲区偏小;如果差值长期很小却仍然频繁全量同步,那问题不在缓冲区,要去看网络或者从库的repl-timeout配置。

还有一种情况容易被忽略:从库的repl-timeout设得太小,网络稍有抖动就被判定为超时断开,断开一次就可能导致偏移量超出覆盖范围。这个参数默认 60 秒,如果跨机房同步延迟较高,可以适当放宽。

注意:调整repl-backlog-size之后,环形缓冲区会在下一次复制相关操作时按新大小重新分配,老数据不会迁移。调大之后建议观察一到两个全量同步周期,确认效果再固化到配置文件。

5. AOF缓冲区与持久化链路里的内存波动

AOF 缓冲区不像前面几类那么容易单独出问题,它更多是作为持久化链路的一环参与进来。但它的影响不小:AOF 缓冲区里的数据在刷盘之前是实打实占着内存的,如果刷盘策略配置不当,或者写入量突然放大,这块内存会明显上涨,同时伴随写延迟抖动。

5.1 aof_buf 的工作机制

每一条写命令执行完之后,Redis 会把它以 RESP 格式追加到 AOF 缓冲区aof_buf里,然后根据appendfsync配置决定什么时候真正写到磁盘。写到磁盘之后,缓冲区里的这部分数据才能释放。所以aof_buf的大小取决于两个因素:写入速率和刷盘频率。

appendfsync有三个取值,行为差别很大。always表示每条命令都同步刷盘,最安全但性能最差,缓冲区几乎不会堆积;everysec表示每秒刷一次,是性能和安全性的折中,也是最常用的配置,正常情况下缓冲区最多堆积一秒的写入量;no表示交给操作系统决定,性能最好,但缓冲区可能堆积较久,且故障时丢数据风险最高。

实际排查中,aof_buf异常增长通常伴随两个现象:aof_rewrite_in_progress为 1,或者磁盘 IO 出现明显瓶颈。AOF 重写期间,主进程的写入会同时进旧的 AOF 缓冲区和重写缓冲区,内存会有额外的峰值;磁盘 IO 跟不上时,aof_buf里的数据刷不下去,越积越多。

5.2 三种刷盘策略的取舍

选哪种刷盘策略,本质是在“丢数据容忍度”和“写入性能”之间做选择,没有标准答案。

always适合数据不容有失的场景,但代价是每条命令都要等磁盘,吞吐会掉得很厉害,除非写入量本来就很小,否则不建议。everysec是绝大多数场景的默认选择,最多丢一秒的数据,性能影响可控。no适合对数据完整性要求不高、但对吞吐要求高的场景,比如纯缓存。

这里有个容易被误读的细节:everysec模式下如果上一秒的刷盘还没完成,主线程会跳过本次刷盘,等下一次再做。这意味着在某些磁盘压力大的时段,实际刷盘间隔可能超过一秒,aof_buf会堆积更多。这个行为可以在日志里看到相关提示,排查时别只盯着配置值,要看实际刷盘耗时。

5.3 大事务与大pipeline对aof_buf的冲击

AOF 缓冲区堆积还有一个来源是写入批次过大。一个包含几千条命令的事务,或者一个超大的 pipeline,执行期间所有命令的 AOF 记录会在短时间内集中写入aof_buf,如果恰好碰上刷盘,这一批数据要一次性落盘,耗时会被放大,缓冲区占用也会出现尖峰。

我踩过的一个坑是:某个批量导入任务用了很大的 pipeline,导入期间 AOF 缓冲区持续偏高,同时aof_buf的刷盘耗时从毫秒级涨到几十毫秒,业务侧的写请求开始超时。后来把 pipeline 拆成小批,每批之间留一点间隔,问题就消失了。不是配置的问题,是写入形态的问题。

所以遇到 AOF 相关的内存和延迟问题,先看写入模式,再看配置。批量任务、定时任务、数据迁移这类操作,尽量安排在低峰期,或者拆成小批执行,避免和正常业务抢 IO。

6. 常见问题速查表与踩坑清单

缓冲区相关的问题,现象往往很相似,但根因可能完全不同。下面这张表是我自己排查时用的对照表,按现象找根因,能省不少时间。

6.1 排查速查表

现象优先检查项常用命令典型根因
内存上涨但数据量不变info clients的最大缓冲区指标info clients、client list输出缓冲区被慢客户端占满
连接数突然下降服务端日志里的缓冲区超限提示查看实例日志、client list输出缓冲区触发硬限制,连接被断开
写入报 OOM 但淘汰没生效used_memory与used_memory_dataset差值info memory缓冲区内存不可淘汰,顶穿了内存预算
从库反复全量同步偏移量差值与repl-backlog-sizeinfo replication积压缓冲区太小,增量同步退化为全量
主库周期性抖动rdb_bgsave_in_progress、fork 次数info stats、info persistence全量同步循环,fork 和传输压力叠加
订阅客户端频繁掉线pubsub类输出缓冲区限制client list、info clients消费速度跟不上发布速度
写延迟周期性尖刺AOF 刷盘耗时、aof_buf占用info persistence磁盘 IO 瓶颈导致刷盘变慢

查表的时候有个顺序建议:先看info clients的两个最大值指标,确认是不是缓冲区问题;再按连接类型定位到具体是哪一类缓冲区;最后才去调参数。反过来先调参数再看现象,很容易把真正的问题盖住。

6.2 几条踩过的坑

第一条,别把缓冲区内存当成可淘汰内存。used_memory超过maxmemory时,淘汰策略只对数据键生效,缓冲区占用的部分淘汰不掉。所以看到used_memory超标但evicted_keys没怎么涨,第一反应应该是去看缓冲区,而不是怀疑淘汰策略没配。

第二条,client list的输出要定期采样,不能只看当下。缓冲区问题是动态的,出事那一刻的快照可能已经错过了峰值。我的做法是每分钟采样一次,把omem和qbuf排在前面的连接记下来,连续几次都排在前面的,基本就是问题连接。

第三条,MONITOR命令要当成高危操作对待。它不只是输出缓冲区的问题,本身还会带来明显的性能开销。线上实在需要抓包排查,也要限定在很短的窗口内,抓完立刻断开,不要挂着不管。

第四条,输出缓冲区限制不要只配normal一类。三类都要配,尤其是主从架构下replica类不配,全量同步期间的行为完全不可控。配置写进配置文件,不要只靠config set,否则一次重启就回到解放前。

第五条,调参之前先确认写入形态。很多缓冲区问题不是参数配错了,而是业务侧突然上了大 batch、大返回、慢消费。参数只是兜底,写入形态才是根因。我在实际处理这类问题时,通常会把“最近有没有上线新的批量任务”作为第一个要问的问题,命中率比翻配置高得多。

第六条,监控指标里把client_recent_max_input_buffer和client_recent_max_output_buffer加上告警。这两个指标是现成的,不需要额外开发,设个阈值就能提前发现异常,比等到内存告警再去排查主动得多。阈值怎么定,可以先跑一周观察基线,再按基线的两到三倍设置。

第七条,处理慢客户端要有隔离思路,不能只靠杀连接。把订阅连接、批量任务连接、普通业务连接分到不同的连接池甚至不同的实例,一类出问题不至于拖垮全部。这个改造成本不高,但收益很明显,尤其是实例规模上去之后。

最后分享一个我自己常用的小习惯:每次调整完缓冲区相关参数,隔一天回看一次info clients和info memory,对比调整前后的峰值变化。参数调整的效果不是立刻能看出来的,要经过一个完整的业务高峰才能验证。只调不看,等于没调。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询