我不会起名字322· 后端 / 算法 / 数据库
📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL
文章目录
- Redis 内存还剩 20G 却写入报错:maxmemory 之外的 3 类隐性占用
- 一、先对齐口径:Redis 的内存不是一个数
- 二、3 类隐性占用
- 类别 1:客户端输出缓冲区(最常见的一个)
- 类别 2:复制相关缓冲(积压区 + 从节点缓冲)
- 类别 3:内存碎片与分配器不归还
- 三、命令清单
- 四、配置模板
- 五、4 步定位顺序
- 六、总结
Redis 内存还剩 20G 却写入报错:maxmemory 之外的 3 类隐性占用
凌晨收到告警:一批写入接口开始报OOM command not allowed when used memory > 'maxmemory'。
登上机器一看,INFO memory显示used_memory是 18G,而maxmemory设的是 20G——明明还剩 2G 可用,写入却被拒了。更奇怪的是内存曲线:过去一小时业务数据量几乎没变,但 Redis 进程的实际占用(used_memory_rss)从 20G 一路涨到了 26G。
这个问题的答案是:maxmemory管的只是「数据」那一块内存,而 Redis 进程还背着三块不被 maxmemory 计入的隐性占用。这篇文章把这三类拆开,给出 4 步定位命令,以及一份能直接抄的内存配置模板。在 Redis 的性能优化清单里,内存口径是最容易被忽略的一环。
一、先对齐口径:Redis 的内存不是一个数
INFO memory里有一堆used_memory_*,先弄清谁是谁:
| 字段 | 含义 | 是否受maxmemory限制 |
|---|---|---|
used_memory | 分配器(jemalloc)视角:数据 + 缓冲 + 部分内部开销 | ✅ 是(超过就触发淘汰/拒写) |
used_memory_rss | 操作系统视角:进程实际占用的物理内存 | ❌ 否 |
used_memory_peak | 历史峰值 | — |
mem_fragmentation_ratio | rss / used_memory,碎片率 | — |
maxmemory | 你设的上限,0 = 不限 | — |
mem_clients_normal | 普通客户端的输入/输出缓冲 | ❌ 否(Redis 7 起单独统计) |
mem_clients_slaves | 从节点客户端缓冲 | ❌ 否 |
mem_aof_buffer | AOF 缓冲 | ❌ 否 |
mem_replication_backlog | 复制积压缓冲区 | ❌ 否 |
关键的两条:
maxmemory限制的是used_memory,不是进程占用的全部内存。官方也明确说过:如果你有 10G 内存,最好把maxmemory设成 8G 左右——要给缓冲和碎片留余量。used_memory里虽然包含了一部分缓冲,但超限判断发生在「写入数据时」,而很多隐性占用是在写入之后才涨起来的。所以会出现「刚才还能写、现在突然拒写」的假象。
健康状态下的碎片率在1.03 左右;超过1.5就说明碎片比较严重了;小于 1更危险,意味着 Redis 用了 swap(虚拟内存),性能会断崖式下跌。
二、3 类隐性占用
类别 1:客户端输出缓冲区(最常见的一个)
Redis 给每个连接分配输出缓冲区,用来暂存「要发给客户端但对方还没收走」的数据。如果客户端读得慢(或者干脆不读),这个缓冲区会一直堆积。
三种典型来源:
- 慢客户端:某个订阅者、批处理任务消费太慢;
- 从节点连接:主节点要给从节点发复制流,从节点处理慢就会堆积(
mem_clients_slaves); - Pub/Sub 消费者:订阅了频道但不消费,Pub/Sub 的输出缓冲是无上限保护里的重点(默认
pubsub32mb 8mb 60)。
Redis 提供了client-output-buffer-limit三档限制:
# 格式:<class> <hard limit> <soft limit> <soft seconds> client-output-buffer-limit normal 0 0 0 # 普通客户端:默认不限制 ⚠ client-output-buffer-limit replica 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60注意normal默认是0 0 0(不限制)——这意味着一个卡死的普通客户端理论上能把内存吃干。生产上建议给normal也设一个硬上限。
类别 2:复制相关缓冲(积压区 + 从节点缓冲)
只要开了主从,就有两块固定的额外开销:
| 缓冲 | 配置 | 说明 |
|---|---|---|
| 复制积压缓冲区 | repl-backlog-size(默认 1mb) | 主节点上保存最近写命令的环形区,断连重连时做部分同步用。从节点多、写入量大时要调大 |
| 从节点输出缓冲 | client-output-buffer-limit replica | 每个从节点一份,从节点多就线性增长 |
一个容易踩的场景:从节点数量 × 写入流量突然放大(比如扩容从节点、或者从节点网络抖动),主节点的复制缓冲会在几分钟内吃掉几个 G,而这些完全不算在业务数据量里。
类别 3:内存碎片与分配器不归还
jemalloc 按固定大小的 bin 分配内存,删掉的 key 留下的空洞不一定能立刻被复用,也不一定马上还给操作系统。表现就是:
used_memory降了(数据删了),但used_memory_rss不降 →mem_fragmentation_ratio升高;- 频繁写入不同大小的 value(尤其是大 value 频繁更新)会让碎片快速累积。
Redis 4.0+ 有自动碎片整理:
activedefrag yes active-defrag-ignore-bytes 100mb # 碎片超过 100MB 才开始整理 active-defrag-threshold-lower 10 # 碎片率下限 10% active-defrag-threshold-upper 100 # 碎片率上限 100%,越高整理越激进手动触发一次(jemalloc 归还内存给 OS,4.0+ 需要编译时支持):
MEMORY PURGE碎片整理会消耗 CPU,建议在低峰开启并根据实际碎片率调阈值,不要照抄。
三、命令清单
| 命令 | 作用 | 看什么 |
|---|---|---|
redis-cli INFO memory | grep -E "used_memory|maxmemory|fragmentation|mem_" | 内存全景 | 对比used_memory/rss/maxmemory |
redis-cli INFO clients | 客户端概览 | connected_clients、client_recent_max_input_buffer |
redis-cli CLIENT LIST | 逐个客户端 | obl(输出缓冲长度)omem(输出缓冲内存)idle |
redis-cli CLIENT LIST | awk '{print $NF}' | sort | uniq -c | 按客户端归类 | 快速找异常来源 |
redis-cli INFO replication | 主从状态 | 从节点数量、master_repl_offset差 |
redis-cli --bigkeys | 扫大 key | 找出占空间的 key 类型 |
redis-cli --memkeys | 按内存扫(Redis 6.2+,需 LFU/LRU) | 直接找最吃内存的 key |
redis-cli MEMORY USAGE <key> | 单 key 内存 | 精确核算 |
redis-cli MEMORY STATS | 内存细账 | 各部分的占比(含clients.normal、replication.backlog等) |
redis-cli MEMORY DOCTOR | 官方诊断建议 | 会直接提示碎片/缓冲问题 |
redis-cli INFO stats | grep -E "evicted_keys|keyspace" | 淘汰情况 | evicted_keys增长 = 在淘汰 |
redis-cli CONFIG GET maxmemory-policy | 淘汰策略 | 确认是不是noeviction |
redis-cli INFO persistence | 持久化状态 | AOF/RDB 相关缓冲是否异常 |
redis-cli SLOWLOG GET 10 | 慢查询 | 大 key 操作常常伴随慢查询 |
四、配置模板
# ===== 内存上限:给缓冲和碎片留 20%~25% 余量 ===== # 机器 32G 内存时,Redis 最多用到 24G,maxmemory 设 20G maxmemory 20gb maxmemory-policy allkeys-lru # 纯缓存用 allkeys-lru;有持久数据用 volatile-lru # 注意:noeviction 会直接拒写,缓存场景不要用它 # ===== 客户端输出缓冲:给 normal 也加上限 ===== client-output-buffer-limit normal 256mb 64mb 60 client-output-buffer-limit replica 512mb 128mb 60 client-output-buffer-limit pubsub 32mb 8mb 60 # ===== 复制缓冲 ===== repl-backlog-size 128mb # 默认 1mb 太小,按「峰值写入速率 × 允许断连时长」估算 repl-backlog-ttl 3600 # 从节点断开后积压区保留时间 # ===== 碎片整理(低峰开启,按实际情况调) ===== activedefrag yes active-defrag-ignore-bytes 100mb active-defrag-threshold-lower 10 active-defrag-threshold-upper 100 # 整理占用的 CPU 上限(25/75 表示最低 25%、最高 75%) active-defrag-cycle-min 25 active-defrag-cycle-max 75运行时改配置(不用重启):
redis-cli CONFIG SET maxmemory 20gb redis-cli CONFIG SET client-output-buffer-limit"normal 256mb 64mb 60"redis-cli CONFIG SET repl-backlog-size134217728redis-cli CONFIG REWRITE# ⭐ 别忘了落盘,否则重启就回到旧值五、4 步定位顺序
第 1 步:先分清「是数据涨了」还是「缓冲涨了」。
redis-cli INFO memory|grep-E"^used_memory:|^used_memory_rss:|^maxmemory:|^mem_fragmentation_ratio:"redis-cli MEMORY STATS判断规则:
used_memory接近maxmemory且evicted_keys在涨 →真的是数据问题,去第 2 步;used_memory远小于maxmemory却还在报 OOM → 看MEMORY STATS里的clients.normal、replication.backlog、aof.buffer,缓冲是元凶;mem_fragmentation_ratio> 1.5 → 碎片问题,去第 4 步。
第 2 步:数据侧找大 key。
redis-cli--bigkeys# 每种类型最大的 keyredis-cli--memkeys# Redis 6.2+:按内存占用排序redis-cli MEMORY USAGE user:cache:10086重点看:是不是有本该设 TTL 却没设的 key、是不是有单个几百 MB 的集合、是不是有某个业务在批量写入新前缀。
第 3 步:缓冲侧找客户端。
redis-cli CLIENT LIST关注omem(输出缓冲内存)和idle(空闲秒数)。出现「omem几十 MB 且idle很大」的客户端,基本就是它。确认后按连接杀掉:
redis-cli CLIENT KILL ADDR10.0.0.7:54321 redis-cli CLIENT KILL TYPE normal# 谨慎:会杀掉所有普通客户端如果是从节点导致的,看INFO replication里各从节点的offset差(master_repl_offset - slave_repl_offset),差得越多说明积压越多。
第 4 步:碎片侧处理。
redis-cli INFO memory|grepmem_fragmentation_ratio redis-cli MEMORY PURGE# 尝试把空闲内存还给 OSredis-cli CONFIG SET activedefragyes碎片降不下去的常见原因是大量中等大小的 value 反复更新(比如 1KB~10KB 的 hash 频繁 hset)。这种情况要么改写入模式(批量合并、控制 value 大小),要么接受碎片率并给足内存余量。
六、总结
maxmemory只管used_memory(数据为主),管不住客户端缓冲、复制积压和碎片——所以「还剩 2G」和「写入被拒」可以同时成立。- 3 类隐性占用:① 客户端输出缓冲(尤其是
normal默认不限、pubsub消费者不消费);② 复制相关(积压区 + 每个从节点一份输出缓冲);③ 内存碎片(碎片率 > 1.5 就要处理,< 1 说明用了 swap,更危险)。 - 排查顺序不要反:先看
INFO memory和MEMORY STATS判断方向(数据 / 缓冲 / 碎片),再分别用--bigkeys、CLIENT LIST、MEMORY PURGE定点处理。一上来就FLUSHDB是最糟的做法。 - 配置上留 20%~25% 余量:机器 32G → Redis 上限 24G →
maxmemory设 20G。并给client-output-buffer-limit normal补上限制(默认是 0 0 0)。 CONFIG SET之后必须CONFIG REWRITE,否则重启回退,半夜还会再响一次。
本文命令基于 Redis 6/7 的字段名(Redis 7 起
INFO memory把客户端/复制/AOF 缓冲拆成了mem_clients_normal、mem_clients_slaves、mem_aof_buffer、mem_replication_backlog等独立字段)。老版本(5.x)没有这些细分字段,需要靠MEMORY STATS或used_memory与业务数据量之差来推断。