一个 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. 自测
used_memory与used_memory_rss各自被哪套机制约束?为什么会出现"不淘汰、不告警、直接被 OOM"的区间?- 大量删除键之后,两个指标分别怎么变化?只看一个会得出什么错误结论?
- 比值小于 1 说明什么?为什么它比碎片更危险,却更容易被误判为健康?
- 主动碎片整理的代价是什么?三个必须同时设置的参数分别控制什么?
- 容量预算为什么不能只按
maxmemory算?fork 开销在什么时刻最明显? - 什么情况下碎片属于正常实现细节,不需要专门治理?