17.内存管理——jemalloc 碎片率统计,内存淘汰 8 种策略 + LRU/LFU 近似算法

在这里插入图片描述

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:dirtyactive - allocated高并发 + 长生命周期对象5 %–15 %
retainedretained / allocated释放后未归还 OS10 %–30 %
metadatametadata / 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_ratioallocator_rss_ratiorss_overhead_ratio 三列打平输出,方便一眼定位是“脏页没归还”还是“元数据膨胀”。


3. 让数字变成决策——8 种淘汰策略全景

Redis 4.0 之后把“内存上限”拆成两层:maxmemory 控制用户数据,maxmemory_allocator(jemalloc 专有)控制分配器层。
used_memory > maxmemory 时,触发“数据淘汰”;当 allocator_active > maxmemory_allocator 时,触发“分配器回收”。
前者 8 种策略,后者 3 种策略,合起来 11 种,但线上 99 % 场景只关心下面 8 种:

策略名代码宏淘汰粒度近似算法复杂度
volatile-lruMAXMEMORY_VOLATILE_LRU仅带 TTL 的 key近似 LRUO(1)
allkeys-lruMAXMEMORY_ALLKEYS_LRU全局近似 LRUO(1)
volatile-lfuMAXMEMORY_VOLATILE_LFU仅带 TTL 的 key近似 LFUO(1)
allkeys-lfuMAXMEMORY_ALLKEYS_LFU全局近似 LFUO(1)
volatile-randomMAXMEMORY_VOLATILE_RANDOM仅带 TTL随机O(1)
allkeys-randomMAXMEMORY_ALLKEYS_RANDOM全局随机O(1)
volatile-ttlMAXMEMORY_VOLATILE_TTL仅带 TTL按 TTL 升序O(logN)
noevictionMAXMEMORY_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 时的三板斧

  1. 主动碎片整理(Active Defrag)
    activedefrag yes 打开后,Redis 主线程每 100 ms 扫描一次 alloc_bins,把“连续空闲页 > 2 MB”且“相邻使用页 < 50 %”的 region 标记为可搬迁,再通过 je_migrate() 把存活对象复制到新 arena,原 region madvise(MADV_DONTNEED) 归还 OS。
    搬迁过程增量式,单次最多 10 ms,CPU 占用可忽略;
    搬迁进度通过 active_defrag_running 指标暴露,100 代表正在搬,0 代表空闲。

  2. 内存淘汰加速
    frag > 1.5used_memory < maxmemory * 0.9 时,说明“数据不多但碎片大”,此时把淘汰策略临时切到 allkeys-lfu,并调大 maxmemory-samples 到 20,一次性把“长期不用的冷 key”清掉,通常可释放 10 %–20 % 的 RSS。
    清理完再把策略切回业务原定值,避免热 key 误伤。

  3. 重启 + 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 + 火焰图,把“内存持续增长却找不到对象”的终极难题拆成三步定位,欢迎继续跟进。
更多技术文章见公众号: 大城市小农民

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乔丹搞IT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值