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;
演化总结
- 从简单到复杂:从82行的简单实现发展到1000+行的复杂系统
- 线程安全演进:从单线程到多线程安全,再到线程本地优化
- 分配器支持扩展:从libc到支持jemalloc、tcmalloc等多种分配器
- 性能优化:从简单的全局计数器到缓存行对齐的线程本地存储
- 功能丰富化:从基本的内存分配到支持内存统计、碎片分析、RSS监控等
源码历程
first commit
ed9b544e
|
|
|
|
为什么要 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
- 类型安全:
char*
是一个具体的数据类型,可以被解引用并用于访问内存中的数据。在 ANSI C 中,char*
通常用于表示字符串,但也可以用于表示任意字节的内存块 - 兼容性:在某些情况下,可能需要将内存块作为字符串来处理,比如使用标准库函数如
strlen
、strcpy
等。将void*
转换为char*
可以提供这种兼容性 - 编码风格:在某些编码风格或项目中,可能习惯于使用
char*
来表示内存块,这样可以在不改变其他代码的情况下重用现有的代码 - 避免警告:在某些编译器或严格的编译选项下,直接使用
void*
可能会引起警告,因为编译器无法确定内存块的大小。转换为char*
可以消除这种警告 - 方便操作:
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文件来集中管理平台特定的配置
#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));
分解理解
- sizeof(long)-1:
-
在32位系统:4-1 = 3 (二进制: 11)
-
在64位系统:8-1 = 7 (二进制: 111)
- _n&(sizeof(long)-1):
-
获取_n在sizeof(long)边界内的偏移量
-
如果_n已经是sizeof(long)的倍数,结果为0
-
否则结果为需要补齐的字节数
- 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 更高效
-
零初始化: mmap 总是返回已清零的内存,所以可以避免显式的 memset 操作
-
内存阈值:
-
macOS libc: 16KB 阈值使用 mmap
-
BSD libc 和 glibc: 128KB 阈值使用 mmap
-
-
懒分配: 内核可以延迟分配页面,这在页表或哈希表大部分为空时减少内存使用
内存碎片率
eddb388e
添加了内存碎片化比率监控在 info 命令
内存碎片化比率 = RSS (实际物理内存使用) / 已分配字节数
--- a/src/config.h
+++ b/src/config.h
+/* test for proc filesystem */
+#ifdef __linux__
+#define HAVE_PROCFS 1
+#endif
实现细节
- 读取 RSS: 从
/proc/<pid>/stat
文件的第24个字段获取 RSS 值 - 页面大小: 使用 sysconf(_SC_PAGESIZE) 获取系统页面大小
- 计算比率: 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
- 优化内存前缀处理:当系统支持malloc_size()时,不再需要内存前缀
- 修复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 重写)中
后续有流现节流机制
实现机制:
- 测量成本: 记录每次调用 zmalloc_get_private_dirty() 的耗时
- 计算间隔: 下次调用必须等待 cow_update_cost * CHILD_COW_DUTY_CYCLE 时间
- 条件检查: 只有在满足时间间隔条件时才进行新的测量
#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部分时速度变慢
解决方案
该提交通过以下方式优化了性能
-
采样缓存机制
不再在每次 INFO 命令执行时实时获取 RSS,而是在 serverCron() 函数中按 server.hz 频率(默认每秒10次)进行采样
-
缓存 RSS 值
将采样得到的 RSS 值存储在 server.resident_set_size 字段中
-
复用缓存值
在生成 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+ 的架构
当前状态
进一步改进和整合:
- 统一的原子操作接口:现在使用 atomicvar.h 中定义的统一接口
- 多层级支持:
-
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:扫描但未移动的键数量
- 修改字典扫描接口:
- 在 dictScan() 函数中添加了 bucketfn 参数,用于处理哈希表桶的碎片整理
技术细节
碎片检测
-
通过 jemalloc 的 je_get_defrag_hint() 函数检测内存碎片
-
比较 bin 利用率和 run 利用率来决定是否值得重新分配
增量处理
-
碎片整理工作被分解为小的增量任务
-
在 serverCron() 中定期执行,避免影响正常操作
自适应策略
-
根据碎片程度动态调整 CPU 使用率
-
碎片越多,CPU 使用率越高
安全机制
-
只在没有子进程(AOF/RDB)时进行碎片整理
-
避免在 fork 期间进行碎片整理,防止内存页面损坏
目的
- 减少内存碎片:通过重新分配内存块来减少内存碎片
- 提高内存利用率:更有效地使用可用内存
- 改善性能:减少内存分配和释放的开销
- 自动管理:无需手动干预,系统自动进行碎片整理
影响
使 Redis 能够:
-
自动检测和处理内存碎片问题
-
在后台持续优化内存使用
-
提供详细的碎片整理统计信息
-
通过配置参数灵活控制碎片整理行为
make redis purge jemalloc after flush, and enable background purging thread
since 6.0
解决了 jemalloc 5 版本的一个内存管理问题:当没有流量时,jemalloc 不会立即将内存释放回操作系统,导致 RSS(常驻内存)保持高位
具体修改内容
-
新增 jemalloc 后台线程配置:
-
在 server.h 中添加了 jemalloc_bg_thread 字段
-
在 config.c 中添加了 jemalloc-bg-thread 配置选项
-
默认启用后台线程(server.jemalloc_bg_thread = 1)
-
-
新增内存清理函数:
-
set_jemalloc_bg_thread():启用/禁用 jemalloc 后台线程
-
jemalloc_purge():强制将所有未使用的保留页面释放回操作系统
-
-
在 flush 操作后执行内存清理:
-
在 flushdbCommand() 和 flushallCommand() 中添加了 jemalloc_purge() 调用
-
只在同步操作(非异步)时执行清理
-
-
新增调试命令:
-
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
目的
这个修改的主要目的是:
-
解决内存泄漏问题:
-
jemalloc 5 使用衰减机制而不是立即释放内存
-
当没有分配活动时,内存不会自动释放回操作系统
-
-
改善内存使用效率:
-
在 flush 操作后立即释放内存
-
启用后台线程持续进行内存清理
-
-
提供调试工具:
-
通过 DEBUG 命令可以监控和调整 jemalloc 参数
-
帮助诊断内存相关问题
-
影响
使 Redis 能够:
-
更及时地释放内存:
-
在 flush 操作后立即释放未使用的内存
-
减少 RSS 占用,提高内存使用效率
-
-
自动内存管理:
-
后台线程持续进行内存清理
-
减少手动干预的需要
-
-
更好的监控能力:
-
通过 DEBUG 命令可以查看和调整内存分配器参数
-
有助于性能调优和问题诊断
-
背景知识
jemalloc 5 的内存管理机制
-
使用衰减机制而不是立即释放
-
在没有分配活动时,内存可能保持在高位
-
这在大数据库 flush 后特别明显,RSS 会保持高位
这个提交通过两个策略解决了这个问题:
-
主动清理:在 flush 操作后立即调用 jemalloc_purge()
-
后台清理:启用 jemalloc 后台线程持续进行内存清理
这是一个重要的性能优化,特别适用于需要频繁清空数据库的场景
zmalloc: add ztrymalloc_usable and ztryrealloc_usable
引入了新的内存分配函数,Redis 内存管理安全性的重要改进,特别是在处理不可信数据(如损坏的 RDB 文件)时提供了更好的保护
具体修改内容
-
新增 try 系列函数:
-
ztrymalloc_usable():尝试分配内存,失败时返回 NULL 而不是 panic
-
ztrycalloc_usable():尝试分配清零内存,失败时返回 NULL
-
ztryrealloc_usable():尝试重新分配内存,失败时返回 NULL
-
-
重构现有函数:
-
zmalloc() 现在调用 ztrymalloc_usable_internal()
-
zcalloc() 现在调用 ztrycalloc_usable()
-
zrealloc() 现在调用 ztryrealloc_usable_internal()
-
-
.改进错误处理:
-
在内存分配失败时,新的 try 函数返回 NULL 而不是调用 zmalloc_oom_handler()
-
这允许调用者优雅地处理内存不足的情况
-
-
新增头文件声明:
- 在 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;
}
解决的问题
-
RDB 加载时的内存不足:
-
当加载损坏的 RDB 数据时,可能会遇到异常大的内存分配请求
-
新的 try 函数允许优雅地处理这种情况,而不是直接崩溃
-
-
测试用例:
-
添加了两个新的测试用例来验证 OOM(内存不足)情况的处理
-
这些测试确保在遇到损坏数据时不会崩溃
-
Use madvise(MADV_DONTNEED) to release memory to reduce COW
since 7.0
现了一个重要的内存优化机制,通过在 fork 子进程中使用 madvise(MADV_DONTNEED) 来释放内存,从而减少 Copy-on-Write (COW) 的内存消耗
背景问题
-
COW 内存问题:在 fork() 后,父进程和子进程共享相同的物理内存页。当任一进程修改页面时,会触发 COW,导致内存消耗翻倍。
-
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 节点
-
集合:遍历字典条目
-
有序集合:遍历跳跃表节点
-
哈希:遍历字典条目
-
流:遍历流条目
影响
- 内存使用优化:显著减少 fork 子进程的内存消耗
- 性能提升:减少 COW 开销,提高 RDB 保存和 AOF 重写性能
- 更好的监控:提供详细的 COW 统计信息
- 兼容性:只在支持的平台上启用,不影响其他环境
测试
新增了 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. */
影响
-
更准确的碎片率:只考虑可整理的小内存块,提供更准确的碎片率
-
更好的监控:新增 muzzy 内存指标,提供更详细的内存使用情况
-
更高效的碎片整理:避免对大内存块进行不必要的碎片整理尝试
-
更准确的触发条件:主动碎片整理的触发条件现在基于更准确的碎片率
解决的问题
-
大内存块误报:避免大内存块导致的虚假高碎片率
-
监控不完整:添加了 muzzy 内存的监控
-
碎片整理效率:只对真正需要整理的小内存块进行处理
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 过程