1. 别被“缓冲区”三个字骗了,它管的远不止缓存
很多人第一次看到“Redis 缓冲区”这个词,脑子里蹦出来的就是缓存。缓存嘛,把热点数据丢进内存,读的时候快一点,就这么简单。但如果你真在生产环境里调过 Redis,就会知道“缓冲区”这三个字背后藏着一整套内存管理的逻辑,它跟缓存是两码事,甚至可以说,缓冲区用不好,缓存再快也白搭。
我这里说的缓冲区,指的是 Redis 在运行过程中,为了协调“生产速度”和“消费速度”不一致而开辟的临时内存区域。最典型的三类:客户端输出缓冲区、主从复制缓冲区、AOF 缓冲区。这三块东西平时不显山不露水,一旦出问题,轻则内存飙升,重则实例被打挂、主从断连、数据丢失。我见过太多团队,Redis 用了一两年,从来没关注过这几个参数,直到某天凌晨被告警叫醒,才发现是某个缓冲区把内存吃光了。
这篇文章适合谁看?如果你只是拿 Redis 当个简单的 key-value 存储,读读写写没出过事,那可以先收藏着;如果你已经开始遇到内存莫名增长、主从延迟、客户端超时、大 key 写入卡顿这些问题,那这篇内容就是给你准备的。我会把三类缓冲区的设计意图、关键参数、计算方式、排查手段全部拆开讲,配上我自己踩过的坑和实测数据,让你看完就能上手调。
核心关键词我先摆出来:客户端输出缓冲区、主从复制缓冲区、AOF 缓冲区、client-output-buffer-limit、repl-backlog、内存溢出排查。这些词你后面会反复看到,它们就是 Redis 缓冲区体系的骨架。
先说一个我自己的真实经历。早些年接手一个项目,Redis 实例内存 8G,平时用量稳定在 3G 左右。有天下午突然开始告警,内存十分钟内冲到 7.8G,然后实例开始拒绝写入。当时第一反应是有人写了大量数据,查了半天没发现异常 key 增长。最后用CLIENT LIST一看,某个客户端的输出缓冲区占了 4 个多 G。原因很简单:那个客户端执行了一个返回百万级元素的命令,但消费端读取速度极慢,数据全堵在输出缓冲区里了。这件事让我彻底明白,缓冲区不是“自动帮你兜底”的,它是有上限的,而且默认配置在很多场景下并不合理。
所以,别把缓冲区当成透明的基础设施。它是 Redis 内存模型里最需要你主动干预的部分之一。下面我按三类缓冲区逐一拆解,每一类都会讲清楚:它为什么存在、关键参数怎么设、出问题了怎么查、我实际用下来什么配置比较稳。
2. 客户端输出缓冲区:最容易被忽视的内存杀手
2.1 它到底是什么,为什么需要限制
客户端输出缓冲区,英文叫 client output buffer,是 Redis 为每个连接的客户端单独维护的一块内存区域。它的作用很直接:当 Redis 执行完一个命令,准备把结果返回给客户端时,如果网络发送速度跟不上数据生成速度,结果就会先堆在这块缓冲区里,等网络有空了再慢慢发。
你可以把它想象成快递站的暂存区。快递员(Redis)把包裹(命令结果)放到暂存区,配送员(网络)再一件件送出去。如果配送员速度慢,暂存区就会越堆越满。Redis 默认对普通客户端不限制这块区域的大小,这就意味着,只要客户端读得够慢,缓冲区就能一直涨,直到把实例内存吃光。
这里有个关键点很多人不知道:输出缓冲区的大小不计入maxmemory的限制。也就是说,你设了maxmemory 4gb,Redis 用到 4G 就开始淘汰数据,但输出缓冲区可以在这 4G 之外继续吃内存。这是 Redis 内存管理里一个非常反直觉的设计,也是很多内存溢出事故的根源。
2.2 三类客户端,三套限制规则
Redis 把客户端分成三类,每类有独立的缓冲区限制参数,配置项叫client-output-buffer-limit:
| 客户端类型 | 配置格式 | 默认值 | 典型场景 |
|---|---|---|---|
| normal | client-output-buffer-limit normal 0 0 0 | 不限制 | 普通业务连接 |
| slave | client-output-buffer-limit slave 256mb 64mb 60 | 256MB 硬限 / 64MB 软限 60秒 | 主从复制连接 |
| pubsub | client-output-buffer-limit pubsub 32mb 8mb 60 | 32MB 硬限 / 8MB 软限 60秒 | 发布订阅连接 |
三个数字的含义分别是:硬限制、软限制、软限制持续时间。硬限制是“一旦超过立刻断开”,软限制是“超过后持续 N 秒才断开”。normal 类型默认全是 0,意思是不限制,这是最危险的默认值。
我个人的建议是,normal 类型一定要设限制,哪怕设大一点。比如client-output-buffer-limit normal 1gb 256mb 60,至少保证单个客户端不会把整个实例拖垮。具体设多少,取决于你的业务里最大的返回结果有多大。如果你有命令会返回几十万元素的集合,那硬限制就得往上调,否则正常业务也会被误杀。
2.3 怎么查、怎么算、怎么调
排查输出缓冲区问题,最常用的命令是CLIENT LIST,它会列出所有连接的详细信息,其中omem字段就是该客户端的输出缓冲区占用字节数。你可以用下面这行命令快速找出占用最高的几个连接:
redis-cli CLIENT LIST | awk '{for(i=1;i<=NF;i++){if($i ~ /^omem=/){split($i,a,"="); print a[2], $0}}}' | sort -rn | head -20这条命令会把所有客户端的 omem 值提取出来,按从大到小排序,取前 20 个。实际用下来,大部分时候你只需要关注前几个就够了,因为输出缓冲区的问题往往是少数几个连接造成的。
那硬限制到底设多少合适?我给你一个计算思路。假设你的业务里,最大的返回结果是 10 万个元素的列表,每个元素平均 50 字节,那么单次返回大约是 5MB。考虑到网络抖动和客户端消费延迟,缓冲区可能会堆积多个命令的结果,保守估计乘以 20 倍,就是 100MB。所以硬限制设 200MB 到 500MB 是比较安全的区间。如果你实在拿不准,先设 1GB,观察一段时间omem的峰值,再往下调。
注意:调小
client-output-buffer-limit之后,超过限制的客户端会被强制断开。如果你的客户端没有重连机制,业务会直接报错。所以调整前一定要确认客户端有断线重连和重试逻辑。
还有一个坑我踩过:CLIENT LIST里的omem是实时值,但输出缓冲区的增长可能非常快。我曾经遇到过一个场景,从 omem 正常到实例被打挂,中间只有不到 30 秒。所以如果你怀疑某个客户端有问题,不要只看一次,要连续采样。可以用watch -n 1 'redis-cli CLIENT LIST | grep 客户端地址'这种方式盯着看。
3. 主从复制缓冲区:主从延迟的隐形推手
3.1 复制缓冲区的两个组成部分
主从复制缓冲区其实包含两块:复制积压缓冲区(repl-backlog)和从库输出缓冲区(slave output buffer)。这两块经常被混为一谈,但作用完全不同。
复制积压缓冲区是主库上的一块固定大小的环形缓冲区,用来保存最近发送的写命令。它的作用是支持部分重同步。当从库短暂断连后重新连上,如果断连期间丢失的数据还在积压缓冲区里,就可以只同步这部分增量,而不用做全量同步。这个缓冲区的大小由repl-backlog-size控制,默认是 1MB。
从库输出缓冲区则是主库为每个从库连接单独维护的发送缓冲区,作用跟客户端输出缓冲区类似:主库生成写命令的速度如果超过网络发送速度,数据就先堆在这里。它的限制参数就是前面提到的client-output-buffer-limit slave。
3.2 repl-backlog-size 到底该设多大
repl-backlog-size默认 1MB,这个值在写入量稍大的场景下根本不够用。一旦从库断连时间稍长,积压缓冲区里的数据被覆盖,从库重连后就只能做全量同步。全量同步的代价有多大?主库要 fork 子进程生成 RDB 文件,然后传输整个数据集,从库要清空自身数据再加载。对于几十 G 的实例,这个过程可能持续几分钟甚至更久,期间从库不可用,主库也有 fork 带来的延迟抖动。
那设多大合适?我给你一个计算公式:
repl-backlog-size = 平均写入速率(字节/秒) × 预计最大断连时间(秒) × 安全系数举个例子,你的主库平均每秒写入 2MB 数据,你希望从库断连 60 秒内都能做部分重同步,安全系数取 2,那么:
2MB/s × 60s × 2 = 240MB所以repl-backlog-size设 256MB 比较合适。当然,这个缓冲区是主库内存里实实在在占用的,设太大也会浪费内存。我一般建议至少设 64MB,写入量大的场景设到 256MB 或 512MB。
提示:
repl-backlog-size可以在线修改,用CONFIG SET repl-backlog-size 256mb即可。但修改后只对新建立的复制连接生效,已有的连接需要重连才会使用新大小。
3.3 从库输出缓冲区爆掉会怎样
从库输出缓冲区爆掉,后果比普通客户端严重得多。因为client-output-buffer-limit slave默认是 256MB 硬限,一旦超过,主库会强制断开与从库的连接。从库发现连接断了,会尝试重连,重连后如果积压缓冲区不够,就触发全量同步。全量同步又可能再次撑爆输出缓冲区,形成恶性循环。
我见过最惨的一个案例:某业务主库写入量很大,从库网络带宽不足,输出缓冲区频繁爆掉,导致从库反复全量同步。每次全量同步期间,主库 fork 导致延迟飙升,业务方以为是 Redis 性能问题,查了两天才定位到是复制缓冲区配置不合理。
排查这个问题,主库上执行INFO replication,看slave0那一行的offset和lag,以及master_repl_offset的差值。如果 lag 持续增大,说明从库消费跟不上。再结合CLIENT LIST里 type 为 slave 的连接的 omem 值,就能确认是不是输出缓冲区堆积了。
调整思路有两个方向:一是加大 slave 输出缓冲区限制,给网络更多缓冲时间;二是优化从库消费能力,比如提升网络带宽、减少从库上的其他负载。前者是治标,后者是治本。我一般建议先加大限制稳住局面,再排查从库为什么消费慢。
4. AOF 缓冲区:数据安全与性能的平衡点
4.1 AOF 缓冲区的写入流程
AOF 缓冲区是 Redis 在执行写命令时,先把命令追加到的一块内存区域。它的存在是为了避免每次写命令都直接落盘,那样性能会差到无法接受。AOF 缓冲区的内容会按照appendfsync策略,定期或实时刷到磁盘上的 AOF 文件里。
appendfsync有三个取值:
always:每条写命令都立即 fsync 到磁盘。最安全,性能最差。everysec:每秒 fsync 一次。折中方案,默认值,最多丢 1 秒数据。no:由操作系统决定什么时候 fsync。性能最好,但断电可能丢较多数据。
AOF 缓冲区本身没有独立的大小限制参数,但它的大小会直接影响 fsync 的耗时。如果缓冲区堆积太多数据,一次 fsync 要写入的量就很大,可能造成主线程阻塞。
4.2 AOF 重写期间的缓冲区膨胀
AOF 重写是另一个容易出问题的环节。重写期间,主进程还在处理写命令,这些新命令会同时写入旧 AOF 缓冲区和重写缓冲区。重写缓冲区的作用是:等子进程生成完新的 AOF 文件后,把重写期间产生的增量命令追加进去,保证数据不丢。
问题在于,如果重写过程耗时很长,重写缓冲区就会持续膨胀。对于写入量大的实例,重写期间重写缓冲区涨到几个 G 都有可能。这块内存同样不计入maxmemory,所以很容易被忽视。
我实测过一个场景:一个写入量约 5MB/s 的实例,AOF 重写耗时 40 秒,重写缓冲区峰值达到 200MB 左右。如果写入量再大一些,或者重写耗时更长,这个数字会成倍增长。
控制重写缓冲区膨胀的手段有几个:一是调大auto-aof-rewrite-percentage,减少重写频率;二是在业务低峰期手动触发重写;三是使用no-appendfsync-on-rewrite yes,在重写期间不 fsync,减少阻塞,但代价是重写期间断电可能丢数据。
注意:
no-appendfsync-on-rewrite yes这个配置要慎用。它确实能减少重写期间的主线程阻塞,但如果重写期间机器断电,AOF 缓冲区里没 fsync 的数据就丢了。如果你的业务对数据安全要求高,不要开这个选项。
4.3 AOF 缓冲区相关的排查命令
查看 AOF 状态用INFO persistence,重点关注这几个字段:
aof_current_size:当前 AOF 文件大小aof_base_size:上次重写后的基础大小aof_pending_rewrite:是否有重写任务在等待aof_buffer_length:AOF 缓冲区当前长度aof_rewrite_buffer_length:重写缓冲区当前长度
如果aof_rewrite_buffer_length持续增长,说明重写期间写入量很大,或者重写耗时太长。这时候可以考虑调整重写触发条件,或者检查磁盘 IO 是否有瓶颈。
5. 缓冲区问题排查速查表与实战经验
5.1 常见问题与对应排查手段
| 现象 | 可能原因 | 排查命令 | 处理方向 |
|---|---|---|---|
| 内存突然飙升 | 客户端输出缓冲区堆积 | CLIENT LIST看 omem | 调小 normal 限制,排查慢客户端 |
| 从库频繁全量同步 | repl-backlog 太小 | INFO replication看 offset 差值 | 调大 repl-backlog-size |
| 主从延迟持续增大 | 从库输出缓冲区堆积 | CLIENT LIST看 slave 连接 omem | 加大 slave 限制,优化从库消费 |
| 写命令延迟抖动 | AOF fsync 阻塞 | INFO persistence看 buffer 长度 | 调整 appendfsync 策略 |
| 重写期间内存暴涨 | 重写缓冲区膨胀 | INFO persistence看 rewrite buffer | 低峰期重写,调大重写触发阈值 |
5.2 我踩过的三个坑
第一个坑是normal 客户端不设限制。前面讲过那个 4G 输出缓冲区的案例,后来我把所有实例的 normal 限制都加上了,硬限 1GB,软限 256MB 持续 60 秒。加完之后,再也没有出现过单个客户端拖垮实例的情况。
第二个坑是repl-backlog-size 用默认值。有个项目从库偶尔网络抖动,每次抖动都触发全量同步,主库 fork 频繁,业务延迟毛刺不断。后来把 repl-backlog-size 从 1MB 调到 128MB,全量同步次数从每天几次降到几乎为零。
第三个坑是AOF 重写期间没关注重写缓冲区。有次大促前压测,写入量上来之后 AOF 重写频繁触发,重写缓冲区涨到 1.5G,加上数据集本身 6G,直接把 8G 的实例打爆了。后来调整了auto-aof-rewrite-percentage和auto-aof-rewrite-min-size,让重写不那么频繁,同时在低峰期手动触发重写,问题才解决。
5.3 几个实用的监控建议
缓冲区问题最好的处理方式是提前发现,而不是等出事了再查。我一般会在监控系统里加这几个指标:
- 每个实例的
client_output_buffer总量,按 normal、slave、pubsub 分类 repl-backlog的使用率aof_rewrite_buffer_length的峰值mem_fragmentation_ratio,内存碎片率高往往伴随缓冲区问题
这些指标不需要多精确,但要有趋势图。一旦发现某个指标持续上涨,就能在问题爆发前介入。
6. 参数配置模板与调优思路
6.1 一套相对稳妥的配置模板
下面这套配置是我在多个生产环境用下来比较稳的,你可以根据自己的实际情况调整:
# 客户端输出缓冲区限制 client-output-buffer-limit normal 1gb 256mb 60 client-output-buffer-limit slave 512mb 128mb 120 client-output-buffer-limit pubsub 64mb 16mb 60 # 复制积压缓冲区 repl-backlog-size 256mb repl-backlog-ttl 3600 # AOF 相关 appendonly yes appendfsync everysec auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 4gb no-appendfsync-on-rewrite no这套配置的核心思路是:给足缓冲空间,但不无限放任。normal 硬限 1GB 是为了防止单个客户端失控;slave 硬限 512MB 是为了给网络波动留足余量;repl-backlog 256MB 是为了覆盖大多数断连场景;AOF 重写阈值调高是为了减少重写频率。
6.2 调优时的取舍逻辑
调优缓冲区参数,本质上是在内存占用和稳定性之间找平衡。限制设得太小,正常业务可能被误杀;设得太大,又失去了保护意义。我的经验是:
- 先观察一周的峰值数据,再定限制值
- 限制值至少是峰值的 2 到 3 倍
- 软限制比硬限制更重要,因为它给了缓冲时间
- 任何参数调整都要有回滚方案
还有一点很重要:不同角色的实例,配置应该不同。主库和从库的 slave 输出缓冲区压力不一样,缓存实例和持久化实例的 AOF 压力也不一样。不要一套配置打天下,要根据实例的实际角色和负载来定。
6.3 最后的经验之谈
Redis 缓冲区这个东西,平时不出事的时候你感觉不到它的存在,一出事就是大事。我的建议是,不管你现在的实例看起来多稳定,都花半个小时检查一下这几个参数:client-output-buffer-limit三个类型是否都设了合理值,repl-backlog-size是否够用,AOF 重写频率是否过高。这几个检查做完,能排除掉大部分缓冲区相关的隐患。
另外,缓冲区问题往往不是孤立的,它背后通常反映的是消费能力不足或网络瓶颈。调参数只是缓解症状,真正要解决,还是得从架构和容量规划入手。比如客户端读得慢,是不是该考虑用 pipeline 或者异步消费;从库延迟大,是不是该加带宽或者拆分复制链路。这些才是治本的方向。
我个人在实际操作中的体会是,Redis 的很多默认配置是为了“能用”,而不是为了“好用”。缓冲区相关的参数尤其如此。你多花一点时间理解它们,就能少熬几个凌晨处理告警的夜。这笔账,怎么算都划算。