我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [Redis 从 0 到 1(三十):删了一半数据 RSS 不降------内存碎片与 activedefrag 观测](#Redis 从 0 到 1(三十):删了一半数据 RSS 不降——内存碎片与 activedefrag 观测)
-
- 一、现象:数字对不上的内存
- [二、碎片治理决策树(收藏点 1)](#二、碎片治理决策树(收藏点 1))
- [三、Go 实操:制造碎片并观测 activedefrag(收藏点 2)](#三、Go 实操:制造碎片并观测 activedefrag(收藏点 2))
- [四、activedefrag 配置与代价(收藏点 3)](#四、activedefrag 配置与代价(收藏点 3))
- 五、命令清单
- [六、排查步骤:RSS 异常不降](#六、排查步骤:RSS 异常不降)
- 七、总结
Redis 从 0 到 1(三十):删了一半数据 RSS 不降------内存碎片与 activedefrag 观测
上一篇《Redis 从 0 到 1(二十九):CPU 突然 100%------commandstats 定位法与 6 步排查链路》我们破了 CPU 100% 案,链接:https://blog.csdn.net/2501_92769340/article/details/167400306 。
第二案:运维删了一半历史数据想释放内存,used_memory 确实降了一半,但操作系统的 RSS(实际占用)纹丝不动------服务器内存压力一点没缓解。
一、现象:数字对不上的内存
bash
INFO memory | grep -E "used_memory:|rss|ratio"
# used_memory:1.05G ← Redis 记账:数据只剩一半了
# used_memory_rss:1.90G ← OS 视角:实际还占着 1.9G
# mem_fragmentation_ratio:1.81
碎片率 1.81:物理内存页里只有 55% 装的是有效数据,其余是"空洞"。成因是 Redis 的内存分配模式:jemalloc 按固定大小档分配,变长 value 频繁增删后,已释放的小块夹杂在存活数据之间,无法归还给 OS。
典型制造碎片的操作:长期运行的实例 + value 大小差异大 + 大量删除(第 22 篇清 bigkey 之后尤其容易触发)。
二、碎片治理决策树(收藏点 1)
mem_fragmentation_ratio 多少?
├── < 1.1:健康,什么都不做
├── 1.1 ~ 1.4:观察,监控趋势
├── 1.4 ~ 1.5:
│ └── 低峰期执行 MEMORY PURGE(一次性归还,秒级)
└── > 1.5:
├── 方案 A(在线):开 activedefrag,边服务边整理
│ └── 注意 CPU 代价(第 29 案警告)
└── 方案 B(根治):主从切换,让新实例从头顺序装载数据
└── RDB 重放是顺序分配,天然低碎片
三、Go 实操:制造碎片并观测 activedefrag(收藏点 2)
go
package main
import (
"context"
"fmt"
"math/rand"
"strings"
"time"
"redis-0to1/redisx"
)
// Ratio 解析 INFO memory 的碎片率(go-redis 无直接字段,从文本提取)
func Ratio(ctx context.Context, rdb *redis.Client) float64 {
info, _ := rdb.Info(ctx, "memory").Result()
for _, line := range strings.Split(info, "\r\n") {
if strings.HasPrefix(line, "mem_fragmentation_ratio:") {
var f float64
fmt.Sscanf(line, "mem_fragmentation_ratio:%f", &f)
return f
}
}
return 0
}
func main() {
rdb, _ := redisx.NewClient(redisx.Config{Addr: "127.0.0.1:6379", Password: "redis123", PoolSize: 10})
ctx := context.Background()
// 1. 制造碎片:写 20 万个变长 value(512B~4KB 随机),
// 然后随机删一半 ------ jemalloc 产生大量无法归还的空洞
for i := 0; i < 200000; i++ {
size := 512 + rand.Intn(3584)
rdb.SetEx(ctx, fmt.Sprintf("frag:k%d", i), strings.Repeat("x", size), time.Hour)
}
for i := 0; i < 100000; i++ {
rdb.Del(ctx, fmt.Sprintf("frag:k%d", rand.Intn(200000)))
}
fmt.Println("删除后碎片率:", Ratio(ctx, rdb)) // 典型 1.3~1.8
// 2. 观察在线整理(需服务端开启 activedefrag)
// CONFIG SET activedefrag yes
// 每 30 秒打印 Ratio,可见逐步回落到 1.0~1.1
t := time.NewTicker(30 * time.Second)
for range t.C {
r := Ratio(ctx, rdb)
fmt.Printf("%s 碎片率=%.2f\n", time.Now().Format("15:04:05"), r)
if r < 1.15 {
fmt.Println("整理完成,可 CONFIG SET activedefrag no 关闭")
return
}
}
}
四、activedefrag 配置与代价(收藏点 3)
conf
# redis.conf ------ 五个参数,别裸开 yes
activedefrag yes
active-defrag-ignore-bytes 100mb # 碎片小于 100MB 不启动(省 CPU)
active-defrag-threshold-lower 10 # 碎片率 >10% 才启动
active-defrag-threshold-upper 100 # 碎片率 >100% 时最激进
active-defrag-cycle-min 5 # 每轮整理占 CPU 下限 5%
active-defrag-cycle-max 20 # 上限 20%(峰值期压不下来就把 max 调小)
代价就是 CPU(第 29 案的系统级事件之一):整理期间 CPU 会多出 5~20% 的持续消耗。所以低碎片率不开、低峰期开、开完关,是标准姿势。MEMORY PURGE 则是"手动一次整理",适合做完了大批量删除后的收尾。
五、命令清单
| 命令 | 作用 |
|---|---|
INFO memory |
mem_fragmentation_ratio / rss |
MEMORY PURGE |
手动触发一次整理 |
CONFIG SET activedefrag yes/no |
在线开关自动整理 |
LATENCY DOCTOR |
确认 CPU 尖峰是否来自 defrag |
六、排查步骤:RSS 异常不降
- 先确认不是假象:确认删除真的发生了(DBSIZE/used_memory_dataset);
- 看
mem_fragmentation_ratio:>1.5 走决策树; - 低峰
MEMORY PURGE试一次:RSS 明显下降 = 碎片实锤; - 顽固碎片(ratio 仍高)→ activedefrag 在线整理,或安排主从切换根治;
- 治本预防:value 大小差异大的场景,考虑定长化或分桶(第 26 篇)。
七、总结
- RSS ≠ used_memory:差值是碎片;ratio >1.5 才需要动手;
- 碎片是 jemalloc + 变长 value + 大量删除的必然产物,删数据后 RSS 不降不是灵异事件;
- 治理阶梯:PURGE(一次性)→ activedefrag(在线,花 CPU)→ 主从切换(根治);
- 开 defrag 必配阈值参数,裸开 yes 会偷走你的 CPU。
下一篇预告:单机故障排完了,下一案上分布式------下一篇《主从切换后订单丢了一页------脑裂的 2 个丢数场景与 min-replicas 防御》,哨兵自动切主的隐性代价来了。