内存碎片与 activedefrag:Redis 内存去哪了

举报
yd_287908631 发表于 2026/11/15 07:31:03 2026/11/15
【摘要】 解读碎片率指标,用 activedefrag 在线整理内存

监控告警里 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 的落差——落差大说明脏页多,趋势平稳下降说明整理在起作用。

常见坑 / 最佳实践

  1. 别在 fork 后立刻看 ratio。 BGSAVE 或 AOF 重写期间写时复制会推高 RSS,此时 1.5+ 可能是假象,等持久化结束再读数。
  2. ratio 小于 1 先查 swap,不是省内存了。 这是数据被换出的信号,优先级高于一切碎片问题。
  3. activedefrag 不是免费的。 active-defrag-cycle-max 保持默认 25 以下,低峰期开启,并盯住 LATENCY 事件防止整理本身造成毛刺。
  4. 云托管实例不一定开放这些配置。 部分托管 Redis 禁止 CONFIG SET,先确认能力再设计治理方案。
  5. 不要追求 ratio 等于 1。 分配器元数据与对齐注定有 10%~30% 的合理开销,把它压到 1.5 以内、字节量可接受即可。

小结

  • 碎片来自 jemalloc 的尺寸类别与"不归还 OS"策略,写入删除越杂越严重。
  • 判断看 mem_fragmentation_ratio 与 mem_fragmentation_bytes,大于 1.5 且字节数大才需要处理,小于 1 先查 swap。
  • activedefrag 在线搬移对象归还内存,配置触发阈值与 CPU 占比,低峰开启。
  • 彻底手段是故障转移后重启节点,手动手段是 MEMORY PURGE。
  • 根治靠规整的 value 大小与克制的删除节奏,碎片治理本质是容量治理。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。