used_memory 3.2 GB,RSS 却 6.8 GB:Redis 内存碎片与淘汰策略
Redis 被 OOMKilled,used_memory 只有 3.2 GB。碎片率 2.13、fork 的 COW 放大、volatile-lru 失效三件事叠在一起,activedefrag 与大 key 分片怎么收场。
凌晨四点,K8s 把 Redis 的 Pod 重启了,原因是 OOMKilled。可 info memory 里 used_memory 只有 3.2 GB,容器的 limits.memory 写的是 8Gi,maxmemory 配的是 6gb。三个数字摆在一起怎么看都不该被杀。
值班同事第一反应是「是不是 JVM 那种堆外问题」,但 Redis 没有堆外。我把监控拉长到七天,used_memory 曲线平稳,used_memory_rss 却在三天里从 4.1 GB 爬到 6.9 GB,两条线越走越开。这就是答案的一半:容器看的是进程实际占的物理内存,也就是 RSS,不是 Redis 自己记的账。
剩下的一半更麻烦:maxmemory 是按 used_memory 判定的,maxmemory-policy 也只在 used_memory 逼近上限时才动作。RSS 已经 6.9 GB,used_memory 才 3.2 GB,淘汰策略永远不会被触发,容器却先被内核杀了。
一、先把三个内存口径分清楚
排查这类问题,第一步不是看配置,是搞清楚到底在看哪个数字。Redis 的 INFO memory 里至少有五个字段相关:
redis-cli -h 10.0.3.21 info memory | grep -E "used_memory|maxmemory|fragmentation|allocator|peak|dataset"
| 字段 | 含义 | 当时的实测值 |
|---|---|---|
used_memory |
Redis 分配器统计的逻辑用量 | 3.21 GB |
used_memory_rss |
操作系统看到的进程物理内存 | 6.87 GB |
used_memory_peak |
历史峰值 | 5.42 GB |
mem_fragmentation_ratio |
rss / used | 2.13 |
allocator_frag_ratio |
jemalloc 内部碎片 | 1.06 |
maxmemory |
逻辑用量的上限 | 6 GB |
mem_fragmentation_ratio = 2.13,健康值通常在 1.0 到 1.5 之间。超过 1.5 说明分配器手里攥着大量释放不回去的空洞;小于 1 反而说明发生了 swap,那是另一种灾难。
allocator_frag_ratio 只有 1.06,说明 jemalloc 自身管理得不错,问题不在分配器内部,而在「已归还给分配器、但没有归还给操作系统」这一段。
二、碎片是怎么攒起来的
jemalloc 按 size class 分配,8、16、32、48、64 字节……一直到几 MB。一个 380 MB 的 hash 被删掉之后,内存回到 jemalloc 的空闲页里,但这些页散落在各个 arena 的不同位置,不连续,操作系统收不回去。只要进程里还有活得久的对象占着中间位置,这些空洞就归并不了。
我们的场景有三件事叠加:
第一,每天凌晨有一个全量同步任务,把一批 hash 结构整体重建——先 DEL 再逐条 HSET。DEL 释放了 1.6 GB,HSET 又申请回来,但申请的位置和释放的位置对不上,碎片就留下来了。
第二,key 的 TTL 分布很不均匀。缓存类 key 有 TTL,会话类 key 没有,两类混在同一个 db 里。有 TTL 的反复生灭,没 TTL 的一直钉在原处,等于把内存切成了碎片。
第三,maxmemory-policy 配的是 volatile-lru,只淘汰带 TTL 的 key。当 used_memory 逼近 6 GB 时,能淘汰的空间已经很有限,淘汰效率急剧下降。
顺带说一个常见误区:
mem_fragmentation_ratio高不代表一定要重启。如果碎片已经稳定在某个值不再上涨,重启只是把问题推迟。要看的是趋势,不是绝对值。
三、先把容器从悬崖边拉回来
止血分两步。第一步是让容量口径对齐,第二步才是处理碎片。
容量上我做了三件事:把 maxmemory 从 6gb 降到 4.5gb,给 RSS 留出碎片与 COW 的空间;把 limits.memory 提到 8Gi 不变,但加了一条 reserved-memory 说明,避免有人按 maxmemory 直接换算;把 maxmemory-policy 从 volatile-lru 改成 allkeys-lru,因为库里有一半 key 没有 TTL,volatile 系策略在纯缓存场景反而更容易被撑死。
# redis.conf 关键改动
maxmemory 4608mb
maxmemory-policy allkeys-lru
activedefrag yes
active-defrag-ignore-bytes 200mb
active-defrag-threshold-lower 15
active-defrag-threshold-upper 100
active-defrag-cycle-min 5
active-defrag-cycle-max 60
activedefrag 是 Redis 4.0 引入的主动碎片整理,原理是把活对象搬到连续页上,把空闲页腾出来还给操作系统。参数里最重要的是 threshold-lower:碎片率低于 15% 时它不动手,避免无谓的 CPU 开销;cycle-max 60 表示最忙时占用 60% 的 CPU 周期做整理——这个值在线上偏高,我先设成 40,观察后再调。
开启后观察一天,mem_fragmentation_ratio 从 2.13 降到 1.28,RSS 回落到 4.2 GB。CPU 使用率峰值多了 3 个百分点,可以接受。
四、fork 与持久化会把内存放大一倍
碎片只是慢性病,RSS 突然翻倍还有另一个来源:BGSAVE。fork 出子进程那一刻,父子共享页表,但 Linux 的 COW 机制意味着任何一个页被写,就要复制一份。写入越频繁,复制越多,极端情况下 RSS 接近 used_memory 的两倍。
我们这台实例开了 save 900 1,也就是 15 分钟内有 1 次写就落盘。缓存场景写入极其频繁,等于每 15 分钟就有一次全量 COW 的窗口。
# 看最近一次 fork 的耗时与内存峰值
redis-cli info stats | grep -E "latest_fork_usec|rdb_"
redis-cli info persistence | grep -E "rdb_last_bgsave|rdb_changes"
latest_fork_usec 是 412000,也就是 fork 本身花了 0.4 秒——页面表太大,fork 本身就会造成毛刺。这里有两条路:一是彻底关掉 RDB,只留 AOF everysec;二是换到无盘复制 / 副本节点做持久化。
我们的做法是主节点关 RDB、保留 AOF,把备份交给从节点。改完 latest_fork_usec 只在 AOF 重写时才有波动,主节点 P99 从 3.8ms 降到 1.1ms。
五、真正的根因在数据结构
前面都是缓解。把 380 MB 的大 hash 拆掉之后,碎片问题才从源头消失。
# 1. 找出大 key(抽样,别在高峰期跑全量)
redis-cli --bigkeys -i 0.1
# 2. 精确看某个 key 的内存与编码
redis-cli memory usage order:snapshot:20261110 SAMPLES 0
# 3. 看它内部字段数与平均长度
redis-cli hlen order:snapshot:20261110
redis-cli --memkeys --memkeys-samples 5
这个 hash 存的是订单快照,按「快照批次」做 key,一个批次几十万个字段。改成 order:snap:{batch}:{shard},按 1000 个字段分片,单 key 从 380 MB 降到 400 KB 左右。分片后每次 DEL 释放的都是小页,jemalloc 能立刻归还给系统,碎片率长期稳定在 1.1 附近。
六、监控怎么配才看得见
used_memory 是 Redis 自己报的,容器 OOM 是内核判的,两者必须同时看,否则就会出现「内存没用满却被杀」的错觉。
我们最终定下的四条告警:
mem_fragmentation_ratio > 1.6且持续 30 分钟;used_memory_rss / limits.memory > 0.75,比maxmemory更贴近真实风险;latest_fork_usec > 200000,fork 毛刺的前兆;evicted_keys突增,说明容量真的不够了,该扩容而不是调策略。
小结
- 容器 OOM 看的是 RSS,
maxmemory看的是used_memory,这两个口径不重合,必须留出碎片余量。 mem_fragmentation_ratio在 1.0–1.5 之间算健康,超过 1.5 先看趋势再看是否开activedefrag。volatile-lru只淘汰带 TTL 的 key,纯缓存库里混着无 TTL 的 key 时会失效,改成allkeys-lru更安全。- fork 期间的 COW 会让 RSS 逼近两倍,主节点关掉 RDB、把持久化交给从节点能顺带解决 P99 毛刺。
- 大 key 是碎片的制造机,按业务维度分片到几百 KB 级别,比任何参数调优都有效。
- 告警要盯
rss / limits和evicted_keys,只盯used_memory一定会漏。
正在加载评论…