跳至主要內容

程序员小富大约 5 分钟

大家好,我是小富。

《十万个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 的内存只涨不降"——它不是不降,是需要特定条件才会降。


我是小富,下期见。

上次编辑于: