内存碎片与 activedefrag:Redis 内存去哪了
监控告警里 Redis 占用内存 12GB,INFO memory 里数据只有 8GB,多的 4GB 去哪了?这是内存碎片:分配器已经拿到的、数据却不在此处的物理内存。碎片严重时白白多花一倍机器成本,还可能触发 OOM。本文以 Redis 7(jemalloc 5.x)为准,讲清碎片成因、判断指标、activedefrag 的原理与配置,以及在线治理之外的根治手段。
一、碎片是怎么产生的
Redis 默认用 jemalloc 分配内存。jemalloc 按固定尺寸类别分配(8B、16B、32B……4KB、页级),释放时内存块回到分配器的空闲链表,并不会立即归还操作系统。当 value 大小不一、写入删除频繁时——典型的如缓存轮换、会话存取、大 key 的 lazyfree 删除——释放留下的空洞尺寸与后续请求不匹配,新对象填不进旧洞,分配器只能继续向 OS 要新页。进程长期运行后,RSS(实际占用物理内存)就显著高于数据真正需要的内存。
两个典型场景助长碎片:一是业务冷启动塞满后又大量删除,峰值内存定型后不回落;二是 value 大小高度随机(几十字节到几十 KB 混杂),空洞很难被复用。
用尺寸类别举个具体例子:进程先写入一批 32KB 的大 value,随后删除,jemalloc 把这些 32KB 的块挂回 32KB 空闲链表;此后业务只写 1KB 的小 value,1KB 复用不了 32KB 的块,分配器只能继续向系统申请新页。于是数据总量没涨,RSS 却停在了历史峰值。
理解 jemalloc 的尺寸类别才能判断碎片是否"可复用"。jemalloc 把小对象按 8 字节递增的多个类别、大对象按 4KB 页的倍数分级,每个类别一条独立空闲链表。跨类别无法复用是碎片的结构性来源:32KB 的洞永远只能装 32KB 类的请求。这也意味着碎片率有一个不可消除的底座——不同尺寸的对象混在一起时,总有一部分空闲块落在"下次不会再用到"的类别里。所以治理目标不是让 ratio 等于 1,而是把它控制在合理区间。
还有一个方向相反的机制需要知道:used_memory 统计的是逻辑分配量,used_memory_rss 来自 /proc/<pid>/statm 的实际物理页数。两者之间的差值除了内部碎片,还包含分配器的保留区(jemalloc 拿到但尚未使用、也尚未归还给 OS 的页面)。这就是为什么 MEMORY PURGE 有时能让 RSS 立刻下降几百 MB,而 used_memory 毫无变化。
二、用 INFO memory 判断
# Memory
used_memory:2147483648 # 数据+开销的逻辑内存,约 2GB
used_memory_rss:3221225472 # 操作系统视角的物理内存,约 3GB
mem_fragmentation_ratio:1.50 # rss / used_memory
mem_fragmentation_bytes:1073741824
mem_allocator:jemalloc-5.3.0
解读经验值:mem_fragmentation_ratio 在 1.0~1.5 之间属正常(元数据与对齐本身有开销);大于 1.5 且 mem_fragmentation_bytes 达到数百 MB 以上,值得处理;小于 1 反而更危险,说明 RSS 低于逻辑内存,Redis 的部分数据被换到了 swap,性能会断崖式下跌,要立即查宿主机内存压力。
MEMORY DOCTOR 会把上述判断用文字说出来,MEMORY STATS 给出分配器级别的细分数据,可以配合使用。
三、activedefrag 的原理与配置
Redis 4.0 起提供主动碎片整理。原理是扫描 jemalloc 的空闲页,把分散的对象搬移到一起,把凑成整页的空闲内存还给操作系统。整个过程在后台进行,可在线开启:
CONFIG SET activedefrag yes
CONFIG SET active-defrag-ignore-bytes 200mb # 碎片字节数低于此值不整理
CONFIG SET active-defrag-threshold-lower 20 # 碎片率超过 20% 才开始(相对上限的百分比)
CONFIG SET active-defrag-threshold-upper 100 # 达到 100% 时全力整理
CONFIG SET active-defrag-cycle-min 5 # 空闲期 CPU 占比下限,默认 1
CONFIG SET active-defrag-cycle-max 40 # 压力期 CPU 占比上限,默认 25
触发条件是"碎片字节数超过 ignore-bytes 且碎片率超过 threshold-lower",两条件同时满足才启动;碎片率越高,整理占用的 CPU 越接近 cycle-max。开启的前提是编译时启用了 jemalloc 且支持 defrag 接口,INFO memory 里 mem_allocator 显示 jemalloc 即满足;不支持时 CONFIG SET 会直接报错。这组参数同样要 CONFIG REWRITE 固化,否则重启后 activedefrag 回到默认关闭状态。
注意:activedefrag 搬移对象发生在主线程切片里,高压力下会带来延迟抖动,建议低峰开启并观察 LATENCY 的
active-defrag-cycle事件。
工作方式上,activedefrag 并不是一次性整理全库,而是每次主线程空闲切片时用游标增量扫描一小段键空间,发现对象所在页碎片化,就把对象搬移到更紧凑的位置,腾出的空页凑成整页后归还系统。因此效果是渐进的,评估 RSS 要看小时级趋势,而不是开完开关就等结果。
整理进度可以观测。INFO stats 里有四个专用于 defrag 的计数器:
redis-cli INFO stats | grep -E "active_defrag|total_active_defrag"
# active_defrag_hits:184271
# active_defrag_misses:903288
# active_defrag_key_hits:12043
# active_defrag_key_misses:58210
# total_active_defrag_time:412879
# current_active_defrag_time:1832
redis-cli INFO memory | grep -E "active_defrag_running|allocator_frag"
# active_defrag_running:0
# allocator_frag_ratio:1.42
# allocator_frag_bytes:734003200
active_defrag_running 在 memory 段,为 0 表示当前没有在整理,非 0 时它的数值就是本次整理打算使用的 CPU 百分比;hits 是成功搬移的次数,misses 是尝试后未能搬移的次数,key_hits 与 key_misses 则是按 key 粒度的统计。hits 持续增长说明整理确实有效;如果 misses 远大于 hits(如上面的 90 万对 18 万),说明大量尝试都失败了——常见原因是碎片跨尺寸类别,搬移本身解决不了。这种情况下继续开着 defrag 只是白白消耗 CPU。
total_active_defrag_time 是累计处于"碎片超限"状态的毫秒数,current_active_defrag_time 是最近一次超限至今的毫秒数。后者持续增长说明碎片一直没降下来,此时应当考虑重启换血而不是继续等 defrag。
allocator_frag_ratio 与 allocator_frag_bytes 是 jemalloc 视角的碎片,与 mem_fragmentation_ratio 的口径不同:前者衡量"分配器内部的碎片",后者衡量"RSS 与逻辑内存的差"。两个指标同时高,说明碎片既有分配器层也有系统层;只有 mem_fragmentation_ratio 高而 allocator_frag_ratio 正常,多半是 COW、代码段或脏页导致的 RSS 抬升。
四、其他手段与根治
手动整理: MEMORY PURGE 让 jemalloc 立即清理脏页、归还 OS,对 RSS 立竿见影,但对"对象离散"型碎片效果有限。
重启换血: 主从/哨兵架构下,对碎片最严重的节点执行手动故障转移,让它降级为从库后重启再拉起,是最彻底的方式。碎片整理本质上是"重新分配一遍",重启等价于零成本的全量整理。哨兵架构下先手动触发 SENTINEL failover 迁走主库,重启原主库,它会自动作为从库追平数据;Cluster 下则逐节点把从库提升、再重启降级的原主库。全程滚动进行并保留冗余,绝不同时重启互为备份的节点。
根治靠写入模式: value 大小尽量规整(同类数据用相近的序列化尺寸)、控制大 key 的删除节奏(分批 UNLINK)、避免"一次性灌满再清空"的负载。碎片是慢性病,activedefrag 只是兜底药。治理期间盯两个数的趋势:mem_fragmentation_bytes 的绝对值,以及 MEMORY PURGE 前后 RSS 的落差——落差大说明脏页多,趋势平稳下降说明整理在起作用。
常见坑 / 最佳实践
- 别在 fork 后立刻看 ratio。 BGSAVE 或 AOF 重写期间写时复制会推高 RSS,此时 1.5+ 可能是假象,等持久化结束再读数。
- ratio 小于 1 先查 swap,不是省内存了。 这是数据被换出的信号,优先级高于一切碎片问题。
- activedefrag 不是免费的。
active-defrag-cycle-max保持默认 25 以下,低峰期开启,并盯住LATENCY事件防止整理本身造成毛刺。 - 云托管实例不一定开放这些配置。 部分托管 Redis 禁止
CONFIG SET,先确认能力再设计治理方案。 - 不要追求 ratio 等于 1。 分配器元数据与对齐注定有 10%~30% 的合理开销,把它压到 1.5 以内、字节量可接受即可。
小结
- 碎片来自 jemalloc 的尺寸类别与"不归还 OS"策略,写入删除越杂越严重。
- 判断看
mem_fragmentation_ratio与mem_fragmentation_bytes,大于 1.5 且字节数大才需要处理,小于 1 先查 swap。 - activedefrag 在线搬移对象归还内存,配置触发阈值与 CPU 占比,低峰开启。
- 彻底手段是故障转移后重启节点,手动手段是
MEMORY PURGE。 - 根治靠规整的 value 大小与克制的删除节奏,碎片治理本质是容量治理。
- 点赞
- 收藏
- 关注作者
评论(0)