大家好,我是小富。
《十万个why》系列持续更新中
线上 Redis 内存告警了,紧急排查发现有两百万个过期的临时 key 没被清理。赶紧写了个脚本用 UNLINK 批量删了。删完一看 INFO memory:
used_memory_human: 5.2GB # 之前是 5.3GB,只降了 100MB?
used_memory_rss_human: 6.8GB # RSS 压根没变
两百万个 key 删了,内存才降了 100MB,操作系统级别的 RSS 更是纹丝不动。这两百万 key 总共占了大几百 MB,钱呢?
第一层原因:jemalloc 不会立刻归还内存给操作系统
Redis 默认使用 jemalloc 作为内存分配器。jemalloc 的工作方式是:向操作系统申请大块内存,自己切成小块分配给 Redis 使用。当 Redis 释放一个 key 时,jemalloc 把这块内存标记为"空闲",放回自己的空闲池,但并不把它还给操作系统。
为什么不还?因为还了之后下次又要重新申请。内存的申请和释放(mmap/munmap 系统调用)是很昂贵的操作。jemalloc 认为你既然用过 6.8GB,以后大概率还会用到,不如留着下次直接分配。
所以 used_memory 降了(Redis 自己知道释放了内存),但 used_memory_rss 不降(操作系统没收回内存)。
这就像你搬出了一间公寓,物业把房间标记为"空置",但大楼并没有拆掉那间房。空间还是被占着的。
第二层原因:内存碎片
即使 jemalloc 知道这些内存空闲了,也不一定能高效复用。因为存在碎片问题。
假设你删了大量 64 字节的 key,现在空闲池里有一堆 64 字节的碎片块。如果接下来要存一个 512 字节的新 key,这些 64 字节的块用不上,jemalloc 只能再申请新的大块。
空闲内存明明有,但因为不连续,用不了。
查看碎片率:
redis-cli INFO memory | grep mem_fragmentation_ratio
# mem_fragmentation_ratio: 1.31
碎片率 1.31 意味着操作系统实际分配的内存是 Redis 有效使用量的 1.31 倍。如果 used_memory 是 5.2GB,那真实占用约 5.2 × 1.31 = 6.8GB。多出来的 1.6GB 全是碎片。
理想的碎片率在 1.0 ~ 1.05 之间。超过 1.5 就需要处理了。
第三层原因:你删的那些 key 太"碎"了
两百万个小 key(每个几十到几百字节)均匀分布在 Redis 的内存空间里。删除它们后,留下的是两百万个分散的"空洞"。
这跟删几个大 key 完全不同。如果你删了 10 个各 50MB 的大 key,释放的内存是连续的大块,很容易被复用。但两百万个小 key 释放的是两百万个碎片,复用效率极低。
那内存什么时候能真正降下来?
情况一:新数据自然填充碎片
如果 Redis 持续有新的写入,jemalloc 会优先使用空闲池里的碎片块来分配新 key。随着时间推移,碎片被新数据填满,used_memory 会重新上升,但 used_memory_rss 不会涨——因为实际没有向操作系统申请新内存。
这是最自然的恢复方式,但不可控。
情况二:开启自动碎片整理(Redis 4.0+)
# redis.conf
activedefrag yes
active-defrag-ignore-bytes 100mb # 碎片超过 100MB 才开始整理
active-defrag-threshold-lower 10 # 碎片率超过 10% 才整理
active-defrag-threshold-upper 100 # 碎片率超过 100% 时全力整理
active-defrag-cycle-min 1 # 整理占 CPU 最小百分比
active-defrag-cycle-max 25 # 整理占 CPU 最大百分比
自动碎片整理会在后台把分散的数据重新紧凑排列,释放出连续的内存块。但这个过程消耗 CPU,且整理期间可能轻微影响 Redis 的响应延迟(通常在微秒级)。
情况三:重启 Redis
这是最简单粗暴但也最有效的方式。重启后 Redis 从 RDB/AOF 重新加载数据,内存会被紧凑地重新分配,碎片率降到接近 1.0。
如果你有主从架构,可以先重启从节点,等从节点同步完毕后做一次主从切换,再重启原主节点。对外无损。
情况四:手动触发 MEMORY PURGE
redis-cli MEMORY PURGE
这个命令会让 jemalloc 尝试把空闲内存归还给操作系统。但效果有限——它只能释放 jemalloc 内部的空闲大块,碎片化严重时效果不大。
怎么预防
| 措施 | 效果 |
|---|---|
| 避免大量小 key 短期内集中过期和创建 | 减少碎片产生 |
| 使用 Hash/Set 等容器类型代替大量独立 key | 内存更紧凑,碎片更少 |
开启 activedefrag | 自动整理碎片 |
| 给小 key 合理设计 key 大小,避免大小差异过大 | jemalloc 的分配粒度更匹配 |
监控 mem_fragmentation_ratio | 超过 1.5 就要预警 |
总结
| 你的期望 | 实际情况 | 原因 |
|---|---|---|
| 删了 key,used_memory 降 | 是的,降了 | Redis 知道释放了 |
| 删了 key,RSS 也降 | 不降 | jemalloc 不归还内存给 OS |
| 空闲内存能被新 key 复用 | 不一定 | 碎片导致空闲块太碎,无法被大请求使用 |
| 删大量小 key 效果 | 效果很差 | 产生大量碎片空洞 |
| 删少量大 key 效果 | 效果好 | 释放的是连续大块内存 |
Redis 的内存管理不是"用了就占、删了就还"的简单模型,中间隔着一个内存分配器(jemalloc)。 分配器为了效率会持有内存不释放,加上碎片问题,就导致了"删了 key 但内存不降"的现象。
理解这个机制后,你就知道为什么 DBA 经常说"Redis 的内存只涨不降"——它不是不降,是需要特定条件才会降。
我是小富,下期见。
