第 24 章
内存分配与 jemalloc:碎片整理原理
第24章 内存分配与 jemalloc:碎片整理原理
Redis 的内存管理是影响性能和稳定性的核心因素之一。本章从内存分配器选择、zmalloc 封装层、jemalloc 内部架构,到在线碎片整理(activedefrag)和惰性释放(lazyfree),进行全面的源码级分析。
24.1 内存分配器选择
Redis 支持三种内存分配器,在编译时指定:
make MALLOC=jemalloc # Linux 默认(推荐生产使用)
make MALLOC=libc # 使用系统 malloc(macOS/BSD 默认,glibc malloc)
make MALLOC=tcmalloc # Google tcmalloc(需安装 libgoogle-perftools-dev)
运行时查看当前使用的分配器:
redis-cli INFO memory | grep mem_allocator
# 输出: mem_allocator:jemalloc-5.3.0
三种分配器对比
| 特性 | glibc malloc | jemalloc | tcmalloc |
|---|---|---|---|
| 碎片率 | 较高(ptmalloc2设计较老) | 低(精心设计的 size class) | 低 |
| 多线程性能 | 差(全局锁) | 优秀(per-CPU arena) | 优秀(per-thread cache) |
| 内存归还OS | 较慢 | 较快(可配置) | 较快 |
| 调试支持 | 有限 | 丰富(统计、prof) | 丰富 |
| 生产可靠性 | 成熟 | 成熟(Firefox、Facebook、Redis) | 成熟(Chrome) |
为什么 Redis 选择 jemalloc 作为 Linux 默认:
- glibc ptmalloc2 在高并发写入时锁争用严重
- jemalloc 的 size class 设计减少了内碎片
- jemalloc 支持
MALLOC_CONF精细调优 - Facebook 等公司在生产中验证了其稳定性
24.2 zmalloc 封装层
Redis 不直接调用 malloc,而是通过 zmalloc.c 封装,主要目的是跟踪已分配的内存量。
核心实现
// zmalloc.c
// PREFIX_SIZE:在每次分配前额外分配的头部大小
// 64位系统:sizeof(size_t) = 8 字节
// 如果使用 jemalloc:PREFIX_SIZE = 0(jemalloc 有 malloc_usable_size)
#ifdef HAVE_MALLOC_SIZE
#define PREFIX_SIZE (0) // jemalloc/tcmalloc 模式
#else
#define PREFIX_SIZE (sizeof(size_t)) // libc 模式,手动存储大小
#endif
void *zmalloc(size_t size) {
void *ptr = malloc(size + PREFIX_SIZE);
if (!ptr) zmalloc_oom_handler(size);
#ifdef HAVE_MALLOC_SIZE
// jemalloc 模式:直接用 malloc_usable_size 获取实际大小
update_zmalloc_stat_alloc(zmalloc_size(ptr));
return ptr;
#else
// libc 模式:在前 PREFIX_SIZE 字节存储实际分配大小
*((size_t*)ptr) = size;
update_zmalloc_stat_alloc(size + PREFIX_SIZE);
return (char*)ptr + PREFIX_SIZE;
#endif
}
void zfree(void *ptr) {
if (ptr == NULL) return;
#ifdef HAVE_MALLOC_SIZE
update_zmalloc_stat_free(zmalloc_size(ptr));
free(ptr);
#else
void *realptr = (char*)ptr - PREFIX_SIZE;
size_t oldsize = *((size_t*)realptr);
update_zmalloc_stat_free(oldsize + PREFIX_SIZE);
free(realptr);
#endif
}
used_memory 统计
// 原子地更新内存使用量统计
// 使用线程本地缓存减少原子操作频率
#define update_zmalloc_stat_alloc(__n) do { \
size_t _n = (__n); \
if (_n & (sizeof(long) - 1)) _n += sizeof(long) - (_n & (sizeof(long) - 1)); \
atomicIncr(used_memory, _n); \
} while(0)
// 当前分配量(字节)
size_t zmalloc_used_memory(void) {
size_t um;
atomicGet(used_memory, um);
return um;
}
used_memory 与 RSS 的差异:
used_memory:Redis 认为自己分配的字节数(zmalloc 跟踪)used_memory_rss:操作系统分配给进程的实际物理内存(通过/proc/pid/smaps或getrusage)- 差值来源:内存碎片、分配器保留的未使用内存
24.3 jemalloc 三层架构
理解 jemalloc 的内部结构有助于理解 Redis 的内存行为。
层次结构
OS(操作系统)
│
│ mmap/brk(2MB 为单位)
▼
Chunk(块,2MB)
│
│ 按 size class 切分
▼
Slab(Slab,内部称 run)
│
│ Slab 内按固定大小切分
▼
Region(区域,实际返回给调用者的内存)
Arena:减少锁争用
jemalloc 默认创建 CPU数 × 4 个 Arena
Thread 1 ──→ Arena 0
Thread 2 ──→ Arena 1
Thread 3 ──→ Arena 2
Thread 4 ──→ Arena 0(循环分配)
每个 Arena 独立管理自己的 Chunk/Bin/Run
线程之间几乎不需要锁同步
通过 MALLOC_CONF 配置:
# 设置 Arena 数量(默认 CPU数×4)
MALLOC_CONF=narenas:8 redis-server redis.conf
# 查看 jemalloc 统计(需要编译时 enable stats)
redis-cli MEMORY MALLOC-STATS
Bin 与 Size Class
jemalloc 预定义了精细的 size class 序列,减少内碎片:
小对象(0–14KB):
8, 16, 32, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256,
320, 384, 448, 512, 640, 768, 896, 1024, 1280, 1536, 1792, 2048,
2560, 3072, 3584, 4096, 5120, 6144, 7168, 8192, 10240, 12288, 14336
中对象(14KB–4MB):按 2MB 页对齐
大对象(>4MB):直接 mmap
内碎片分析:
- 请求 100 字节 → 实际分配 112 字节 → 内碎片 12 字节(12%)
- 请求 200 字节 → 实际分配 224 字节 → 内碎片 24 字节(12%)
- glibc malloc:请求 100 字节 → 实际分配 128 字节 → 内碎片 28 字节(28%)
24.4 内存碎片率分析
INFO memory 关键指标
redis-cli INFO memory
关键字段解析:
# Memory
used_memory:1073741824 # 1GB: Redis 认为分配的内存
used_memory_human:1.00G
used_memory_rss:1610612736 # 1.5GB: OS 分配给进程的 RSS
used_memory_rss_human:1.50G
used_memory_peak:1073741824 # 历史峰值
used_memory_peak_human:1.00G
used_memory_peak_perc:100.00% # 当前/峰值比
used_memory_overhead:847249408 # Redis 内部开销(字典、对象头、事件循环等)
used_memory_startup:895776 # 启动时基础内存
used_memory_dataset:226492416 # 纯数据内存(used_memory - overhead)
used_memory_dataset_perc:21.09% # 数据占总分配比
allocator_allocated:1073815552 # 分配器实际分配量
allocator_active:1342177280 # 分配器活跃内存(含分配器保留)
allocator_resident:1610612736 # 分配器向OS申请的量(≈ RSS)
mem_fragmentation_ratio:1.50 # = used_memory_rss / used_memory
mem_fragmentation_bytes:536870912 # 碎片字节数
mem_not_counted_for_evict:0 # 不参与 maxmemory 计算的内存
mem_replication_backlog:1048576 # 复制积压缓冲区占用
mem_total_replication_buffers:2097152 # 所有复制缓冲区合计
mem_clients_slaves:0 # 从库客户端占用
mem_clients_normal:20512 # 普通客户端占用
mem_cluster_links:0 # 集群连接占用
mem_aof_buffer:8 # AOF 缓冲区占用
mem_allocator:jemalloc-5.3.0 # 当前使用的分配器
active_defrag_running:0 # 在线碎片整理是否运行中
lazyfree_pending_objects:0 # 待惰性释放的对象数量
lazyfreed_objects:0 # 已惰性释放的对象数量
碎片率诊断
mem_fragmentation_ratio 解读:
< 1.0 → 使用了 swap(严重!内存不足)
1.0–1.1 → 健康
1.1–1.5 → 轻微碎片(可接受)
1.5–2.0 → 碎片较重,考虑开启 activedefrag
> 2.0 → 碎片严重,需要立即处理
碎片产生原因:
- 大量不同大小的对象混合存储
- 频繁的 SET/DEL 操作(内存反复申请和释放)
- 数据过期删除(删除后的空洞)
- 大 value 直接 mmap 后释放(RSS 不立即归还 OS)
24.5 activedefrag 在线碎片整理
Redis 4.0 引入的 activedefrag 功能允许在不重启服务的情况下整理内存碎片。
工作原理
// defrag.c(Redis 内部实现)
// 核心思路:
// 1. 扫描数据库中的每个 key
// 2. 对每个 value,检查其内存地址是否在高碎片区域
// 3. 如果是,重新分配一块新内存,将数据拷贝过去
// 4. 更新所有指向旧内存的指针(redisObject.ptr)
// 5. 释放旧内存 → jemalloc 回收碎片化的 slab
void activeDefragCycle(void) {
// 控制 CPU 使用率:根据碎片率动态调整占用比例
// active-defrag-cycle-min ~ active-defrag-cycle-max
size_t hits_per_second = computeDefragHitsPerSecond();
// 扫描当前 DB 的字典
// dictScanDefrag 是专门为 defrag 设计的扫描函数
// 每次扫描一个 bucket,对其中的 entry 调用 defragCallback
unsigned long cursor = dictScanDefrag(server.db[current_db].dict,
cursor,
defragCallback,
&server.db[current_db]);
}
// 重新分配单个对象
void *activeDefragAlloc(void *ptr) {
size_t size = zmalloc_size(ptr);
void *newptr;
// 检查该内存地址所在的 jemalloc slab 是否值得整理
if (!je_get_defrag_hint(ptr)) return NULL;
// 分配新内存并拷贝
newptr = zmalloc(size);
if (newptr == NULL) return NULL;
memcpy(newptr, ptr, zmalloc_size(newptr));
zfree(ptr);
return newptr;
}
配置详解
# redis.conf
# 是否开启在线碎片整理(默认 no)
activedefrag yes
# 碎片字节数超过此值才开始整理(默认 100mb)
active-defrag-ignore-bytes 100mb
# 碎片率(mem_fragmentation_ratio - 1)× 100 超过此值才开始(默认 10 = 10%)
active-defrag-threshold-lower 10
# 碎片率超过此值时以最大 CPU 限制运行(默认 100 = ratio 2.0)
active-defrag-threshold-upper 100
# 碎片整理最少占用 CPU 百分比(默认 1%)
active-defrag-cycle-min 1
# 碎片整理最多占用 CPU 百分比(默认 25%)
active-defrag-cycle-max 25
# 每次扫描最多整理的 set/hash/zset/list 的 listpack 节点数
active-defrag-max-scan-fields 1000
整理期间的停顿
activedefrag 并非完全无停顿:
移动小对象(< 1KB): 停顿 < 1µs(可忽略)
移动中等对象(< 64KB):停顿 < 10µs
移动大对象(> 1MB): 停顿 1–10ms(需要注意)
建议:
- 低峰期将 active-defrag-cycle-max 调高(50–75%)加快整理
- 业务高峰期将 active-defrag-cycle-max 降低(5–10%)减少影响
- 对延迟敏感业务(P99 < 1ms 要求):不建议开启 activedefrag
改用定期重启 slave 后切主的方式处理碎片
24.6 lazyfree 后台惰性释放
大对象的同步释放(free())会阻塞主线程数毫秒甚至数十毫秒。Redis 4.0 引入 lazyfree 将释放操作异步化。
DEL vs UNLINK
# DEL:同步删除(大对象会阻塞)
DEL bigkey
# UNLINK:异步删除(主线程只断开引用,bio 线程真正释放)
UNLINK bigkey
# 测试对比
redis-cli DEL biglist # 包含 100万个元素的 list,可能阻塞 100ms+
redis-cli UNLINK biglist # 立即返回,后台异步释放
lazyfree 源码流程
// lazyfree.c
// UNLINK 命令的核心:将释放任务提交给 bio 线程
int dbAsyncDelete(redisDb *db, robj *key) {
// 小对象直接同步删除(成本极低)
if (dictSize(db->expires) > 0)
dictDelete(db->expires, key->ptr);
// 从 dict 中摘除 entry(但不立即释放 value)
dictEntry *de = dictUnlink(db->dict, key->ptr);
if (de) {
robj *val = dictGetVal(de);
// 计算释放成本(对象大小)
size_t free_effort = lazyfreeGetFreeEffort(key, val);
// 成本超过阈值 → 异步释放
if (free_effort > LAZYFREE_THRESHOLD &&
val->refcount == 1) {
atomicIncr(lazyfree_objects, 1);
// 提交到 BIO_LAZY_FREE 队列
bioCreateLazyFreeJob(lazyfreeFreeObject, 1, val);
dictSetVal(db->dict, de, NULL); // 防止 dictFreeUnlinkedEntry 释放
}
dictFreeUnlinkedEntry(db->dict, de);
}
// 触发 key space 通知
if (server.lazyfree_lazy_server_del)
return de != NULL;
return C_ERR; // 让调用者用同步删除作为降级
}
// bio 线程执行的实际释放函数
void lazyfreeFreeObject(void *args[]) {
robj *o = (robj *) args[0];
decrRefCount(o); // 减少引用计数,如果归零则真正释放
atomicDecr(lazyfree_objects, 1);
}
lazyfreeGetFreeEffort:释放成本估算
size_t lazyfreeGetFreeEffort(robj *key, robj *obj) {
if (obj->type == OBJ_LIST) {
quicklist *ql = obj->ptr;
return ql->len; // list 元素数量
} else if (obj->type == OBJ_SET && obj->encoding == OBJ_ENCODING_HT) {
dict *ht = obj->ptr;
return dictSize(ht); // set 元素数量
} else if (obj->type == OBJ_ZSET && obj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = obj->ptr;
return zs->zsl->length; // zset 元素数量
} else if (obj->type == OBJ_HASH && obj->encoding == OBJ_ENCODING_HT) {
dict *ht = obj->ptr;
return dictSize(ht); // hash 元素数量
} else if (obj->type == OBJ_STREAM) {
size_t effort = 0;
stream *s = obj->ptr;
effort += s->length; // stream entry 数量
effort += raxSize(s->cgroups); // 消费者组数量
return effort;
} else {
return 1; // string 等小对象,认为成本=1
}
}
#define LAZYFREE_THRESHOLD 64 // 成本超过64才异步释放
lazyfree 配置
# redis.conf
# UNLINK 命令(主动异步删除)—— 由命令本身决定,不受配置影响
# 被动过期删除是否异步
lazyfree-lazy-expire yes # 默认 no,建议 yes
# server 内部触发的删除(如 RENAME 时删除旧 key)是否异步
lazyfree-lazy-server-del yes # 默认 no,建议 yes
# 主从复制中,从库接收 FLUSHDB/FLUSHALL 是否异步清空
replica-lazy-flush yes # 默认 no,建议 yes(从库 FLUSHDB 阻塞问题)
# 内存淘汰是否异步
lazyfree-lazy-eviction yes # 默认 no,建议 yes
# 用户主动执行 FLUSHDB/FLUSHALL 是否异步
lazyfree-lazy-user-del yes # 默认 no
lazyfree-lazy-user-flush yes # 默认 no(FLUSHDB ASYNC 的配置化版本)
24.7 bio.c:后台 I/O 线程
bio.c 实现了 Redis 的后台线程机制,用于执行不适合在主线程同步执行的操作。
// bio.c
// 三类后台任务
#define BIO_CLOSE_FILE 0 // 关闭文件描述符(异步 close())
#define BIO_AOF_FSYNC 1 // AOF 文件 fsync(everysec 模式)
#define BIO_LAZY_FREE 2 // 惰性释放对象
// 任务结构
struct bio_job {
time_t time; // 提交时间
void (*free_fn)(void *args[]); // 释放函数
void *args[3]; // 传给 free_fn 的参数
};
// 每类任务有独立的队列和线程
static pthread_t bio_threads[BIO_NUM_OPS];
static pthread_mutex_t bio_mutex[BIO_NUM_OPS];
static pthread_cond_t bio_newjob_cond[BIO_NUM_OPS];
static list *bio_jobs[BIO_NUM_OPS];
// bio 线程主循环
void *bioProcessBackgroundJobs(void *arg) {
int type = (unsigned long) arg;
// 降低优先级,不与主线程竞争 CPU
struct sched_param sp;
sp.sched_priority = sched_get_priority_min(SCHED_RR);
pthread_setschedparam(pthread_self(), SCHED_RR, &sp);
while (1) {
listNode *ln = listFirst(bio_jobs[type]);
if (ln == NULL) {
// 队列空 → 等待新任务
pthread_cond_wait(&bio_newjob_cond[type], &bio_mutex[type]);
continue;
}
// 执行任务
struct bio_job *job = ln->value;
listDelNode(bio_jobs[type], ln);
pthread_mutex_unlock(&bio_mutex[type]);
// 根据任务类型执行不同操作
if (type == BIO_CLOSE_FILE) {
close((long)job->args[0]);
} else if (type == BIO_AOF_FSYNC) {
redis_fsync((long)job->args[0]);
} else if (type == BIO_LAZY_FREE) {
job->free_fn(job->args); // 调用 lazyfreeFreeObject 等
}
zfree(job);
pthread_mutex_lock(&bio_mutex[type]);
}
}
24.8 内存使用分析工具
单个 key 内存分析
# 查看单个 key 的内存占用
redis-cli MEMORY USAGE mykey
# 指定采样深度(对于 list/hash 等,采样更多元素)
redis-cli MEMORY USAGE mykey SAMPLES 100
# 诊断建议
redis-cli MEMORY DOCTOR
# 输出示例:
# - High peak to current ratio: Your peak used memory is 5.01G, current is 1.00G.
# Consider using MEMORY PURGE to reclaim memory.
# - High total RSS: Your RSS is 1.5G, but used_memory is 1.0G.
# Check for memory fragmentation using 'info memory'.
大 key 扫描
# 扫描最大的 key(按编码大小)
redis-cli --bigkeys
# 输出示例:
# Biggest string found 'user:profile:12345' has 51200 bytes
# Biggest list found 'events:queue' has 100000 items
# Biggest hash found 'product:catalog' has 50000 fields
# 内存分布扫描(每个 key 的估算内存)
redis-cli --memkeys
# 热 key 分析(需要 Redis 4.0+)
redis-cli --hotkeys
OBJECT 命令
# 查看对象编码
OBJECT ENCODING mykey # listpack, skiplist, embstr, raw, int, ...
# 查看 LRU 空闲时间(秒)
OBJECT IDLETIME mykey
# 查看引用计数
OBJECT REFCOUNT mykey
# 查看对象频率(LFU 模式)
OBJECT FREQ mykey
内存清理操作
# 触发内存 purge(将碎片内存归还 OS,会短暂阻塞)
MEMORY PURGE
# 查看 jemalloc 内部统计
MEMORY MALLOC-STATS
# 动态调整 active defrag 参数
CONFIG SET active-defrag-cycle-max 50
CONFIG SET activedefrag yes
24.9 生产内存优化建议
配置建议
# 1. 开启 lazyfree(减少大对象删除阻塞)
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
lazyfree-lazy-eviction yes
replica-lazy-flush yes
# 2. 碎片严重时开启 activedefrag
activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100
active-defrag-cycle-min 1
active-defrag-cycle-max 25
# 3. 使用 jemalloc(Linux 已默认,无需额外配置)
# 4. 禁用 THP(透明大页,会导致 fork 时 COW 开销骤增)
# 在 /etc/rc.local 中添加:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
数据结构优化
# 使用 listpack 编码(小对象)比 skiplist/hashtable 节省 50–70% 内存
# 确保数据量在阈值内:
# Hash(listpack 阈值,默认值)
CONFIG GET hash-max-listpack-entries # 128 个字段以内
CONFIG GET hash-max-listpack-value # 每个字段值 64 字节以内
# ZSet(listpack 阈值)
CONFIG GET zset-max-listpack-entries # 128 个元素以内
CONFIG GET zset-max-listpack-value # 每个成员 64 字节以内
# Set(intset 阈值)
CONFIG GET set-max-intset-entries # 512 个整数以内
# List(listpack 阈值)
CONFIG GET list-max-listpack-size # 128 个元素以内
内存监控告警
# 碎片率告警(Prometheus + redis_exporter)
redis_mem_fragmentation_ratio > 1.5 # 警告
redis_mem_fragmentation_ratio > 2.0 # 严重
# 内存使用率告警
redis_memory_used_bytes / redis_memory_max_bytes > 0.85 # 警告
# lazyfree 积压告警
redis_lazyfree_pending_objects > 10000 # 大量待释放对象
本章小结
- Redis 支持 jemalloc/libc/tcmalloc 三种分配器,Linux 下 jemalloc 为默认推荐
zmalloc层跟踪used_memory,与 OS RSS 的差值反映内存碎片和分配器保留- jemalloc 通过 Arena(per-CPU)、Bin(size class)、Chunk(2MB block)三层架构实现低碎片高并发
mem_fragmentation_ratio> 1.5 时考虑开启activedefrag,> 2.0 需立即处理activedefrag在线整理:扫描 key,重新分配碎片化 value 的内存,更新指针,释放旧内存lazyfree/UNLINK将大对象释放异步化,避免主线程阻塞 ms 级延迟bio.c维护3个后台线程:文件关闭、AOF fsync、惰性释放,所有耗时 I/O 操作都经由 bio 异步化- 生产建议:开启所有 lazyfree 选项 + activedefrag,禁用 THP,监控碎片率和 lazyfree 积压