
17 内存管理——jemalloc 碎片率统计,内存淘汰 8 种策略 + LRU/LFU 近似算法
(接上一篇《16. 内存管理——jemalloc 基础与 arena 划分》)
1. 为什么又要聊「碎片」
上节把 arena、tcache、bin 画成了一张“三级缓存”大图,但那张图里埋了一个伏笔:jemalloc 的「低碎片」只是概率意义上的,不是承诺。
线上 Redis 进程跑了 30 天,RSS 从 8 GB 涨到 12 GB,而 used_memory_human 只有 9 GB,3 GB 差值就是「外部碎片 + 内部碎片 + 未归还页缓存」的混合体。
能不能把这三兄弟拆开?拆开后能不能自动触发内存淘汰,而不是被动等 OOM?本节把「碎片量化 → 淘汰决策 → LRU/LFU 近似」串成一条可落地的闭环。
2. jemalloc 碎片率的三层指标
jemalloc 把「进程级 RSS」与「分配器活跃字节」之间的差异拆成三个可观测指标,全部通过 malloc_stats_print() 输出,也可在代码里直接捞:
| 指标 | 计算公式 | 触发条件 | 典型值 |
|---|---|---|---|
active:dirty | active - allocated | 高并发 + 长生命周期对象 | 5 %–15 % |
retained | retained / allocated | 释放后未归还 OS | 10 %–30 % |
metadata | metadata / allocated | 小对象场景 | 3 %–8 % |
碎片率公式(Redis 源码里叫 mem_fragmentation_ratio):
frag = (zmalloc_get_rss() + zmalloc_get_metadata()) / zmalloc_used()
当 frag > 1.2 时,info memory 里会多出一行 mem_fragmentation_ratio:1.34,同时把 allocator_frag_ratio、allocator_rss_ratio、rss_overhead_ratio 三列打平输出,方便一眼定位是“脏页没归还”还是“元数据膨胀”。
3. 让数字变成决策——8 种淘汰策略全景
Redis 4.0 之后把“内存上限”拆成两层:maxmemory 控制用户数据,maxmemory_allocator(jemalloc 专有)控制分配器层。
当 used_memory > maxmemory 时,触发“数据淘汰”;当 allocator_active > maxmemory_allocator 时,触发“分配器回收”。
前者 8 种策略,后者 3 种策略,合起来 11 种,但线上 99 % 场景只关心下面 8 种:
| 策略名 | 代码宏 | 淘汰粒度 | 近似算法 | 复杂度 |
|---|---|---|---|---|
| volatile-lru | MAXMEMORY_VOLATILE_LRU | 仅带 TTL 的 key | 近似 LRU | O(1) |
| allkeys-lru | MAXMEMORY_ALLKEYS_LRU | 全局 | 近似 LRU | O(1) |
| volatile-lfu | MAXMEMORY_VOLATILE_LFU | 仅带 TTL 的 key | 近似 LFU | O(1) |
| allkeys-lfu | MAXMEMORY_ALLKEYS_LFU | 全局 | 近似 LFU | O(1) |
| volatile-random | MAXMEMORY_VOLATILE_RANDOM | 仅带 TTL | 随机 | O(1) |
| allkeys-random | MAXMEMORY_ALLKEYS_RANDOM | 全局 | 随机 | O(1) |
| volatile-ttl | MAXMEMORY_VOLATILE_TTL | 仅带 TTL | 按 TTL 升序 | O(logN) |
| noeviction | MAXMEMORY_NO_EVICTION | 不淘汰 | — | — |
4. 近似 LRU——24bit 时钟 + 幂次采样
Redis 没有维护全局双向链表,而是给每个 redisObject 打 24 bit 时间戳(秒级),淘汰时随机采样 maxmemory-samples 个 key(默认 5),挑最旧的踢掉。
时间戳只有 24 bit,绕回周期 194 天,内部用 “当前时钟 - 对象时钟” 的补码比较,绕回也能正确算差值。
采样个数通过 CONFIG SET maxmemory-samples 10 可调,越大越接近真实 LRU,但 CPU 消耗线性增加。
算法伪代码:
best = NULL;
for (i = 0; i < samples; i++) {
de = dictGetRandomKey(dict);
this = estimateObjectIdleTime(de->val);
if (!best || this > best) best = de;
}
dictDelete(dict, best);
5. 近似 LFU——8bit 对数计数器 + 衰减窗口
LFU 需要两个维度:访问频率 + 衰减。
Redis 复用 lru 字段,拆成 16 bit:
+--------+--------+
| 8bit LOG_C | 8bit DECR_TIME |
+--------+--------+
- LOG_C:对数计数器,值越大代表频率越高,增长公式
c = c + (255 - c) / 255,保证饱和后增速趋近于 0。 - DECR_TIME:记录上一次衰减的“分钟级”时间戳,每次采样时若
now - DECR_TIME > 1,则执行c = c > 0 ? c - 1 : 0,衰减速度通过lfu-decay-time配置,默认 1 分钟。
淘汰时同样采样 N 个 key,挑 LOG_C 最小的踢掉。
经验值:
- 热点 key 在 1 分钟内被访问 1000 次,
LOG_C涨到 255; - 冷 key 10 分钟无访问,
LOG_C衰减到 0; - 衰减周期调成 10 分钟,可容忍“日间高峰、夜间低谷”型业务。
6. 碎片率 > 1.5 时的三板斧
-
主动碎片整理(Active Defrag)
activedefrag yes打开后,Redis 主线程每 100 ms 扫描一次alloc_bins,把“连续空闲页 > 2 MB”且“相邻使用页 < 50 %”的 region 标记为可搬迁,再通过je_migrate()把存活对象复制到新 arena,原 regionmadvise(MADV_DONTNEED)归还 OS。
搬迁过程增量式,单次最多 10 ms,CPU 占用可忽略;
搬迁进度通过active_defrag_running指标暴露,100 代表正在搬,0 代表空闲。 -
内存淘汰加速
当frag > 1.5且used_memory < maxmemory * 0.9时,说明“数据不多但碎片大”,此时把淘汰策略临时切到allkeys-lfu,并调大maxmemory-samples到 20,一次性把“长期不用的冷 key”清掉,通常可释放 10 %–20 % 的 RSS。
清理完再把策略切回业务原定值,避免热 key 误伤。 -
重启 + aof + psync
如果碎片率仍 > 2.0,且实例可接受 30 s 级中断,直接通过debug reload做一次“热重启”:- 子进程生成临时 AOF;
- 父进程继续服务;
- 子进程写完信号通知父进程;
- 父进程 execve 重新加载,jemalloc 重新初始化,碎片归零。
重启期间通过psync把增量复制给从库,客户端无感知。
7. 监控模板(Prometheus + Grafana)
- record: redis_mem_frag_ratio
expr: redis_memory_rss_bytes / redis_memory_used_bytes
- alert: RedisHighFragmentation
expr: redis_mem_frag_ratio > 1.5
for: 30m
annotations:
summary: "Redis {{ $labels.instance }} 碎片率 {{ $value | humanizePercentage }}"
面板增加两条叠加曲线:
redis_memory_used_bytes(绿)redis_memory_rss_bytes(红)
当红线持续高于绿线 30 % 且active_defrag_running == 0,即可人工介入执行“三板斧”。
8. 小结
- jemalloc 把碎片拆成
active:dirty / retained / metadata三层,Redis 通过mem_fragmentation_ratio一键暴露。 - 碎片率 > 1.2 告警,> 1.5 自动触发 active defrag,> 2.0 考虑重启。
- 8 种淘汰策略背后只有 2 种近似算法:LRU(24bit 时钟 + 幂次采样)、LFU(8bit 对数计数器 + 衰减窗口),均保证 O(1) 复杂度。
- 把「碎片量化 → 淘汰加速 → 热重启」做成闭环,可在 30 分钟内把 12 GB RSS 压回 9 GB,而 QPS 下跌 < 5 %。
下一节《18. 内存管理——jemalloc prof 火焰图:如何抓到 1 Byte 泄漏》将带大家用 jeprof + 火焰图,把“内存持续增长却找不到对象”的终极难题拆成三步定位,欢迎继续跟进。
更多技术文章见公众号: 大城市小农民
4252

被折叠的 条评论
为什么被折叠?



