Redis 内存告警排查:从 maxmemory-policy 到 key 命名空间

举报
yd_282015185 发表于 2026/11/03 08:57:37 2026/11/03
【摘要】 内存告警别急着扩容。策略配错、大 key、TTL 泄漏这三种根因,扩到 32G 也治不好。本文按 INFO memory 六个字段、六种淘汰策略、前缀聚合三步排查,附八条可直接抄的命令和一份决策表。

## 一、告警响起来时,先别急着扩容


Redis 内存告警是最容易「加内存了事」的一类问题。但加内存通常只买到两三天时间,因为真正常见的三种根因都不在容量上:


1. **淘汰策略配错**,数据根本不淘汰,内存只涨不跌

2. **大 key / 热 key**,少数几个 key 吃掉了大半内存

3. **key 泄漏**,代码里写了没有过期时间的 key,长期堆积


这三种情况下,扩到 32G 也会在某个时点再次打满。所以第一步不是加内存,而是把下面六个数字拿到手。


## 二、INFO memory 里只看六个字段


```

redis-cli INFO memory

```


输出很长,但有六个字段决定了后续方向:


```

used_memory: 8589934592

used_memory_rss: 12884901888

used_memory_peak: 9126805504

mem_fragmentation_ratio: 1.50

maxmemory: 8589934592

maxmemory_policy: noeviction

```


逐个解读:


**used_memory** —— Redis 自己统计的分配量,包含数据、内部结构、复制缓冲区。这是 Redis 认为「我用了多少」。


**used_memory_rss** —— 操作系统看到的进程实际占用(RSS)。包含内存碎片、分配器开销。


**mem_fragmentation_ratio** —— 前两者的比值。`1.0~1.5` 正常,`> 1.5` 说明碎片较多,`< 1` 说明部分内存被换出到 swap(这种情况性能会断崖下跌,必须立刻处理)。


**used_memory_peak** —— 历史峰值。如果 peak 远大于当前值,说明曾经有过一次流量尖峰,之后数据被淘汰掉了;如果 peak ≈ used_memory,说明内存是一路涨上来的,基本可以判定为泄漏。


**maxmemory** —— 上限。**如果这个值是 0,说明没有设置上限**,Redis 会一直吃到系统 OOM 为止。这是最常见的一类配置事故。


**maxmemory_policy** —— 淘汰策略。下一节专门讲。


拿到这六个值,基本就能分出三条路:


| 特征 | 判断 | 方向 |

|---|---|---|

| `maxmemory = 0` | 没设上限 | 立刻设上限 + 策略 |

| `peak ≈ used_memory` 且持续上涨 | 泄漏或没有淘汰 | 查策略与 key 的 TTL |

| `fragmentation_ratio > 1.5` | 碎片 | 开 activedefrag 或迁移重建 |

| `fragmentation_ratio < 1` | 发生 swap | 调 vm.swappiness、加内存 |

| `maxmemory_policy = noeviction` | 不淘汰 | 见下节 |


## 三、maxmemory-policy 的六种策略,选错就是事故


Redis 提供六种策略,本质是两组正交的选择:**淘汰范围**(全部 key / 仅带 TTL 的 key)和 **淘汰依据**(LRU / LFU / TTL / 随机)。


| 策略 | 淘汰范围 | 依据 |

|---|---|---|

| `noeviction` | 不淘汰,写入报 OOM 错误 | — |

| `allkeys-lru` | 所有 key | 最久未使用 |

| `allkeys-lfu` | 所有 key | 访问频率最低 |

| `allkeys-random` | 所有 key | 随机 |

| `volatile-lru` | 仅设置了 TTL 的 key | 最久未使用 |

| `volatile-ttl` | 仅设置了 TTL 的 key | 剩余 TTL 最短 |


三个高频踩坑:


**坑一:`volatile-lru` 遇上「大部分 key 没有 TTL」。**


这是最典型的线上事故。团队为了「只淘汰缓存、别动持久数据」,选了 `volatile-lru`。听起来很合理,但结果是:内存打满时,只有在 TTL 的 key 会被淘汰。如果 80% 的内存被没有 TTL 的 key 占着,那么剩下 20% 的 key 会被反复淘汰、反复回源,缓存命中率掉到个位数,同时内存仍然打满,写入开始报 `OOM command not allowed when used memory > 'maxmemory'`。


排查办法很简单,看有多少 key 带 TTL:


```

redis-cli INFO keyspace

```


输出:


```

db0:keys=1834521,expires=31204,avg_ttl=0

```


`keys=1834521`、`expires=31204`,意味着只有 1.7% 的 key 有过期时间,其余 98.3% 是「永久的」。这种情况下 `volatile-*` 系列策略等于没生效,必须改用 `allkeys-lru` 或 `allkeys-lfu`。


**坑二:`allkeys-lru` 把不该淘汰的数据淘汰了。**


如果这个 Redis 里既有缓存又有业务数据(比如队列、计数器、分布式锁),`allkeys-lru` 会把业务数据也当成缓存淘汰掉,表现为「锁突然失效」「计数器归零」。


正确做法是**实例隔离**:缓存和数据分不同实例,缓存实例用 `allkeys-lfu`,数据实例用 `noeviction` 并把上限设得足够高、配合监控告警。混用是长期隐患。


**坑三:LRU 不是真 LRU。**


Redis 的 LRU 是近似算法:默认采样 5 个 key(`maxmemory-samples`),淘汰其中最久未使用的。采样数越大越接近真 LRU,但 CPU 开销也越大。生产上 `10` 是个不错的折中:


```

maxmemory-samples 10

```


同理,LFU 的计数器有衰减机制(`lfu-decay-time`),默认 1 分钟衰减一次。如果业务有明显的高低峰,`allkeys-lfu` 通常比 `allkeys-lru` 命中率高,因为它保留的是「访问频率高的」而不是「最近访问过的」,后者容易被一次全量扫描污染。


## 四、设了 maxmemory 为什么还会 OOM


这是让人困惑的现象:明明设了 8G 上限,进程却因为 RSS 超过 8G 被系统杀掉。原因是 `maxmemory` 只管**数据集**,下面几块内存不在它的管辖范围内:


**主从复制缓冲区**。每个 replica 有一个 `repl-backlog`,加上 `client-output-buffer-limit replica` 控制的待发送缓冲。全量同步期间,这个缓冲可能涨到几个 G。它不计入 maxmemory,但计入 RSS。


**AOF 重写缓冲区**。`aof_rewrite_buf` 在重写期间把新写入攒起来,重写完成后一次性追加。写入量大时这块能到几百 M 甚至上 G。


**fork 的 Copy-On-Write**。做 RDB 快照或 AOF 重写时 Redis 会 fork 子进程,子进程与父进程共享内存页,但父进程一旦写入就触发 COW,页被复制。最坏情况下 RSS 翻倍。这就是「为什么每天凌晨备份时内存会翻倍」的原因。


检查这几块:


```

redis-cli INFO clients

redis-cli INFO persistence

redis-cli INFO replication

```


`persistence` 里的 `rdb_bgsave_in_progress`、`aof_rewrite_in_progress` 能告诉你当时是否在 fork;`replication` 里的 `slave0:...,offset=...` 和 `client_output_buffer` 相关指标能看出复制缓冲压力。


**实操建议**:容器 limit 或机器内存,按 `maxmemory × 2` 预留。如果内存吃紧,用 `vm.overcommit_memory=1` 并确保有 swap 兜底,但要注意 swap 会让性能崩掉,只适合作为「避免被杀」的最后一道保险。


## 五、大 key 和热 key:内存的真正黑洞


内存 8G 里有 5G 是三个 key 贡献的,这种事故每年都会遇到几次。定位手段有四个,成本递增:


**1. `--bigkeys`(离线、低风险)**


```

redis-cli --bigkeys -i 0.1

```


它用 SCAN 遍历,按类型统计最大的 key。`-i 0.1` 表示每 100 条命令休眠 0.1 秒,避免拖慢实例。缺点是只能看出「最大的单个 key」,看不出总量分布。


**2. `--memkeys`(Redis 6+,直接给内存占用)**


```

redis-cli --memkeys

```


比 `--bigkeys` 更准,因为它按实际内存分配统计,而不是按元素个数。


**3. `--hotkeys`(需要 LFU 策略)**


```

redis-cli --hotkeys

```


只有 `maxmemory-policy` 是 `allkeys-lfu` 或 `volatile-lfu` 时才有效,因为它读的是 LFU 计数器。如果实例是 LRU 策略,这个命令会返回空。


**4. 采样估算某个前缀的总量**


要看「哪类 key 占了最多内存」,得自己聚合:


```bash

redis-cli --scan --pattern '*' | awk -F: '{print $1":"$2}' | sort | uniq -c | sort -rn | head -20

```


如果 key 命名规范(`业务:实体:ID`),这条命令能直接告诉你哪类业务在膨胀。注意 `--scan` 本身是安全的全量遍历,但不能保证强一致。


更进一步,可以采样若干 key 用 `MEMORY USAGE` 估算:


```

redis-cli MEMORY USAGE user:profile:10086 SAMPLES 5

```


然后 `平均单 key 内存 × 该类 key 数量` 得到估算总量。这比全量扫要快得多。


## 六、key 泄漏:最常见的「内存只涨不跌」


代码里写了 `SET key value` 却没设过期时间,是最普遍的泄漏源。三种典型写法:


```php

// 错误:没有 TTL,永久驻留

$redis->set("user:session:" . $uid, $data);


// 正确:明确 TTL

$redis->setex("user:session:" . $uid, 1800, $data);

```


```python

# 错误:set 后再 expire 不是原子的,中间崩溃就永久了

r.set(key, val)

r.expire(key, 3600)


# 正确:一条命令搞定

r.set(key, val, ex=3600)

```


第三种是「每次写入 key 名都不同」,比如拼了时间戳:


```python

key = f"stat:{date}:{hour}:{minute}:{second}:{uid}" # 每秒一个 key,永不重复

```


这类 key 数量随时间线性增长,是隐蔽性最强的泄漏。


排查两步走:


**第一,看 keyspace 的 expires 占比**(前面提过)。如果 `expires/keys < 0.9`,而业务上应该是纯缓存,就已经能说明问题。


**第二,按前缀看新增速度**。隔 10 分钟跑两次统计,对比增量:


```bash

redis-cli --scan --pattern 'stat:*' | wc -l

```


两次差值除以时间,就是新增速率。如果某个前缀在稳定增长且没有对应的清理逻辑,就是它。


定位到之后,清理要小心:**绝不能用 `KEYS pattern` 删**,会阻塞主线程。用 `SCAN` 游标分批删:


```bash

redis-cli --scan --pattern 'stat:2026:*' | head -1000 | xargs -r -n 100 redis-cli UNLINK

```


`UNLINK` 是异步删除,把释放内存的操作放到后台线程,不会卡住主线程。Redis 4.0+ 都应该优先用 `UNLINK` 而不是 `DEL`。


## 七、碎片:ratio 高了怎么办


`mem_fragmentation_ratio` 长期高于 1.5,说明分配器(jemalloc)保留了大量空页。原因是数据大小分布变化剧烈,或者做过大量删除。


两种处理方式:


**开启主动碎片整理(Redis 4.0+)**


```

activedefrag yes

active-defrag-ignore-bytes 100mb

active-defrag-threshold-lower 10

active-defrag-threshold-upper 100

```


`threshold-lower 10` 表示碎片率超过 10% 才开始整理,`upper 100` 表示到 100% 就全力整理。整理过程会消耗 CPU,建议先观察 10 分钟再决定是否常开。


**或者迁移重建**:主从切换一次,新实例的内存是紧凑的。适合能接受秒级中断的场景。


判断是否需要处理的一个实用阈值:**碎片浪费的绝对量**。


```

wasted = used_memory_rss - used_memory

```


如果 `wasted` 只有几百 M,ratio 再高也不值得折腾;如果 `wasted` 超过 2G,就值得处理。


## 八、一份可直接抄的排查清单


```bash

# 1. 六个关键字段

redis-cli INFO memory | grep -E "used_memory:|used_memory_rss:|used_memory_peak:|mem_fragmentation_ratio|maxmemory:|maxmemory_policy:"


# 2. 是否设了上限

redis-cli CONFIG GET maxmemory

redis-cli CONFIG GET maxmemory-policy


# 3. TTL 覆盖率

redis-cli INFO keyspace


# 4. 大 key

redis-cli --bigkeys -i 0.1


# 5. 按前缀聚合

redis-cli --scan --pattern '*' | awk -F: '{print $1":"$2}' | sort | uniq -c | sort -rn | head -20


# 6. 单 key 内存

redis-cli MEMORY USAGE <key> SAMPLES 5


# 7. 是否在 fork / 复制压力

redis-cli INFO persistence | grep -E "in_progress|rdb_last"

redis-cli INFO replication


# 8. 慢命令(可能是大 key 扫描造成的)

redis-cli SLOWLOG GET 10

```


第 6 步的 `SAMPLES 5` 很关键:默认 `MEMORY USAGE` 会全量采样一个 key,如果 key 有几百万元素,可能阻塞几百毫秒。给个采样数就安全了。


## 九、容量怎么估才靠谱


很多团队按「key 数量 × 平均大小」估容量,结果差三倍。原因是漏了三块开销:


1. **每个 key 的元数据开销**:一个 key 除了值本身,还有 dictEntry、redisObject、SDS 头,小 key 的情况下这部分能占到 50% 以上。比如 `SET k v`,值只有 1 字节,实际占用约 60 字节。

2. **哈希表扩容的冗余**:Redis 的 dict 扩容是翻倍,装载因子在 0.1~1 之间波动,平均有近一倍冗余。

3. **复制缓冲与 fork COW**:前面讲过,最坏情况翻倍。


实操上,用 `INFO memory` 的 `used_memory_dataset` 除以 `keys`,得到真实的「平均单 key 成本」,再乘以预期 key 数,最后乘 1.5 的余量。这个估算比拍脑袋准得多。


```

平均单 key 成本 = used_memory_dataset / db0.keys

```


一半是浪费一半是现实。尤其是小 key 场景,一定要按实际测出来的值算,不要按「值的大小」算。


## 十、决策表


| 现象 | 判断 | 处理 |

|---|---|---|

| `maxmemory = 0` | 无上限 | 立刻设上限与策略 |

| 写入报 `OOM command not allowed` | 已达上限且不淘汰 | 改策略或扩容,先看 keyspace |

| `expires/keys < 0.5` 且策略是 `volatile-*` | 策略失效 | 改 `allkeys-lru/lfu` |

| `peak ≈ used_memory` 持续上涨 | 泄漏 | 按前缀查增速,补 TTL |

| 少数 key 占大半内存 | 大 key | 拆分或改结构(hash 分桶) |

| `fragmentation_ratio > 1.5` 且浪费 > 2G | 碎片 | `activedefrag` 或迁移重建 |

| `fragmentation_ratio < 1` | 用了 swap | 调 `vm.swappiness`,扩内存 |

| 凌晨内存翻倍 | fork COW | 错峰备份或上从库备份 |

| 淘汰频繁但命中率骤降 | 缓存被反复淘汰 | 扩容或改 LFU,检查是否混用实例 |


## 收尾


Redis 内存问题的排查顺序其实很固定:**先看上限有没有设,再看策略对不对,然后看 key 的 TTL 覆盖率,最后才看大 key 和碎片**。


这个顺序的价值在于成本递增:前两步各花十秒,能解决掉一半的问题;大 key 扫描和前缀聚合要花几分钟;扩容最贵,而且往往治标不治本。


下次告警响的时候,先别打开控制台点扩容,跑一遍 `INFO memory | grep` 那六行,答案通常就在里面。


【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。