2682 字
约 8 分钟
13
S12-A12 · 内存碎片与内存告警:used_memory 与 RSS 的差

一个 Redis 实例告警:宿主机内存使用率 92%,容器随时可能被 OOM 杀掉。但 INFO memory 里 used_memory_human 只有 3.2G,而 used_memory_rss_human 是 6.1G,配置上限 maxmemory 是 4G。

差出的这 2.9G 既没有被数据占用,也不会被 maxmemory 算进去——它是碎片:已分配给 Redis、但当前没有被任何键使用的那部分内存。它不是"泄漏",重启就消失;但它同样能把实例压死,而所有基于 used_memory 的告警都不会响。


1. 三个数各自在说什么

指标 含义 谁会用它
used_memory Redis 自己统计的数据占用(逻辑量) maxmemory 淘汰判定、容量规划
used_memory_rss 操作系统看到的常驻内存(物理量) 容器限额、OOM、宿主监控
mem_fragmentation_ratio rss / used_memory 的比值 碎片健康度

关键点是两者由不同的机制约束:淘汰由 used_memory 与 maxmemory 决定,而 OOM 由 rss 与容器限额决定。于是存在一个危险区间——used_memory 远低于 maxmemory,而 rss 已接近容器上限。此时不淘汰、不告警、直接被杀。

// 典型的三层约束,任何一层没对上都会出问题
maxmemory 4gb                     // 数据层上限,按 used_memory 判定
container memory limit 5gb        // 物理层上限,按 rss 判定
// 若 rss 因碎片涨到 5.2gb,进程被 OOM kill — 而 used_memory 只有 3.2gb

2. 碎片从哪来

一个直觉上的误解值得先排除:碎片不是你写错了什么。它来自分配器"按尺寸分级"的分配方式——申请 100 字节,实际给 112 字节,多出的部分在那一页里就固定浪费了。这类内部浪费始终存在,健康实例的比值也在 1.1 到 1.5 之间。所以看碎片不是看它有没有,而是看它有没有失控。

碎片是分配器行为与数据变化共同产生的,不是配置错误。

成因 机制 特征
大量删除 释放的页不归还原系统 删除高峰后 rss 不降
值大小多变 不同尺寸的分配无法复用同一页 混合尺寸写入最明显
频繁改写的结构 每次改写可能重新分配 hash/list 反复增删
大键删除 一次性释放大块但不归还 单次操作即产生
分配器行为 jemalloc 按 size class 分配,内部有浪费 始终存在一定比例

还有一类成因在 Redis 之外:混合部署。同一个宿主机上跑着别的进程,Redis 的 rss 与容器限额都不变,但可用物理内存变少,于是它更容易被 OOM killer 选中。这类问题查起来像碎片,实际上是邻居。

第一项最典型:一次数据清理删掉了 40% 的键,used_memory 立刻下降,rss 几乎不动。此时 mem_fragmentation_ratio 从 1.1 跳到 2.0 以上。如果只看 used_memory,会误以为"内存充足"。

3. 比值的两种异常方向

比值偏离 1 有两个方向,含义完全不同。

比值 含义 处置
≈ 1.0–1.5 健康 无
> 1.5 碎片偏多 观察趋势,评估重启或激活碎片整理
> 2.0 碎片严重 计划重启,检查是否有大键删除
< 1.0 部分内存在 swap 或被换出 严重:检查 swap 使用,性能已受影响

第四项最危险,也最容易被误判为"内存很健康"。比值小于 1 说明 Redis 的部分内存不在物理内存里——已经发生了 swap。此时延迟会突然变差,而所有内存指标看起来都正常。

// 判断是否发生 swap:比比值本身更直接的证据
redis-cli info memory | grep -E "used_memory_human|used_memory_rss_human|mem_fragmentation_ratio"
cat /proc/$(pgrep -x redis-server | head -1)/status | grep -E "VmRSS|VmSwap"
// VmSwap 非 0 → 已经换出,优先处理 swap 而不是碎片

4. 处置手段与各自的代价

手段 效果 代价
重启实例 彻底消除碎片 需要切换(主从/集群),有中断风险
activedefrag yes 在线整理 额外 CPU 与内存开销,可能影响延迟
调整 maxmemory 留余量 把碎片算进预算 数据容量减少
拆分实例 每个实例的碎片绝对值变小 运维复杂度上升
规范写入(避免反复改写大结构) 从源头减少 需要改业务代码

重启前有一件事必须先确认:碎片是均匀的还是集中在少数节点。集群模式下,只有个别节点碎片高时,逐个重启即可;如果全部节点同时高,说明写入模式有问题,重启只能缓解一轮。

// 集群内逐节点比较碎片,决定是局部处理还是全局治理
for node in $(redis-cli -c cluster nodes | awk '{print $2}' | cut -d@ -f1); do
  echo -n "$node "
  redis-cli -h ${node%:*} -p ${node#*:} info memory | grep mem_fragmentation_ratio
done
// 全部 > 2.0 → 写入模式问题;个别 > 2.0 → 局部重启即可

第二项值得细说:主动碎片整理在 Redis 4.0 之后可用,但它不是免费的。开启后需要配置 CPU 占用上限与最小碎片阈值,否则会在低峰期持续消耗 CPU,或者在高峰期加剧延迟。

// 主动碎片整理:必须同时限制 CPU 与触发阈值
activedefrag yes
active-defrag-ignore-bytes 100mb        # 碎片量小于此值不整理
active-defrag-threshold-lower 10        # 碎片率低于 10% 不做
active-defrag-threshold-upper 100       # 碎片率高于 100% 用满 CPU 上限
active-defrag-cycle-min 1               # 最低 CPU 占比(%)
active-defrag-cycle-max 25              # 最高 CPU 占比(%),按延迟预算调

5. 容量规划必须按 rss 算

这是最实用的一条结论:给 Redis 预留内存时,不能只用 maxmemory。

规划依据 结果
只按 maxmemory 碎片一多就被 OOM
maxmemory + 固定 20% 常见做法,但仍可能不够(大键删除后碎片可翻倍)
maxmemory × 经验比值 1.5 较稳,需按实际比值校准
按观测到的 rss 峰值 × 1.2 最稳,需要历史数据
用 rss 但未考虑持久化开销 忽略了 fork 与 AOF 重写的瞬时翻倍

规划时还有一个时间维度值得记下:预算要按峰值算,不是按平均算。同一实例在平时 rss 是 4G,在做 AOF 重写时可能是 7G;容量按平均值给,等于每个月都有几天在赌。

最后一行常被漏掉:开启 RDB 时 fork 会带来写时复制的额外占用,峰值可能接近数据量的两倍。碎片与 fork 叠加,是"平时没事、备份时被杀"的典型原因。

// 容量预算:四项相加,不是取最大
redis_capacity_budget:
  data_maxmemory: 4Gi
  fragmentation_allowance: 1.5      # 按实际比值校准,初值 1.5
  fork_overhead: 1.2                # 有 RDB/AOF 重写时不能省
  reserved_for_os: 512Mi
// 容器限额 ≈ data × fragmentation × fork + reserved
  container_limit: "8Gi"

按这个式子,4G 数据量的实例需要 8G 容器限额。许多团队按 maxmemory 直接给 4.5G,于是备份时被杀。

6. 排错:五种"内存看着够却被杀"

现象 成因 处理方向
容器 OOM 但 used_memory 很低 碎片导致 rss 高 查 mem_fragmentation_ratio
备份期间被杀 fork 写时复制叠加碎片 容量预算加 fork 系数
重启后内存降一半 碎片被回收,确认是碎片 评估 activedefrag 或定期重启
延迟突然变差但指标正常 发生 swap,比值 < 1 检查 VmSwap,禁用 swap 或加内存
删了大量键但内存不降 释放的页不归还 属正常行为,不必当成故障

还有一条容易被忽略的排错方向:把 rss 的时间曲线和业务峰值对齐看。碎片与峰值通常不同步——碎片是缓慢累积的,而峰值是周期性的。两条曲线同时高,问题在容量;只有碎片曲线高,问题在分配与删除模式。

第四项最隐蔽:swap 一旦发生,P99 延迟会从毫秒级跳到几十毫秒,而 CPU、内存、连接数所有指标都正常。这类问题的排查入口只有一个——看 /proc/<pid>/status 里的 VmSwap。

7. 边界:什么情况不必关注碎片

场景 为什么 做法
实例数据量小(< 1G) 碎片绝对值小 只看 used_memory
数据只增不删 碎片几乎不产生 比值稳定在健康区
频繁重启的短生命周期实例 碎片来不及累积 不必专门治理
用内存型数据库但已给足余量 碎片被预算吸收 监控即可

还有一层组织上的边界:碎片治理不该由业务团队负责。它取决于实例的分配器行为、重启能力与容量预算,都是平台侧的决策。把"碎片高"当成业务方的告警项,结果是业务方既看不到 rss,也无权重启实例——这类告警最后一定被静音。

判断口径一句话:碎片是否已经进入容器限额的射程。没进射程,它是正常的实现细节;进了射程,它就是会杀掉进程的东西。


8. 自测

  1. used_memory 与 used_memory_rss 各自被哪套机制约束?为什么会出现"不淘汰、不告警、直接被 OOM"的区间?
  2. 大量删除键之后,两个指标分别怎么变化?只看一个会得出什么错误结论?
  3. 比值小于 1 说明什么?为什么它比碎片更危险,却更容易被误判为健康?
  4. 主动碎片整理的代价是什么?三个必须同时设置的参数分别控制什么?
  5. 容量预算为什么不能只按 maxmemory 算?fork 开销在什么时刻最明显?
  6. 什么情况下碎片属于正常实现细节,不需要专门治理?
S12-A12 · 内存碎片与内存告警:used_memory 与 RSS 的差
https://www.clxhxhhr.top/posts/6394/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。