第 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 默认


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 的差异


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

内碎片分析


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  → 碎片严重,需要立即处理

碎片产生原因

  1. 大量不同大小的对象混合存储
  2. 频繁的 SET/DEL 操作(内存反复申请和释放)
  3. 数据过期删除(删除后的空洞)
  4. 大 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:同步删除(大对象会阻塞)
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  # 大量待释放对象

本章小结

本章评分
4.6  / 5  (6 评分)

💬 留言讨论