前言

最近要做兼容 Redis 协议的存储组件,遂学习 Redis 3.0 源码,文章力求从 high-level 捋清楚各模块的设计,并简要记录其实现。参考《Redis 设计与实现》


内存管理库

在预编译阶段按平台定义MALLOC,USE_JEMALLOC等宏,来选定内存管理库:

1
2
3
4
5
6
7
uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
ifeq ($(uname_S),Linux)
MALLOC=jemalloc
endif
ifeq ($(MALLOC),jemalloc)
FINAL_CFLAGS+= -DUSE_JEMALLOC -I../deps/jemalloc/include
endif

便于选定替换 malloc 的库函数,将现成的内存统计接口封装为 zmalloc_size

1
2
3
4
#if defined(USE_JEMALLOC)
#define malloc(size) je_malloc(size)
#define zmalloc_size(p) je_malloc_usable_size(p)
#endif

内存操作封装

  • 需求:redis 需准确统计内存用量,以实现maxmemory做 LRU 键清理、正确计算内存碎片率等功能
  • 问题:若仅 libc 可用,调用 malloc 后虽能算出分配的总字节数,但 free 时无法得知释放了多少字节
  • 解决:额外分配一个字长(对齐保证)即sizeof(size_t)字节作为 header,记录内存段的大小

zmalloc

1
2
3
4
5
6
7
8
9
10
11
12
13

void *zmalloc(size_t size) {
void *ptr = malloc(size+PREFIX_SIZE);
if (!ptr) zmalloc_oom_handler(size); // 全局函数指针
#ifdef HAVE_MALLOC_SIZE
update_zmalloc_stat_alloc(zmalloc_size(ptr));
return ptr;
#else
*((size_t*)ptr) = size; // 多分配 size_t,8B 来存 size 的值
update_zmalloc_stat_alloc(size+PREFIX_SIZE); // 累加本次分配字节数
return (char*)ptr+PREFIX_SIZE; // 右移跳过 header 返回可用内存起始地址
#endif
}

示例:libc 实现的zmalloc(10)内存布局如下,内存对齐会多分配 6B,将在内存头部PREFIX_SIZE中存入值 16

image-20190917211507245

函数宏:

  • update_zmalloc_stat_alloc:累加内存对齐后实际分配的内存大小;注意对 8 取余的位运算实现为&0111,即只取最后 3 位,效率高于 %,类比<<求二次方

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 对 _n 向上取整为 8 的倍数,冗余凑足 1 个字长,推算 malloc 实际分配的字节数
    #define update_zmalloc_stat_alloc(__n) do { \
    size_t _n = (__n); \
    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
    if (zmalloc_thread_safe) { \
    update_zmalloc_stat_add(_n); \
    } else { \
    used_memory += _n; \
    } \
    } while(0) // do-while 保证宏展开后作为整体执行;宏参数用 () unwrap 避免运算优先级错乱
  • update_zmalloc_stat_add:线程安全地更新已分配内存大小static size_t used_memory

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 若库版本支持则尽量使用原子操作
    #if defined(__ATOMIC_RELAXED)
    #define update_zmalloc_stat_add(__n) __atomic_add_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
    #else // 加互斥锁更新值
    #define update_zmalloc_stat_add(__n) do { \
    pthread_mutex_lock(&used_memory_mutex); \
    used_memory += (__n); \
    pthread_mutex_unlock(&used_memory_mutex); \
    } while(0)

zrealloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void *zrealloc(void *ptr, size_t size) {
void *realptr;
size_t oldsize;
void *newptr;
if (ptr == NULL) return zmalloc(size);

realptr = (char*)ptr-PREFIX_SIZE; // 左移跳过 header 定位 malloc 分配的内存起始地址
oldsize = *((size_t*)realptr);
newptr = realloc(realptr,size+PREFIX_SIZE);
if (!newptr) zmalloc_oom_handler(size);

*((size_t*)newptr) = size;
update_zmalloc_stat_free(oldsize); // 计算并累加 diff 字节数
update_zmalloc_stat_alloc(size);
return (char*)newptr+PREFIX_SIZE; // 同样右移跳过 header 返回
}

同理,其余zfree, zmalloc_size等都有先左移定位真实内存地址的操作


内存统计

Redis 命令 INFO 的输出中:

1
2
3
4
5
6
7
# Memory
used_memory:1039360 # zmalloc_used_memory # 返回 used_memory
used_memory_human:1015.00K
used_memory_rss:2256896 # zmalloc_get_rss # 分割 /proc/self/stat 读 RSS,统计包括共享库在内的实际使用物理内存大小
used_memory_rss_human:2.15M
mem_fragmentation_ratio:2.25 # 内存碎片率
mem_fragmentation_bytes:1251776

内存碎片率:RSS 与已分配内存的比例

1
2
3
float zmalloc_get_fragmentation_ratio(size_t rss) {
return (float) rss / zmalloc_used_memory();
}
  • 0~1:实际使用的物理内存少于申请的内存大小,即有部分内存被换入 swap 分区;比值越低,响应延迟越高(随机访问,磁盘 10ms 而内存 100ns,差五个量级);需及时扩容内存
  • 1.5~:碎片率超 50%,由于 key 长度调整、jemalloc 冗余分配等原因,导致内存申请但未使用,产生内存碎片;4.0 后可配置 active-defrag* 自动清理碎片
  • 1~1.5:正常

总结

Redis 的内存管理实现了跨平台封装,并服务于内存统计。在 libc 平台,zmalloc 在每段内存头部存入段大小,并在寻址时左移跳过该头部。维护全局变量used_memory并进行线程安全地读写;实现了 RSS 的读取并计算内存碎片率等统计功能