zmalloc演化

第一阶段:初始实现 (2009年第一个commit)

特点

  • 非常简单的实现,只有82行代码(有效也就41行)

  • 使用全局变量used_memory跟踪内存使用

  • 在分配的内存前添加size_t大小的前缀来存储分配大小

  • 支持基本的zmalloc、zrealloc、zfree、zstrdup函数

核心实现

static size_t used_memory = 0;

void *zmalloc(size_t size) {
    void *ptr = malloc(size+sizeof(size_t));
    *((size_t*)ptr) = size;
    used_memory += size+sizeof(size_t);
    return ptr+sizeof(size_t);
}

第二阶段:线程安全支持 (2010年)

关键改进

  • 引入zmalloc_thread_safe标志

  • 使用pthread_mutex保护内存计数器

  • 支持动态启用线程安全模式

  • 添加了zmalloc_enable_thread_safeness()函数

核心变化

static int zmalloc_thread_safe = 0;
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER;

#define increment_used_memory(_n) do { \
    if (zmalloc_thread_safe) { \
        pthread_mutex_lock(&used_memory_mutex);  \
        used_memory += _n; \
        pthread_mutex_unlock(&used_memory_mutex); \
    } else { \
        used_memory += _n; \
    } \
} while(0)

第三阶段:多分配器支持 (2011年)

重要特性

  • 支持jemalloc、tcmalloc、libc等多种分配器

  • 通过宏定义实现分配器切换

  • 添加了HAVE_MALLOC_SIZE支持

  • 引入了zmalloc_size()函数

核心改进

#if defined(USE_JEMALLOC)
#define ZMALLOC_LIB ("jemalloc-" __xstr(JEMALLOC_VERSION_MAJOR) "." __xstr(JEMALLOC_VERSION_MINOR))
#define JEMALLOC_MANGLE
#include <jemalloc/jemalloc.h>
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) JEMALLOC_P(malloc_usable_size)(p)
#endif

第四阶段:现代实现 (当前版本)

主要特性

  • 多线程优化的内存计数器(使用数组和线程本地存储)

  • 支持多种分配器的深度集成

  • 添加了usable size相关函数

  • 支持内存碎片统计和RSS监控

  • 添加了jemalloc特定的优化

核心架构

typedef struct used_memory_entry {
    redisAtomic long long used_memory;
    char padding[CACHE_LINE_SIZE - sizeof(long long)];
} used_memory_entry;

static __attribute__((aligned(CACHE_LINE_SIZE))) used_memory_entry used_memory[MAX_THREADS];
static __thread long my_thread_index = -1;

演化总结

  1. 从简单到复杂:从82行的简单实现发展到1000+行的复杂系统
  2. 线程安全演进:从单线程到多线程安全,再到线程本地优化
  3. 分配器支持扩展:从libc到支持jemalloc、tcmalloc等多种分配器
  4. 性能优化:从简单的全局计数器到缓存行对齐的线程本地存储
  5. 功能丰富化:从基本的内存分配到支持内存统计、碎片分析、RSS监控等

源码历程

first commit

ed9b544e

zmalloc.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#ifndef _ZMALLOC_H
#define _ZMALLOC_H

void *zmalloc(size_t size);
void *zrealloc(void *ptr, size_t size);
void *zfree(void *ptr);
char *zstrdup(const char *s);
size_t zmalloc_used_memory(void);

#endif /* _ZMALLOC_H */
zmalloc.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdlib.h>
#include <string.h>

static size_t used_memory = 0;

void *zmalloc(size_t size) {
    void *ptr = malloc(size+sizeof(size_t));  // 多分配 sizeof(size_t) 字节
    
    *((size_t*)ptr) = size;                   // 在开头存储实际请求的大小
    used_memory += size+sizeof(size_t);       // 更新已用内存统计
    return ptr+sizeof(size_t);                // 返回用户可用区域的指针
}

void *zrealloc(void *ptr, size_t size) {
    void *realptr;
    size_t oldsize;
    void *newptr;

    if (ptr == NULL) return zmalloc(size);
    realptr = ptr-sizeof(size_t);
    oldsize = *((size_t*)realptr);
    newptr = realloc(realptr,size+sizeof(size_t));
    if (!newptr) return NULL;

    *((size_t*)newptr) = size;
    used_memory -= oldsize;
    used_memory += size;
    return newptr+sizeof(size_t);
}

void zfree(void *ptr) {
    size_t *realptr = (size_t*)ptr - 1;  // 回退到size信息位置
    size_t oldsize = *realptr;           // 读取存储的大小
    used_memory -= oldsize + sizeof(size_t);
    free(realptr);                       // 释放整个内存块
}

char *zstrdup(const char *s) {
    size_t l = strlen(s)+1;
    char *p = zmalloc(l);

    memcpy(p,s,l);
    return p;
}

size_t zmalloc_used_memory(void) {
    return used_memory;
}

为什么要 size+sizeof(size_t)?

内存布局设计

+----------------+------------------+
|   size_t size  |   用户数据区域   |
|   (8字节)      |   (size字节)     |
+----------------+------------------+
^                ^
ptr              ptr+sizeof(size_t) (返回给用户)

具体作用

  • 存储元数据:在用户数据前面预留 sizeof(size_t) 字节,用来存储用户实际请求的内存大小

  • 内存跟踪:通过这个存储的大小信息,可以实现:

    • 统计已分配内存总量(used_memory)

    • 在 zfree 时知道要释放多少内存

    • 内存泄漏检测和调试

使用场景

// 用户调用
char *data = zmalloc(100);

// 实际分配的内存布局:
// [8字节的size信息][100字节的用户数据]
//  ^                ^
//  ptr              data (返回给用户)

size+sizeof(size_t) 是为了在用户数据前面预留空间存储元数据,实现内存使用统计和正确的释放机制。这是很多内存分配器的常见设计模式,既提供了内存跟踪功能,又保持了用户接口的简洁性

ANSI-C compatibility

a4d1ba9a

--- a/zmalloc.c
+++ b/zmalloc.c
void *zmalloc(size_t size) {
     *((size_t*)ptr) = size;
     used_memory += size+sizeof(size_t);
-    return ptr+sizeof(size_t);
+    return (char*)ptr+sizeof(size_t);
 }

void *zrealloc(void *ptr, size_t size) {
     void *newptr;

     if (ptr == NULL) return zmalloc(size);
-    realptr = ptr-sizeof(size_t);
+    realptr = (char*)ptr-sizeof(size_t);
     oldsize = *((size_t*)realptr);
     newptr = realloc(realptr,size+sizeof(size_t));
     if (!newptr) return NULL;
     
zrealloc(void *ptr, size_t size) {
     *((size_t*)newptr) = size;
     used_memory -= oldsize;
     used_memory += size;
-    return newptr+sizeof(size_t);
+    return (char*)newptr+sizeof(size_t);
 }

void zfree(void *ptr) {

     size_t oldsize;

     if (ptr == NULL) return;
-    realptr = ptr-sizeof(size_t);
+    realptr = (char*)ptr-sizeof(size_t);
     oldsize = *((size_t*)realptr);
     used_memory -= oldsize+sizeof(size_t);
     free(realptr);

ANSI-C兼容性问题:

  • 问题:ANSI-C标准不允许对void*指针进行算术运算

  • 解决方案:将void转换为char后再进行指针算术运算

  • 原因:char*是唯一可以进行字节级指针算术运算的指针类型

为什么转换成 (char*) 更符合 ansi c

  1. 类型安全char* 是一个具体的数据类型,可以被解引用并用于访问内存中的数据。在 ANSI C 中,char* 通常用于表示字符串,但也可以用于表示任意字节的内存块
  2. 兼容性:在某些情况下,可能需要将内存块作为字符串来处理,比如使用标准库函数如 strlenstrcpy 等。将 void* 转换为 char* 可以提供这种兼容性
  3. 编码风格:在某些编码风格或项目中,可能习惯于使用 char* 来表示内存块,这样可以在不改变其他代码的情况下重用现有的代码
  4. 避免警告:在某些编译器或严格的编译选项下,直接使用 void* 可能会引起警告,因为编译器无法确定内存块的大小。转换为 char* 可以消除这种警告
  5. 方便操作char* 可以方便地进行字节操作,比如通过索引来访问和修改内存块中的特定字节

实际使用中,应该根据上下文来决定是否需要这种转换。如果只是简单地分配内存,而不需要进行字节操作或与字符串相关的操作,那么这种转换可能是不必要的

ANSI-C兼容性的重要性

跨平台兼容性

  • 不同编译器的严格程度不同

  • 某些嵌入式系统只支持ANSI-C

编译器兼容性

  • 老版本的GCC可能不支持C99特性

  • 某些商业编译器对标准更严格

代码可移植性

  • 确保代码在各种环境下都能编译

  • 减少因编译器差异导致的问题

潜在影响

  • ⚠️ 代码稍微冗长了一些(需要显式类型转换)

  • ⚠️ 需要更多的类型转换代码

zmalloc fix, return NULL or real malloc failure

8d196eba

--- a/zmalloc.c
+++ b/zmalloc.c
void *zmalloc(size_t size) {
     void *ptr = malloc(size+sizeof(size_t));

+    if (!ptr) return NULL;
     *((size_t*)ptr) = size;
     used_memory += size+sizeof(size_t);
     return (char*)ptr+sizeof(size_t);

macosx malloc_size

ec93bba3

架构演进

条件编译模式

  • 建立了HAVE_MALLOC_SIZE模式

  • 为后续支持其他分配器(jemalloc, tcmalloc)奠定了基础

--- a/zmalloc.c
+++ b/zmalloc.c
@@ -31,23 +31,45 @@
 #include <stdlib.h>
 #include <string.h>

+#ifdef __APPLE__
+#include <malloc/malloc.h>
+#define HAVE_MALLOC_SIZE
+#define redis_malloc_size(p) malloc_size(p)
+#endif
+
 static size_t used_memory = 0;

 void *zmalloc(size_t size) {
     void *ptr = malloc(size+sizeof(size_t));

     if (!ptr) return NULL;
+#ifdef HAVE_MALLOC_SIZE
+    used_memory += redis_malloc_size(ptr);
+    return ptr;
+#else
     *((size_t*)ptr) = size;
     used_memory += size+sizeof(size_t);
     return (char*)ptr+sizeof(size_t);
+#endif
 }

 void *zrealloc(void *ptr, size_t size) {
+#ifndef HAVE_MALLOC_SIZE
     void *realptr;
+#endif
     size_t oldsize;
     void *newptr;

     if (ptr == NULL) return zmalloc(size);
+#ifdef HAVE_MALLOC_SIZE
+    oldsize = redis_malloc_size(ptr);
+    newptr = realloc(ptr,size);
+    if (!newptr) return NULL;
+
+    used_memory -= oldsize;
+    used_memory += redis_malloc_size(newptr);
+    return newptr;
+#else
     realptr = (char*)ptr-sizeof(size_t);
     oldsize = *((size_t*)realptr);
     newptr = realloc(realptr,size+sizeof(size_t));
@@ -57,17 +79,25 @@ void *zrealloc(void *ptr, size_t size) {
     used_memory -= oldsize;
     used_memory += size;
     return (char*)newptr+sizeof(size_t);
+#endif
 }

 void zfree(void *ptr) {
+#ifndef HAVE_MALLOC_SIZE
     void *realptr;
     size_t oldsize;
+#endif

     if (ptr == NULL) return;
+#ifdef HAVE_MALLOC_SIZE
+    used_memory -= redis_malloc_size(ptr);
+    free(ptr);
+#else
     realptr = (char*)ptr-sizeof(size_t);
     oldsize = *((size_t*)realptr);
     used_memory -= oldsize+sizeof(size_t);
     free(realptr);
+#endif
 }

config.h

dde65f3f

配置系统重构

  • 创建了config.h文件来集中管理平台特定的配置
config.h
#ifndef __CONFIG_H
#define __CONFIG_H

/* malloc_size() */
#ifdef __APPLE__
#include <malloc/malloc.h>
#define HAVE_MALLOC_SIZE
#define redis_malloc_size(p) malloc_size(p)
#endif

/* define redis_fstat to fstat or fstat64() */
#ifdef __APPLE__
#define redis_fstat fstat64
#define redis_stat stat64
#else
#define redis_fstat fstat
#define redis_stat stat
#endif

#endif
--- a/zmalloc.c
+++ b/zmalloc.c

-#ifdef __APPLE__
-#include <malloc/malloc.h>
-#define HAVE_MALLOC_SIZE
-#define redis_malloc_size(p) malloc_size(p)
-#endif
+#include "config.h"

oom check

6b47e12e

--- a/zmalloc.c
+++ b/zmalloc.c

+#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include "config.h"

 static size_t used_memory = 0;

+static void zmalloc_oom(size_t size) {
+    fprintf(stderr, "zmalloc: Out of memory trying to allocate %lu bytes\n",
+        size);
+    fflush(stderr);
+    abort();
+}
+
 void *zmalloc(size_t size) {
     void *ptr = malloc(size+sizeof(size_t));

-    if (!ptr) return NULL;
+    if (!ptr) zmalloc_oom(size);
 #ifdef HAVE_MALLOC_SIZE
     used_memory += redis_malloc_size(ptr);
     return ptr;
void *zrealloc(void *ptr, size_t size) {
 #ifdef HAVE_MALLOC_SIZE
     oldsize = redis_malloc_size(ptr);
     newptr = realloc(ptr,size);
-    if (!newptr) return NULL;
+    if (!newptr) zmalloc_oom(size);

     used_memory -= oldsize;
     used_memory += redis_malloc_size(newptr);
void *zrealloc(void *ptr, size_t size) {
     realptr = (char*)ptr-sizeof(size_t);
     oldsize = *((size_t*)realptr);
     newptr = realloc(realptr,size+sizeof(size_t));
-    if (!newptr) return NULL;
+    if (!newptr) zmalloc_oom(size);

     *((size_t*)newptr) = size;
     used_memory -= oldsize;

设计哲学确立

确立了内存不足即终止的内存管理策略

Redis is not designed to recover from out of memory conditions

这个commit体现了Redis的一个重要设计原则:

内存不足时立即终止

  • 不尝试优雅降级

  • 不进行内存回收尝试

  • 直接调用abort()终止程序

假设内存充足

  • Redis假设运行环境有足够的内存

  • 如果内存不足,程序直接崩溃比继续运行更安全

著名的 PREFIX_SIZE

d8b5f18f

--- a/zmalloc.c
+++ b/zmalloc.c

+#if defined(__sun)
+#define PREFIX_SIZE sizeof(long long)
+#else
+#define PREFIX_SIZE sizeof(size_t)
+#endif
+

 void *zmalloc(size_t size) {
-    void *ptr = malloc(size+sizeof(size_t));
+    void *ptr = malloc(size+PREFIX_SIZE);

     if (!ptr) zmalloc_oom(size);
 #ifdef HAVE_MALLOC_SIZE
 void *zmalloc(size_t size) {
     return ptr;
 #else
     *((size_t*)ptr) = size;
-    used_memory += size+sizeof(size_t);
-    return (char*)ptr+sizeof(size_t);
+    used_memory += size+PREFIX_SIZE;
+    return (char*)ptr+PREFIX_SIZE;
 #endif
 }

 void *zrealloc(void *ptr, size_t size) {
     used_memory += redis_malloc_size(newptr);
     return newptr;
 #else
-    realptr = (char*)ptr-sizeof(size_t);
+    realptr = (char*)ptr-PREFIX_SIZE;
     oldsize = *((size_t*)realptr);
-    newptr = realloc(realptr,size+sizeof(size_t));
+    newptr = realloc(realptr,size+PREFIX_SIZE);
     if (!newptr) zmalloc_oom(size);

     *((size_t*)newptr) = size;
     used_memory -= oldsize;
     used_memory += size;
-    return (char*)newptr+sizeof(size_t);
+    return (char*)newptr+PREFIX_SIZE;
 #endif
 }

 void zfree(void *ptr) {
     used_memory -= redis_malloc_size(ptr);
     free(ptr);
 #else
-    realptr = (char*)ptr-sizeof(size_t);
+    realptr = (char*)ptr-PREFIX_SIZE;
     oldsize = *((size_t*)realptr);
-    used_memory -= oldsize+sizeof(size_t);
+    used_memory -= oldsize+PREFIX_SIZE;
     free(realptr);
 #endif
 }

Solaris平台兼容性修复

早期开发中重要的跨平台兼容性改进,专门针对Solaris系统进行了优化

核心问题:Solaris系统的内存对齐要求

问题背景

Solaris系统对内存对齐有特殊要求(数据必须按其自然对齐方式访问),如

  • long long(64位)必须从8字节对齐的地址开始
  • int(32位)必须从4字节对齐的地址开始 如果未对齐访问,SPARC会触发总线错误(Bus Error),导致程序崩溃

硬件实现原因

SPARC的早期设计(如SPARC v7/v8)使用简单的总线协议和加载/存储指令,依赖对齐来简化硬件设计。未对齐访问需要多次内存操作,但SPARC选择直接禁止它,将责任交给编译器(通过填充或对齐指令)

性能优化

对齐访问能最大化内存带宽利用率。SPARC通常用于高性能计算和服务器领域,对齐要求有助于避免硬件处理未对齐时的性能损耗

实际影响

  • 编程差异

    在SPARC上,未对齐的指针强制转换(如char*long long*)会崩溃,而x86可能仅性能下降

  • 编译器行为

    SPARC的编译器(如GCC)会插入填充字节(padding)或生成对齐指令(如.align 8),而x86编译器通常忽略对齐(除非显式指定-malign-double等)

现代演进

  • SPARC v9

    后续版本(如UltraSPARC)支持部分未对齐访问,但仍有性能惩罚,建议保持对齐

  • x86的SSE/AVX

    SIMD指令(如MOVAPS)要求对齐,表明x86在特定场景下也需对齐优化

关键差异总结

特性 SPARC x86
对齐要求 严格(崩溃于未对齐访问) 宽松(硬件自动处理)
硬件复杂度 简单,依赖编译器保证对齐 复杂,内置未对齐访问支持
性能影响 对齐时性能最优 未对齐时性能下降
典型应用场景 服务器、高性能计算 通用计算(桌面/移动)

总结

SPARC的严格对齐反映了RISC架构对硬件简单性和确定性的追求,而x86的宽松对齐源于CISC的兼容性和灵活性设计

理解这些差异有助于编写可移植的高效代码(尤其在嵌入式或跨平台开发中)

解决方案:引入PREFIX_SIZE宏

#if defined(__sun)
#define PREFIX_SIZE sizeof(long long)  // Solaris使用long long对齐
#else
#define PREFIX_SIZE sizeof(size_t)     // 其他系统使用size_t对齐
#endif

void *zmalloc(size_t size) {
    void *ptr = malloc(size+PREFIX_SIZE);  // 使用动态的PREFIX_SIZE
    // ...
    return (char*)ptr+PREFIX_SIZE;
}

thread safe counter

4ad37480

--- a/zmalloc.c
+++ b/zmalloc.c

+#define increment_used_memory(_n) do { \
+    if (zmalloc_thread_safe) { \
+        pthread_mutex_lock(&used_memory_mutex);  \
+        used_memory += _n; \
+        pthread_mutex_unlock(&used_memory_mutex); \
+    } else { \
+        used_memory += _n; \
+    } \
+} while(0)
+
+#define decrement_used_memory(_n) do { \
+    if (zmalloc_thread_safe) { \
+        pthread_mutex_lock(&used_memory_mutex);  \
+        used_memory -= _n; \
+        pthread_mutex_unlock(&used_memory_mutex); \
+    } else { \
+        used_memory -= _n; \
+    } \
+} while(0)
+
 static size_t used_memory = 0;
+static int zmalloc_thread_safe = 0;
 pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER;

 static void zmalloc_oom(size_t size) {
 void *zmalloc(size_t size) {

     if (!ptr) zmalloc_oom(size);
 #ifdef HAVE_MALLOC_SIZE
-    used_memory += redis_malloc_size(ptr);
+    increment_used_memory(redis_malloc_size(ptr));
     return ptr;
 #else
     *((size_t*)ptr) = size;
-    used_memory += size+PREFIX_SIZE;
+    increment_used_memory(size+PREFIX_SIZE);
     return (char*)ptr+PREFIX_SIZE;
 #endif
 }
 
 void *zrealloc(void *ptr, size_t size) {
     newptr = realloc(ptr,size);
     if (!newptr) zmalloc_oom(size);

-    used_memory -= oldsize;
-    used_memory += redis_malloc_size(newptr);
+    decrement_used_memory(oldsize);
+    increment_used_memory(redis_malloc_size(newptr));
     return newptr;
 #else
     realptr = (char*)ptr-PREFIX_SIZE;

void *zrealloc(void *ptr, size_t size) {
     if (!newptr) zmalloc_oom(size);

     *((size_t*)newptr) = size;
-    used_memory -= oldsize;
-    used_memory += size;
+    decrement_used_memory(oldsize);
+    increment_used_memory(size);
     return (char*)newptr+PREFIX_SIZE;
 #endif
 }

void zfree(void *ptr) {

     if (ptr == NULL) return;
 #ifdef HAVE_MALLOC_SIZE
-    used_memory -= redis_malloc_size(ptr);
+    decrement_used_memory(redis_malloc_size(ptr));
     free(ptr);
 #else
     realptr = (char*)ptr-PREFIX_SIZE;
     oldsize = *((size_t*)realptr);
-    used_memory -= oldsize+PREFIX_SIZE;
+    decrement_used_memory(oldsize+PREFIX_SIZE);
     free(realptr);
 #endif
 }

 size_t zmalloc_used_memory(void) {
-    return used_memory;
+    size_t um;
+
+    if (zmalloc_thread_safe) pthread_mutex_lock(&used_memory_mutex);
+    um = used_memory;
+    if (zmalloc_thread_safe) pthread_mutex_unlock(&used_memory_mutex);
+    return um;
+}
+
+void zmalloc_enable_thread_safeness(void) {
+    zmalloc_thread_safe = 1;
 }

引入了线程安全的内存管理机制

内存对齐

d3277ecd

--- a/zmalloc.c
+++ b/zmalloc.c
@@ -40,7 +40,9 @@

-#define increment_used_memory(_n) do { \
+#define increment_used_memory(__n) do { \
+    size_t _n = (__n); \
+    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
     if (zmalloc_thread_safe) { \
         pthread_mutex_lock(&used_memory_mutex);  \
         used_memory += _n; \
@@ -50,7 +52,9 @@
     } \
 } while(0)

-#define decrement_used_memory(_n) do { \
+#define decrement_used_memory(__n) do { \
+    size_t _n = (__n); \
+    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
     if (zmalloc_thread_safe) { \
         pthread_mutex_lock(&used_memory_mutex);  \
         used_memory -= _n; \

内存对齐的精确计算

  • 早期Redis用size+对齐的方式估算内存,因为没有办法获得实际分配的大小

  • 但随着主流分配器都支持malloc_usable_size/je_malloc_usable_size等API,Redis可以直接获取真实分配量,不再需要手动对齐补齐

对齐计算逻辑

if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1));

分解理解

  1. sizeof(long)-1:
  • 在32位系统:4-1 = 3 (二进制: 11)

  • 在64位系统:8-1 = 7 (二进制: 111)

  1. _n&(sizeof(long)-1):
  • 获取_n在sizeof(long)边界内的偏移量

  • 如果_n已经是sizeof(long)的倍数,结果为0

  • 否则结果为需要补齐的字节数

  1. sizeof(long)-(_n&(sizeof(long)-1)):
  • 计算需要补齐到下一个sizeof(long)边界的字节数

具体例子

32位系统(sizeof(long) = 4)

原始大小 二进制 &3结果 补齐字节数 最终大小
1 001 1 4-1=3 4
2 010 2 4-2=2 4
3 011 3 4-3=1 4
4 100 0 0 4
5 101 1 4-1=3 8

64位系统(sizeof(long) = 8)

原始大小 二进制 &7结果 补齐字节数 最终大小
1 001 1 8-1=7 8
7 111 7 8-7=1 8
8 1000 0 0 8
9 1001 1 8-1=7 16

zcalloc

399f2f40

用 calloc 替换 malloc + memset 的组合,以提高内存分配效率

--- a/src/dict.c
+++ b/src/dict.c
int dictExpand(dict *d, unsigned long size)
     if (dictIsRehashing(d) || d->ht[0].used > size)
         return DICT_ERR;

+    /* Allocate the new hashtable and initialize all pointers to NULL */
     n.size = realsize;
     n.sizemask = realsize-1;
-    n.table = zmalloc(realsize*sizeof(dictEntry*));
+    n.table = zcalloc(realsize*sizeof(dictEntry*));
     n.used = 0;

-    /* Initialize all the pointers to NULL */
-    memset(n.table, 0, realsize*sizeof(dictEntry*));

--- a/src/vm.c
+++ b/src/vm.c
 void vmInit(void) {
     } else {
         redisLog(REDIS_NOTICE,"Swap file allocated with success");
     }
-    server.vm_bitmap = zmalloc((server.vm_pages+7)/8);
+    server.vm_bitmap = zcalloc((server.vm_pages+7)/8);
     redisLog(REDIS_VERBOSE,"Allocated %lld bytes page table for %lld pages",
         (long long) (server.vm_pages+7)/8, server.vm_pages);
-    memset(server.vm_bitmap,0,(server.vm_pages+7)/8);


--- a/src/zmalloc.c
+++ b/src/zmalloc.c

+void *zcalloc(size_t size) {
+    void *ptr = calloc(1, size+PREFIX_SIZE);
+
+    if (!ptr) zmalloc_oom(size);
+#ifdef HAVE_MALLOC_SIZE
+    increment_used_memory(redis_malloc_size(ptr));
+    return ptr;
+#else
+    *((size_t*)ptr) = size;
+    increment_used_memory(size+PREFIX_SIZE);
+    return (char*)ptr+PREFIX_SIZE;
+#endif
+}

--- a/src/zmalloc.h
+++ b/src/zmalloc.h

+void *zcalloc(size_t size);

优化原理

系统级优化: 当系统使用 mmap 分配内存时,calloc 比 malloc + memset 更高效

  1. 零初始化: mmap 总是返回已清零的内存,所以可以避免显式的 memset 操作

  2. 内存阈值:

    • macOS libc: 16KB 阈值使用 mmap

    • BSD libc 和 glibc: 128KB 阈值使用 mmap

  3. 懒分配: 内核可以延迟分配页面,这在页表或哈希表大部分为空时减少内存使用

内存碎片率

eddb388e

添加了内存碎片化比率监控在 info 命令

内存碎片化比率 = RSS (实际物理内存使用) / 已分配字节数

--- a/src/config.h
+++ b/src/config.h

+/* test for proc filesystem */
+#ifdef __linux__
+#define HAVE_PROCFS 1
+#endif

实现细节

  1. 读取 RSS: 从 /proc/<pid>/stat 文件的第24个字段获取 RSS 值
  2. 页面大小: 使用 sysconf(_SC_PAGESIZE) 获取系统页面大小
  3. 计算比率: RSS / 已分配内存

监控内存效率

  • 比率接近 1.0: 内存使用高效,碎片化少

  • 比率显著 > 1.0: 存在内存碎片化问题

mac 内存碎片率

73db2acc

  • Linux: 通过/proc文件系统获取内存信息

  • Mac OS X: 通过task_info()API获取内存信息

Don’t use prefix when malloc_size() can be called

7cdc98b6

  1. 优化内存前缀处理:当系统支持malloc_size()时,不再需要内存前缀
  2. 修复OSX上的符号查找问题:通过宏明确使用tcmalloc函数,避免符号解析到原生malloc/free

技术细节分析

内存前缀优化

修改前

// 所有系统都使用前缀
#define PREFIX_SIZE sizeof(size_t)  // 或 sizeof(long long)

修改后

#ifdef HAVE_MALLOC_SIZE
#define PREFIX_SIZE (0)  // 支持malloc_size时不需要前缀
#else
/* Use at least 8 bytes alignment on all systems. */
#if SIZE_MAX < 0xffffffffffffffffull
#define PREFIX_SIZE 8
#else
#define PREFIX_SIZE (sizeof(size_t))
#endif
#endif

分配器选择优化

config.h

--- a/src/config.h
+++ b/src/config.h

 #include <AvailabilityMacros.h>
 #endif

-/* test for malloc_size() */
-#ifdef __APPLE__
+/* use tcmalloc's malloc_size() when available */
+#if defined(USE_TCMALLOC)
+#include <google/tcmalloc.h>
+#if TC_VERSION_MAJOR >= 1 && TC_VERSION_MINOR >= 6
+#define HAVE_MALLOC_SIZE 1
+#define redis_malloc_size(p) tc_malloc_size(p)
+#endif
+#endif
+
+/* fallback to native malloc_size() for osx */
+#if defined(__APPLE__) && !defined(HAVE_MALLOC_SIZE)
 #include <malloc/malloc.h>
 #define HAVE_MALLOC_SIZE 1
 #define redis_malloc_size(p) malloc_size(p)

显式宏定义

--- a/src/zmalloc.c
+++ b/src/zmalloc.c

+#ifdef HAVE_MALLOC_SIZE
+#define PREFIX_SIZE (0)
+#else

+/* Explicitly override malloc/free etc when using tcmalloc. */
+#if defined(USE_TCMALLOC)
+#define malloc(size) tc_malloc(size)
+#define calloc(count,size) tc_calloc(count,size)
+#define realloc(ptr,size) tc_realloc(ptr,size)
+#define free(ptr) tc_free(ptr)

性能影响

内存使用优化

// 修改前:每个分配都需要额外的前缀
void *ptr = malloc(size + PREFIX_SIZE);  // 额外开销

// 修改后:直接分配,无额外开销
void *ptr = malloc(size);  // 无额外开销

内存大小获取

// 修改前:需要读取前缀
size_t size = *(size_t*)((char*)ptr - PREFIX_SIZE);

// 修改后:直接调用系统函数
size_t size = malloc_size(ptr);

get rss,fragmentation拆分

92e28228

将原本混合在一起的RSS获取和碎片率计算功能分离成两个独立的函数

jemalloc

0811e89c8

redis 2.4

jemalloc 2.2.1

内存分配器演进

  • 2010年: 主要使用系统malloc

  • 2011年: 添加jemalloc支持

  • 后续: jemalloc成为Linux上的默认分配器

buildin atomic

80ff1fc6

替换了原来的互斥锁操作,使用 GCC 的原子内置函数

  • __sync_add_and_fetch 和 __sync_sub_and_fetch 提供了原子性的加减操作
--- a/src/config.h
+++ b/src/config.h

+#if (__i386 || __amd64) && __GNUC__
+    #include <features.h>
+
+    #if __GNUC_PREREQ(4,1)
+        #define HAVE_ATOMIC
+    #endif
+#endif

--- a/src/zmalloc.c
+++ b/src/zmalloc.c

+#ifdef HAVE_ATOMIC
+#define update_zmalloc_stat_add(__n) __sync_add_and_fetch(&used_memory, (__n))
+#define update_zmalloc_stat_sub(__n) __sync_sub_and_fetch(&used_memory, (__n))
+#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)
+
+#define update_zmalloc_stat_sub(__n) do { \
+    pthread_mutex_lock(&used_memory_mutex); \
+    used_memory -= (__n); \
+    pthread_mutex_unlock(&used_memory_mutex); \
+} while(0)
+
+#endif
+


#define update_zmalloc_stat_alloc(__n,__size) do { \
     size_t _n = (__n); \
     if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
     if (zmalloc_thread_safe) { \
-        pthread_mutex_lock(&used_memory_mutex);  \
-        used_memory += _n; \
-        pthread_mutex_unlock(&used_memory_mutex); \
+        update_zmalloc_stat_add(_n); \
     } else { \
         used_memory += _n; \
     } \
 } while(0)

 #define update_zmalloc_stat_free(__n) do { \
     size_t _n = (__n); \
     if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
     if (zmalloc_thread_safe) { \
-        pthread_mutex_lock(&used_memory_mutex);  \
-        used_memory -= _n; \
-        pthread_mutex_unlock(&used_memory_mutex); \
+        update_zmalloc_stat_sub(_n); \
     } else { \
         used_memory -= _n; \
     } \
 } while(0)
 
  size_t zmalloc_used_memory(void) {
     size_t um;

-    if (zmalloc_thread_safe) pthread_mutex_lock(&used_memory_mutex);
-    um = used_memory;
-    if (zmalloc_thread_safe) pthread_mutex_unlock(&used_memory_mutex);
+    if (zmalloc_thread_safe) {
+#ifdef HAVE_ATOMIC
+        um = __sync_add_and_fetch(&used_memory, 0);
+#else
+        pthread_mutex_lock(&used_memory_mutex);
+        um = used_memory;
+        pthread_mutex_unlock(&used_memory_mutex);
+#endif
+    }
+    else {
+        um = used_memory;
+    }
+
     return um;
 }

zmalloc_get_private_dirty

since 2.6 3bfeb9c1

主要作用是监控和估算 Copy-on-Write (CoW) 内存使用情况,特别是在 Redis 的子进程(如 RDB 保存、AOF 重写)中

后续有流现节流机制

实现机制:

  1. 测量成本: 记录每次调用 zmalloc_get_private_dirty() 的耗时
  2. 计算间隔: 下次调用必须等待 cow_update_cost * CHILD_COW_DUTY_CYCLE 时间
  3. 条件检查: 只有在满足时间间隔条件时才进行新的测量
#if defined(HAVE_PROCFS)
size_t zmalloc_get_private_dirty(void) {
    char line[1024];
    size_t pd = 0;
    FILE *fp = fopen("/proc/self/smaps","r");

    if (!fp) return 0;
    while(fgets(line,sizeof(line),fp) != NULL) {
        if (strncmp(line,"Private_Dirty:",14) == 0) {
            char *p = strchr(line,'k');
            if (p) {
                *p = '\0';
                pd += strtol(line+14,NULL,10) * 1024;
            }
        }
    }
    fclose(fp);
    return pd;
}
#else
size_t zmalloc_get_private_dirty(void) {
    return 0;
}
#endif

Private_Dirty 的含义

Private_Dirty 是什么

  • 这是 Linux /proc/self/smaps 文件中的一个字段

  • 表示进程私有的、已被修改但尚未写回磁盘的内存页面大小

  • 这些页面在 fork 时会发生 Copy-on-Write

为什么选择 Private_Dirty

  • 它直接反映了可能发生 CoW 的内存大小

  • 比 RSS (Resident Set Size) 更准确地反映 CoW 影响

  • 是 Linux 内核提供的标准指标

cache RSS in serverCron()

since 3.2 93253c27

获取 RSS信息是一个相对较慢的操作,这导致生成 INFO memory部分时速度变慢

解决方案

该提交通过以下方式优化了性能

  1. 采样缓存机制

    不再在每次 INFO 命令执行时实时获取 RSS,而是在 serverCron() 函数中按 server.hz 频率(默认每秒10次)进行采样

  2. 缓存 RSS 值

    将采样得到的 RSS 值存储在 server.resident_set_size 字段中

  3. 复用缓存值

    在生成 INFO 输出和计算内存碎片率时,使用缓存的 RSS 值而不是实时调用 zmalloc_get_rss()

潜在影响

  • RSS 值现在最多有 100ms 的延迟(基于默认的 server.hz=10)

  • 对于大多数监控场景,这种延迟是可以接受的

C11 __atomic

__sync_add_and_fetch
__sync_sub_and_fetch

to

__atomic_add_fetch
__atomic_sub_fetch

支持的架构

  • PowerPC(主要受益者)

  • x86/x86-64(轻微提升)

  • ARM(如果支持 C11 原子操作)

  • 其他支持 GCC 4.8.2+ 的架构

当前状态

进一步改进和整合:

  1. 统一的原子操作接口:现在使用 atomicvar.h 中定义的统一接口
  2. 多层级支持:
  • C11 _Atomic 关键字(最高优先级)

  • GCC __atomic 内置函数

  • GCC __sync 内置函数(向后兼容)

  • 更复杂的实现:当前版本使用线程本地存储和更复杂的原子操作来支持多线程环境

THP detection / reporting functions added

since 3.2

实现了对 Transparent Huge Pages (THP) 的检测和报告功能,这是一个重要的系统级内存管理特性

active memory defragmentation

since 5.0

这个提交实现了 Redis 的主动内存碎片整理功能,这是一个重要的性能优化特性。

具体修改内容

新增 jemalloc 接口

  • 在 deps/jemalloc/src/jemalloc.c 中添加了 je_get_defrag_hint() 函数

  • 这个函数帮助应用程序决定哪些指针值得重新分配以减少碎片

新增内存分配函数

  • zmalloc_no_tcache() 和 zfree_no_tcache():绕过线程缓存的分配/释放函数

  • 这些函数直接与分配器 arena bins 交互

新增碎片整理核心函数

  • activeDefragAlloc():通用分配的碎片整理助手

  • activeDefragSds():SDS 字符串的碎片整理助手

  • activeDefragStringOb():robj 和字符串对象的碎片整理助手

  • dictIterDefragEntry():字典条目的碎片整理助手

  • dictDefragTables():字典主分配的碎片整理助手

  • zslDefrag():有序集合的碎片整理助手

新增碎片整理扫描函数

  • defargKey():对主字典中的每个键进行碎片整理

  • defragScanCallback():主数据库字典的碎片整理扫描回调

  • defragDictBucketCallback():哈希表桶的碎片整理回调

新增碎片整理控制函数

  • getAllocatorFragmentation():获取分配器碎片比率

  • activeDefragCycle():执行增量碎片整理工作

新增配置参数

  • active_defrag_enabled:是否启用主动碎片整理

  • active_defrag_ignore_bytes:开始主动碎片整理的最小碎片浪费字节数

  • active_defrag_threshold_lower:开始主动碎片整理的最小碎片百分比

  • active_defrag_threshold_upper:使用最大努力的最大碎片百分比

  • active_defrag_cycle_min:碎片整理的最小 CPU 百分比

  • active_defrag_cycle_max:碎片整理的最大 CPU 百分比

新增统计信息

  • stat_active_defrag_hits:移动的分配数量

  • stat_active_defrag_misses:扫描但未移动的分配数量

  • stat_active_defrag_key_hits:有移动分配的键数量

  • stat_active_defrag_key_misses:扫描但未移动的键数量

  1. 修改字典扫描接口:
  • 在 dictScan() 函数中添加了 bucketfn 参数,用于处理哈希表桶的碎片整理

技术细节

碎片检测

  • 通过 jemalloc 的 je_get_defrag_hint() 函数检测内存碎片

  • 比较 bin 利用率和 run 利用率来决定是否值得重新分配

增量处理

  • 碎片整理工作被分解为小的增量任务

  • 在 serverCron() 中定期执行,避免影响正常操作

自适应策略

  • 根据碎片程度动态调整 CPU 使用率

  • 碎片越多,CPU 使用率越高

安全机制

  • 只在没有子进程(AOF/RDB)时进行碎片整理

  • 避免在 fork 期间进行碎片整理,防止内存页面损坏

目的

  1. 减少内存碎片:通过重新分配内存块来减少内存碎片
  2. 提高内存利用率:更有效地使用可用内存
  3. 改善性能:减少内存分配和释放的开销
  4. 自动管理:无需手动干预,系统自动进行碎片整理

影响

使 Redis 能够:

  • 自动检测和处理内存碎片问题

  • 在后台持续优化内存使用

  • 提供详细的碎片整理统计信息

  • 通过配置参数灵活控制碎片整理行为

make redis purge jemalloc after flush, and enable background purging thread

since 6.0

解决了 jemalloc 5 版本的一个内存管理问题:当没有流量时,jemalloc 不会立即将内存释放回操作系统,导致 RSS(常驻内存)保持高位

具体修改内容

  1. 新增 jemalloc 后台线程配置:

    • 在 server.h 中添加了 jemalloc_bg_thread 字段

    • 在 config.c 中添加了 jemalloc-bg-thread 配置选项

    • 默认启用后台线程(server.jemalloc_bg_thread = 1)

  2. 新增内存清理函数:

    • set_jemalloc_bg_thread():启用/禁用 jemalloc 后台线程

    • jemalloc_purge():强制将所有未使用的保留页面释放回操作系统

  3. 在 flush 操作后执行内存清理:

    • 在 flushdbCommand() 和 flushallCommand() 中添加了 jemalloc_purge() 调用

    • 只在同步操作(非异步)时执行清理

  4. 新增调试命令:

    • DEBUG MALLCTL:获取或设置 malloc 调优整数参数

    • DEBUG MALLCTL-STR:获取或设置 malloc 调优字符串参数

技术细节

jemalloc 后台线程

void set_jemalloc_bg_thread(int enable) {
   char val = !!enable;
   je_mallctl("background_thread", NULL, 0, &val, 1);
}

强制内存清理

int jemalloc_purge(void) {
   char tmp[32];
   unsigned narenas = 0;
   size_t sz = sizeof(unsigned);
   if (!je_mallctl("arenas.narenas", &narenas, &sz, NULL, 0)) {
       snprintf(tmp, sizeof(tmp), "arena.%u.purge", narenas);
       if (!je_mallctl(tmp, NULL, 0, NULL, 0))
           return 0;
   }
   return -1;
}

flush 后的清理

#if defined(USE_JEMALLOC)
/* jemalloc 5 doesn't release pages back to the OS when there's no traffic.
* for large databases, flushdb blocks for long anyway, so a bit more won't
* harm and this way the flush and purge will be synchronous. */
if (!(flags & EMPTYDB_ASYNC))
   jemalloc_purge();
#endif

目的

这个修改的主要目的是:

  1. 解决内存泄漏问题:

    • jemalloc 5 使用衰减机制而不是立即释放内存

    • 当没有分配活动时,内存不会自动释放回操作系统

  2. 改善内存使用效率:

    • 在 flush 操作后立即释放内存

    • 启用后台线程持续进行内存清理

  3. 提供调试工具:

    • 通过 DEBUG 命令可以监控和调整 jemalloc 参数

    • 帮助诊断内存相关问题

影响

使 Redis 能够:

  1. 更及时地释放内存:

    • 在 flush 操作后立即释放未使用的内存

    • 减少 RSS 占用,提高内存使用效率

  2. 自动内存管理:

    • 后台线程持续进行内存清理

    • 减少手动干预的需要

  3. 更好的监控能力:

    • 通过 DEBUG 命令可以查看和调整内存分配器参数

    • 有助于性能调优和问题诊断

背景知识

jemalloc 5 的内存管理机制

  • 使用衰减机制而不是立即释放

  • 在没有分配活动时,内存可能保持在高位

  • 这在大数据库 flush 后特别明显,RSS 会保持高位

这个提交通过两个策略解决了这个问题:

  1. 主动清理:在 flush 操作后立即调用 jemalloc_purge()

  2. 后台清理:启用 jemalloc 后台线程持续进行内存清理

这是一个重要的性能优化,特别适用于需要频繁清空数据库的场景

zmalloc: add ztrymalloc_usable and ztryrealloc_usable

引入了新的内存分配函数,Redis 内存管理安全性的重要改进,特别是在处理不可信数据(如损坏的 RDB 文件)时提供了更好的保护

具体修改内容

  1. 新增 try 系列函数:

    • ztrymalloc_usable():尝试分配内存,失败时返回 NULL 而不是 panic

    • ztrycalloc_usable():尝试分配清零内存,失败时返回 NULL

    • ztryrealloc_usable():尝试重新分配内存,失败时返回 NULL

  2. 重构现有函数:

    • zmalloc() 现在调用 ztrymalloc_usable_internal()

    • zcalloc() 现在调用 ztrycalloc_usable()

    • zrealloc() 现在调用 ztryrealloc_usable_internal()

  3. .改进错误处理:

    • 在内存分配失败时,新的 try 函数返回 NULL 而不是调用 zmalloc_oom_handler()

    • 这允许调用者优雅地处理内存不足的情况

  4. 新增头文件声明:

    • 在 zmalloc.h 中添加了新函数的声明

技术细节

since 6.2

函数层次结构

// 内部函数(处理实际分配)
ztrymalloc_usable_internal()
ztrycalloc_usable_internal()
ztryrealloc_usable_internal()

// 公共 try 函数(返回 NULL 而不是 panic)
ztrymalloc_usable()
ztrycalloc_usable()
ztryrealloc_usable()

// 公共 panic 函数(失败时调用 oom_handler)
zmalloc_usable()
zcalloc_usable()
zrealloc_usable()

错误处理改进

// 之前:直接 panic
if (!ptr) zmalloc_oom_handler(size);

// 现在:返回 NULL,让调用者决定如何处理
if (newptr == NULL) {
   if (usable) *usable = 0;
   return NULL;
}

解决的问题

  1. RDB 加载时的内存不足:

    • 当加载损坏的 RDB 数据时,可能会遇到异常大的内存分配请求

    • 新的 try 函数允许优雅地处理这种情况,而不是直接崩溃

  2. 测试用例:

    • 添加了两个新的测试用例来验证 OOM(内存不足)情况的处理

    • 这些测试确保在遇到损坏数据时不会崩溃

Use madvise(MADV_DONTNEED) to release memory to reduce COW

since 7.0

现了一个重要的内存优化机制,通过在 fork 子进程中使用 madvise(MADV_DONTNEED) 来释放内存,从而减少 Copy-on-Write (COW) 的内存消耗

背景问题

  1. COW 内存问题:在 fork() 后,父进程和子进程共享相同的物理内存页。当任一进程修改页面时,会触发 COW,导致内存消耗翻倍。

  2. Redis 的特殊情况:在 Redis 的 fork 子进程中(如 RDB 保存、AOF 重写),子进程序列化完键值对后就不再访问这些数据,但父进程可能会修改它们,导致不必要的 COW

具体修改内容

新增内存释放函数

void zmadvise_dontneed(void *ptr);
void dismissMemory(void* ptr, size_t size_hint);
void dismissObject(robj *o, size_t dump_size);
void dismissMemoryInChild(void);

实现 madvise 机制

void zmadvise_dontneed(void *ptr) {
#if defined(USE_JEMALLOC)
   // 计算页面对齐的地址和大小
   char *aligned_ptr = (char *)(((size_t)ptr+page_size_mask) & ~page_size_mask);
   // 使用 MADV_DONTNEED 释放页面
   madvise((void *)aligned_ptr, real_size&~page_size_mask, MADV_DONTNEED);
#endif
}

在序列化过程中释放内存

  • 在 rdbSaveRio() 中,序列化每个键值对后调用 dismissObject()

  • 在 rewriteAppendOnlyFileRio() 中,序列化每个键值对后调用 dismissObject()

子进程内存释放

void dismissMemoryInChild(void) {
   // 释放复制缓冲区
   if (server.repl_backlog != NULL) {
       dismissMemory(server.repl_backlog, server.repl_backlog_size);
   }
   // 释放所有客户端内存
   // 遍历所有客户端,释放查询缓冲区、输出缓冲区等
}

智能释放策略

  • 只有当平均项目大小超过页面大小时才遍历复杂数据结构

  • 避免对小对象进行不必要的遍历

  • 只在 jemalloc 下启用(因为其他分配器可能不支持)

新增统计信息

  • stat_current_cow_peak:COW 峰值大小

  • 改进的日志输出,显示当前、峰值和平均 COW 大小

THP 检测

  • 检测透明大页是否启用

  • 如果启用,禁用内存释放机制(因为 THP 会影响 madvise 效果)

技术细节

页面对齐

*// 向上对齐到页面边界*
char *aligned_ptr = (char *)(((size_t)ptr+page_size_mask) & ~page_size_mask);

条件释放

*// 只有当大小超过页面大小的一半时才释放*
if (size_hint && size_hint <= server.page_size/2) return;

对象类型特定释放

  • 字符串:直接释放 SDS

  • 列表:遍历 quicklist 节点

  • 集合:遍历字典条目

  • 有序集合:遍历跳跃表节点

  • 哈希:遍历字典条目

  • 流:遍历流条目

影响

  1. 内存使用优化:显著减少 fork 子进程的内存消耗
  2. 性能提升:减少 COW 开销,提高 RDB 保存和 AOF 重写性能
  3. 更好的监控:提供详细的 COW 统计信息
  4. 兼容性:只在支持的平台上启用,不影响其他环境

测试

新增了 tests/integration/dismiss-mem.tcl 测试文件,验证:

  • 各种数据类型的内存释放

  • 客户端输出缓冲区释放

  • 客户端查询缓冲区释放

  • 复制缓冲区释放

Add latency tracking sample percentiles config and latency tracking info command

为 Redis 添加了延迟跟踪的百分位数配置和延迟跟踪信息命令,延迟直方图功能

具体修改内容

新增延迟跟踪配置选项

  • latency-tracking-info-percentiles:配置要跟踪的延迟百分位数(如 50.0, 99.0, 99.9)

  • 支持 0.0 到 100.0 之间的百分位数

  • 可以配置多个百分位数,用空格分隔

新增延迟跟踪信息命令:

  • LATENCY HISTOGRAM 命令:显示命令的延迟直方图信息

  • 支持查看所有命令或特定命令的延迟统计

  • 显示调用次数、延迟直方图等详细信息

延迟直方图功能

  • 使用 HDR 直方图来跟踪延迟分布

  • 支持按命令类型分组统计

  • 提供详细的延迟分布信息

命令支持

  • 支持查看所有命令的延迟统计

  • 支持查看特定命令的延迟统计

  • 支持过滤无效命令名

使用示例

配置百分位数

CONFIG SET latency-tracking-info-percentiles "50.0 99.0 99.9"

查看延迟直方图

LATENCY HISTOGRAM
LATENCY HISTOGRAM set get

Defragger improvements around large bins

since 7.4

改进了 Redis 的内存碎片整理器,特别是针对大内存块(large bins)的处理,并添加了新的内存监控指标

具体修改内容

改进碎片计算逻辑

// 之前:计算所有内存的碎片率
float frag_pct = ((float)active / allocated)*100 - 100;
size_t frag_bytes = active - allocated;

// 现在:只计算小内存块的碎片率
float frag_pct = (float)frag_smallbins_bytes / allocated * 100;
*out_frag_bytes = frag_smallbins_bytes;

新增小内存块碎片计算函数

size_t zmalloc_get_frag_smallbins(void) {
   unsigned nbins;
   size_t sz, frag = 0;

   // 遍历所有内存块
   for (unsigned j = 0; j < nbins; j++) {
       // 计算每个小内存块的碎片
       frag += ((nregs * curslabs) - curregs) * reg_size;
   }
   return frag;
}

扩展内存分配器信息接口

int zmalloc_get_allocator_info(size_t *allocated, size_t *active, size_t *resident,
                              size_t *retained, size_t *muzzy, size_t *frag_smallbins_bytes)

新增 muzzy 内存指标

  • allocator_muzzy:通过 madvise(…, MADV_FREE) 释放的内存

  • 这些页面在操作系统回收之前仍会显示为 RSS

更新内存监控统计

  • 在 server.h 中添加了 allocator_muzzy 和 allocator_frag_smallbins_bytes 字段

  • 在 server.c 中更新了内存统计收集逻辑

  • 在 object.c 中更新了内存开销计算

改进 INFO 命令输出

  • 添加了 allocator.muzzy 指标

  • 更新了碎片率计算,只考虑小内存块

技术细节

为什么只考虑小内存块

  • 大内存块通常没有外部碎片,或者至少是不可整理的

  • 如果大部分内存使用都是大内存块,可能会导致显示高碎片率,但实际上对用户来说并不是很多内存

muzzy 内存的概念

/* Unlike retained, Muzzy representats memory released with `madvised(..., MADV_FREE)`.
* These pages will show as RSS for the process, until the OS decides to re-use them. */

碎片计算改进

/* Calculate the fragmentation ratio as the proportion of wasted memory in small
* bins (which are defraggable) relative to the total allocated memory (including large bins).
* This is because otherwise, if most of the memory usage is large bins, we may show high percentage,
* despite the fact it's not a lot of memory for the user. */

影响

  1. 更准确的碎片率:只考虑可整理的小内存块,提供更准确的碎片率

  2. 更好的监控:新增 muzzy 内存指标,提供更详细的内存使用情况

  3. 更高效的碎片整理:避免对大内存块进行不必要的碎片整理尝试

  4. 更准确的触发条件:主动碎片整理的触发条件现在基于更准确的碎片率

解决的问题

  1. 大内存块误报:避免大内存块导致的虚假高碎片率

  2. 监控不完整:添加了 muzzy 内存的监控

  3. 碎片整理效率:只对真正需要整理的小内存块进行处理

Allocate Lua VM code with jemalloc instead of libc, and count it used memory

使用 jemalloc 替代 libc 来分配 Lua VM 代码,并统计其使用的内存。解决了 Redis 中 Lua 脚本内存控制的问题

背景问题

  • Lua 内存控制问题:Lua 内存控制不通过 Redis 的 zmalloc.c,导致 Redis 的 maxmemory 无法限制用户滥用 Lua 脚本造成的内存问题

  • 内存统计不准确:Lua VM 内存不是 used_memory 的一部分,导致内存使用统计不完整

解决方案

  • 使用 jemalloc 替代 libc:利用 jemalloc 更好的内存碎片管理和速度优势

  • 创建专用 arena:为所有 Lua VM(脚本和函数)创建共享的 arena,避免阻塞 defragger

  • 创建绑定 tcache:为 Lua VM 创建独立的 tcache,避免与主线程的内存竞争

实现细节

Lua 内存分配器集成 (src/script.c)

#if defined(USE_JEMALLOC)
/* 当 Lua 使用 jemalloc 时,将 luaAlloc 作为 lua_newstate 的参数传入 */
static void *luaAlloc(void *ud, void *ptr, size_t osize, size_t nsize) {
    unsigned int tcache = (unsigned int)(uintptr_t)ud;
    if (nsize == 0) {
        zfree_with_flags(ptr, MALLOCX_ARENA(server.lua_arena) | MALLOCX_TCACHE(tcache));
        return NULL;
    } else {
        return zrealloc_with_flags(ptr, nsize, MALLOCX_ARENA(server.lua_arena) | MALLOCX_TCACHE(tcache));
    }
}

新增内存管理函数 (src/zmalloc.c)

  • zmalloc_with_flags(): 支持标志的内存分配

  • zrealloc_with_flags(): 支持标志的内存重分配

  • zfree_with_flags(): 支持标志的内存释放

  • zmalloc_get_allocator_info_by_arena(): 获取指定 arena 的内存信息

服务器结构扩展 (src/server.h)

struct redisServer {
    // ... 其他字段
    unsigned int lua_arena;         /* eval lua arena used in jemalloc. */
    // ... 其他字段
};

struct malloc_stats {
    // ... 现有字段
    size_t lua_allocator_allocated;
    size_t lua_allocator_active;
    size_t lua_allocator_resident;
    size_t lua_allocator_frag_smallbins_bytes;
};

关键特性

内存隔离

  • 专用 Arena:为 Lua VM 创建独立的 jemalloc arena,避免与主线程内存竞争

  • 独立 Tcache:每次创建 Lua VM 时创建新的私有 tcache,确保线程安全

内存统计

  • 新增 INFO 字段:在 INFO DEBUG 中添加了 4 个新的 Lua 内存统计字段:

  • allocator_allocated_lua: Lua arena 分配的总字节数

  • allocator_active_lua: Lua arena 中活跃页面分配的总字节数

  • allocator_resident_lua: Lua arena 中物理驻留数据页面的最大字节数

  • allocator_frag_bytes_lua: Lua arena 中的碎片字节数

内存碎片管理

  • 碎片统计排除:从内存碎片统计中移除 Lua 内存统计,避免 Lua 内存碎片的影响

  • defrag 优化:避免 Lua 内存影响主内存的 defrag 过程

redis zmalloc 全局 used_memory 读写拆分

zmalloc 全局 used_memory 读写拆分