我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字------内存构成与容量规划](#Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字——内存构成与容量规划)
-
- [一、现象:数据 1G,used_memory 2G](#一、现象:数据 1G,used_memory 2G)
- [二、INFO memory 的 8 个关键字段(收藏点 1)](#二、INFO memory 的 8 个关键字段(收藏点 1))
- [三、编码转换:同一份数据,内存差 5 倍](#三、编码转换:同一份数据,内存差 5 倍)
- [四、Go 实操:编码转换的内存测量(收藏点 2)](#四、Go 实操:编码转换的内存测量(收藏点 2))
- [五、容量规划公式(收藏点 3)](#五、容量规划公式(收藏点 3))
- 六、排查步骤:内存超标时按顺序看
- 七、总结
Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字------内存构成与容量规划
上一篇《Redis 从 0 到 1(二十五):连接池又打满了------PoolSize 公式、泄漏排查与 PoolStats》我们把连接池调成了理性状态,链接:https://blog.csdn.net/2501_92769340/article/details/167399911 。
本篇往内存里看:Redis 的内存到底花在哪了,怎么规划才不被半夜的 OOM 叫醒。
一、现象:数据 1G,used_memory 2G
运维同学扩容前做容量核对:INFO memory 显示 used_memory 2.1GB,而业务自己估算的数据量只有 1GB。"另一半被谁吃了?"
答案通常藏在四个口袋里:数据结构开销、编码差异、缓冲区、碎片。本篇把这四个口袋逐个拆开,给每个数字一个名字。
二、INFO memory 的 8 个关键字段(收藏点 1)
| 字段 | 含义 | 关注点 |
|---|---|---|
used_memory |
Redis 自己记账的数据+结构总字节 | 容量规划基准;admin 视角 |
used_memory_rss |
操作系统视角实际占用(含碎片、页缓存) | 与 used_memory 的比值见下行 |
mem_fragmentation_ratio |
RSS / used_memory | 1.0~1.1 健康;>1.5 警惕碎片(第 30 篇);<1.0 说明 swap 或共享内存 |
used_memory_dataset |
纯数据部分 | 业务真实数据量 |
used_memory_overhead |
结构开销(dictEntry、redisObject、过期字典) | overhead/dataset 比例反映 key 数量级开销 |
used_memory_lua |
Lua 引擎占用(本系列不用 Lua,此项很小) | 异常增长要查 |
mem_clients_normal / mem_clients_slaves |
客户端输出缓冲 | slave 缓冲大 = 复制卡了(第 8 篇来源 3) |
used_memory_startup |
启动基础开销 | 常数,无需关注 |
经验诊断:
- used_memory 涨但 used_memory_dataset 不涨 → 结构开销在涨:key 太多或大量空结构;
- overhead 占一半以上 → key 设计有问题(短 key 千万级时 overhead 能超过数据本身,第 4 篇的 key 长度规范就是为它定的);
- rss >> used_memory → 碎片(下一篇)。
三、编码转换:同一份数据,内存差 5 倍
Redis 每种结构都有"紧凑编码"和"普通编码"两档,按元素数量/大小自动转换:
| 结构 | 紧凑编码 | 转换阈值(redis.conf 可调) | 普通编码 |
|---|---|---|---|
| Hash | listpack | hash-max-listpack-entries 128、hash-max-listpack-value 64 |
hashtable |
| ZSet | listpack | zset-max-listpack-entries 128 |
skiplist |
| List | listpack | list-max-listpack-size 128 |
quicklist |
| Set | intset | set-max-intset-entries 512 |
hashtable |
含义:一个 100 个 field 的小 Hash 走 listpack,内存接近纯数据;第 129 个 field 触发转换整体搬迁成 hashtable,内存瞬间翻几倍。同一份数据,128 个 field 和 129 个 field 的内存差可能是 3 倍------这就是"数据明明一样多,内存却对不上"的常见原因。
bash
OBJECT ENCODING user:1001 # 看当前编码
CONFIG GET hash-max* # 看阈值
四、Go 实操:编码转换的内存测量(收藏点 2)
go
package main
import (
"context"
"fmt"
"time"
"redis-0to1/redisx"
)
func main() {
rdb, err := redisx.NewClient(redisx.Config{Addr: "127.0.0.1:6379", Password: "redis123", PoolSize: 10})
if err != nil {
panic(err)
}
ctx := context.Background()
// 对照实验:128 个 field(listpack)vs 200 个 field(hashtable),同 field 大小
measure := func(n int) {
key := fmt.Sprintf("memtest:%d", n)
pipe := rdb.Pipeline()
for i := 0; i < n; i++ {
pipe.HSet(ctx, key, fmt.Sprintf("f%03d", i), "value-of-same-length-32bytes")
}
pipe.Exec(ctx)
enc, _ := rdb.ObjectEncoding(ctx, key).Result()
usage, _ := rdb.MemoryUsage(ctx, key, 0).Result()
fmt.Printf("fields=%d 编码=%s 内存=%d 字节\n", n, enc, usage)
rdb.Del(ctx, key)
}
measure(128) // 典型结果:编码=listpack,约 5KB
measure(200) // 典型结果:编码=hashtable,约 15KB+(3 倍差)
}
(数字随版本略有出入,量级不变:转换点前后内存差数倍。)
五、容量规划公式(收藏点 3)
实例容量规划:
1. 数据量 = 单条平均字节 × 条数 × 结构系数(listpack 小对象约 1.2,hashtable 约 2~4)
2. 加 key 开销:key 数 × (dictEntry+redisObject ≈ 100 字节)
3. 加缓冲区余量:AOF 重写缓冲区 + 复制缓冲 ≈ 数据量的 10%~20%
4. 加碎片余量:RSS = used_memory × 1.1~1.3(第 30 篇)
5. maxmemory 设为物理内存的 70%(给 fork 留出 COW 空间,第 7 篇 RDB 需要)
例:1 亿 key、单 value 500B、平均 3 个 field 的 Hash
→ 数据 50GB × 结构系数 2 + key 开销 10GB + 缓冲 12GB ≈ 122GB
→ 实例物理内存 ≥ 122GB / 0.7 ≈ 175GB → 一台装不下就上 Cluster 分片
六、排查步骤:内存超标时按顺序看
INFO memory拍全景:dataset vs overhead vs rss 三分法定位大类;MEMORY USAGE抽查可疑大 key(第 22 篇);OBJECT ENCODING抽查高频 key 是否全在 hashtable(阈值被业务数据形态踩穿);- 对照公式重算容量规划,决定扩内存还是拆 Cluster。
七、总结
- used_memory ≠ 数据量:结构开销、编码、缓冲、碎片四个口袋;
- 编码转换点(listpack→hashtable)是内存的隐形台阶,阈值可配可调可观测;
- 容量规划五步法:数据 × 结构系数 + key 开销 + 缓冲 + 碎片 + maxmemory 打 7 折;
- 小 Hash/小 ZSet 保持 listpack 是免费的内存优化。
下一篇预告:内存省了一半,网络传输还能再省------下一篇《JSON 体积是 Protobuf 的 3.7 倍?3 种序列化的 Go 基准测试》,用 Benchmark 数据说话,选对你的 Codec。