redis4.0 内存碎片整理源码解析

2020-06-20 20:18:00   最后更新: 2020-06-20 20:19:38   访问数量:62




redis 作为一个内存型数据库,在使用中常常会遇到的问题就是内存碎片的问题

redis 并没有维护自己的内存池,而是直接通过操作系统中 malloc 族的各个函数来实现在堆内存上的动态分配和释放,这就增加了 redis 对内存管理的复杂度,尤其是在频繁插入数据和删除数据的场景下, 操作系统堆内存中会造成大量碎片,导致实际占用的系统内存远大于 redis 本身所需要占用的内存,从而造成资源的浪费

本文我们就来看看如何去处理这个问题

 

redis 提供了 info memory 命令,用来查看 redis 内存的占用情况:

 

 

主要返回了下列参数:

  • used_memory -- 已使用内存大小,包括 redis 本身的内存开销与用户数据占用的内存
  • used_memory_human -- 用户数据占用的内存
  • used_memory_rss -- redis 占用的物理内存
  • used_memory_peak -- redis 内存使用的峰值
  • used_memory_peak_human -- 用户数据占用内存的峰值
  • used_memory_lua -- lua 脚本执行时占用的内存
  • mem_fragmentation_ratio -- 内存碎片率

 

内存碎片率的计算

内存碎片率 mem_fragmentation_ratio = used_memory_rss / used_memory,他指的是 redis 实际占用的内存占他所需要内存的比例

内存碎片率通常在 1~1.5 之间比较理想,表示内存碎片较少,而高于 1.5 则说明内存碎片较多,资源浪费的现象比较严重,当然,这个数值越大,内存碎片的问题就越大

如果内存碎片率小于 1,则说明有一定比例的内存中的页被置换到了硬盘中,此时通常是因为机器内存不足,这样的情况下,由于硬盘读写本身的性能问题以及页表的反复置换,redis 性能会出现明显下降

那么如果 redis 碎片率过高,我们应该如何去处理呢?很简单,只要重启 redis 服务,redis 会释放全部内存,并在重新启动时读取持久化文件,进行批量内存分配,内存碎片的问题也就不存在了

如果使用的 redis-4.0 版本以上,可以通过配置开启 redis 自动碎片整理功能,下面我们就来通过源码看看 redis 自动碎片整理是如何工作的

 

Active Defrag 功能是作为实验性功能从 redis 4.0 版本开始引入的,他可以在 redis 正常运行过程中,以一定的条件定时触发,我们可以通过下面的配置实现这些条件的定义

 

自动碎片整理相关配置

# 开启自动内存碎片整理(总开关)

activedefrag yes

# 当碎片达到 100mb 时,开启内存碎片整理,默认为 100mb

active-defrag-ignore-bytes 100mb

# 当碎片超过 10% 时,开启内存碎片整理,默认为 10

active-defrag-threshold-lower 10

# 内存碎片超过 100%,则尽最大努力整理

active-defrag-threshold-upper 100

# 内存自动整理占用资源最小百分比

active-defrag-cycle-min 25

# 内存自动整理占用资源最大百分比

active-defrag-cycle-max 75

 

自动碎片整理的启动

上一篇文章中,我们介绍了 redis 的事件驱动模型:

Redis 中的事件驱动

 

我们知道,redis 中,事件分为文件事件与时间事件两种,显然,定时执行的自动碎片整理任务就是时间事件的一种

那么,时间事件是什么时候注册的呢?我们来看源码 server.c

int main(int argc, char **argv) { /* ... */ initServer(); /* ... */ } void initServer(void) { /* ... */ /* Create the timer callback, this is our way to process many background * operations incrementally, like clients timeout, eviction of unaccessed * expired keys and so forth. */ if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) { serverPanic("Can't create event loop timers."); exit(1); } /* ... */ } int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { /* ... */ databasesCron(); /* ... */ } void databasesCron(void) { /* ... */ /* Defrag keys gradually. */ if (server.active_defrag_enabled) activeDefragCycle(); /* ... */ }

 

 

从上面的代码中,经过层层嵌套,在 redis 初始化过程中,调用了 aeCreateTimeEvent 函数将 serverCron 函数绑定到了事件循环中

aeCreateTimeEvent 函数通过将第三个参数传入的函数作为事件回调函数,第四个参数作为回调参数创建了一个时间事件并且添加到事件循环上,在 ae.c 中,他的定义如下:

long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds, aeTimeProc *proc, void *clientData, aeEventFinalizerProc *finalizerProc) { long long id = eventLoop->timeEventNextId++; aeTimeEvent *te; te = zmalloc(sizeof(*te)); if (te == NULL) return AE_ERR; te->id = id; aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms); te->timeProc = proc; te->finalizerProc = finalizerProc; te->clientData = clientData; te->prev = NULL; te->next = eventLoop->timeEventHead; if (te->next) te->next->prev = te; eventLoop->timeEventHead = te; return id; }

 

 

这里,我们就可以清晰的看到,时间事件在事件循环结构中是通过双向链表来进行存储的,并且这里将新增的事件添加到了链表首部

而通过这个函数的第二个参数,redis 指定了 serverCron 每毫秒执行一次

那么,serverCron 函数做了什么呢?它除了周期性地执行上述的定时碎片整理外,还会周期性地执行过期key的回收动作、主从重连、Cluster节点间的重连、BGSAVE 和 AOF rewrite 的触发执行等等,可以说,serverCron 是 redis 最为核心的时间事件

 

碎片整理主循环 -- activeDefragCycle

自动碎片整理的代码全部集中在 defrag.c 文件中,而其中最为重要的,就是上面函数中调用的 activeDefragCycle

 

判断是否需要进行碎片整理

除了上述代码中判断是否已经开启配置的 server.active_defrag_enabled 之外,在 activeDefragCycle 中,经过一系列运算实现了自动碎片整理阈值的判断

#define INTERPOLATE(x, x1, x2, y1, y2) ( (y1) + ((x)-(x1)) * ((y2)-(y1)) / ((x2)-(x1)) ) #define LIMIT(y, min, max) ((y)<(min)? min: ((y)>(max)? max: (y))) void activeDefragCycle(void) { /* ... */ /* Once a second, check if we the fragmentation justfies starting a scan * or making it more aggressive. */ run_with_period(1000) { size_t frag_bytes; float frag_pct = getAllocatorFragmentation(&frag_bytes); /* If we're not already running, and below the threshold, exit. */ if (!server.active_defrag_running) { if(frag_pct < server.active_defrag_threshold_lower || frag_bytes < server.active_defrag_ignore_bytes) return; } /* 计算内存碎片整理所需要占用的主线程资源 */ /* Calculate the adaptive aggressiveness of the defrag */ int cpu_pct = INTERPOLATE(frag_pct, server.active_defrag_threshold_lower, server.active_defrag_threshold_upper, server.active_defrag_cycle_min, server.active_defrag_cycle_max); cpu_pct = LIMIT(cpu_pct, server.active_defrag_cycle_min, server.active_defrag_cycle_max); /* 限制占用资源范围 */ /* We allow increasing the aggressiveness during a scan, but don't * reduce it. */ if (!server.active_defrag_running || cpu_pct > server.active_defrag_running) { server.active_defrag_running = cpu_pct; serverLog(LL_VERBOSE, "Starting active defrag, frag=%.0f%%, frag_bytes=%zu, cpu=%d%%", frag_pct, frag_bytes, cpu_pct); } } if (!server.active_defrag_running) return; timelimit = 1000000*server.active_defrag_running/server.hz/100; if (timelimit <= 0) timelimit = 1; /* ... */ }

 

 

可以看到,碎片整理是否执行主要是通过 server.active_defrag_running、server.active_defrag_ignore_bytes、server.active_defrag_threshold_lower、server.active_defrag_threshold_upper 四个变量来决定的

而后,redis 通过 server.active_defrag_cycle_min 和 server.active_defrag_cycle_max 两个变量以及上面提到的 server.active_defrag_threshold_lower 和 server.active_defrag_threshold_upper 使用两个差值函数计算出了本次执行所限制的毫秒数

这些变量分别对应了 redis 的配置:

  • server.active_defrag_running 对应 activedefrag
  • server.active_defrag_ignore_bytes 对应 active-defrag-ignore-bytes
  • server.active_defrag_threshold_lower 对应 active-defrag-threshold-lower
  • server.active_defrag_threshold_upper 对应 active-defrag-threshold-upper
  • server.active_defrag_cycle_min 对应 active-defrag-cycle-min
  • server.active_defrag_cycle_max 对应 active-defrag-cycle-max

 

可以在 config.c 中看到具体的参数解析以及对应字段的赋值

 

整理过程

核心的整理过程在 activeDefragCycle 函数接下来的循环中:

void activeDefragCycle(void) { /* ... */ start = ustime(); do { cursor = dictScan(db->dict, cursor, defragScanCallback, defragDictBucketCallback, db); /* Once in 16 scan iterations, or 1000 pointer reallocations * (if we have a lot of pointers in one hash bucket), check if we * reached the tiem limit. */ if (cursor && (++iterations > 16 || server.stat_active_defrag_hits - defragged > 1000)) { if ((ustime() - start) > timelimit) { return; } iterations = 0; defragged = server.stat_active_defrag_hits; } } while(cursor); /* ... */ }

 

 

dictScan 函数定义在 dict.c 中,用来通过游标 cursor 来遍历 dict,并且使用遍历结果作为参数调用指定的回调函数,而 activeDefragCycle 通过定义 static 全局的 cursor 变量实现了增量整理

每次遍历一个节点,都会通过判断当前时间戳是否超过时间限制来判断是否需要继续

无论是 defragScanCallback 还是 defragDictBucketCallback 中,碎片整理最终调用的都是 activeDefragAlloc 函数:

void* activeDefragAlloc(void *ptr) { int bin_util, run_util; size_t size; void *newptr; if(!je_get_defrag_hint(ptr, &bin_util, &run_util)) { server.stat_active_defrag_misses++; return NULL; } /* if this run is more utilized than the average utilization in this bin * (or it is full), skip it. This will eventually move all the allocations * from relatively empty runs into relatively full runs. */ if (run_util > bin_util || run_util == 1<<16) { server.stat_active_defrag_misses++; return NULL; } /* move this allocation to a new allocation. * make sure not to use the thread cache. so that we don't get back the same * pointers we try to free */ size = zmalloc_size(ptr); newptr = zmalloc_no_tcache(size); memcpy(newptr, ptr, size); zfree_no_tcache(ptr); return newptr; }

 

 

可以看到,在 activeDefragAlloc 函数中,通过 zmalloc_no_tcache 函数分配了全新的连续空间,并且通过 memcpy 拷贝,然后释放掉了旧的空间,从而实现碎片整理的功能

 

redis 4.0 还提供了手动碎片整理的 Memory Purge 功能,在 object.c 中,redis 实现了对命令 memory purge 的处理:

} else if (!strcasecmp(c->argv[1]->ptr,"purge") && c->argc == 2) { #if defined(USE_JEMALLOC) char tmp[32]; unsigned narenas = 0; size_t sz = sizeof(unsigned); if (!je_mallctl("arenas.narenas", &narenas, &sz, NULL, 0)) { sprintf(tmp, "arena.%d.purge", narenas); if (!je_mallctl(tmp, NULL, 0, NULL, 0)) { addReply(c, shared.ok); return; } } addReplyError(c, "Error purging dirty pages"); #else addReply(c, shared.ok); /* Nothing to do for other allocators. */ #endif }

 

 

如果使用的是默认的内存分配器 jemalloc,那么上述代码会通过 je_mallctl 函数实现脏页的清除,从而起到碎片整理的作用

由此可见,与自动整理相比,手动整理更加简单粗暴,而效果上,显然是自动整理更为彻底

 

本文基于 redis4.0 版本源码详细介绍了 redis 内存碎片的产生以及碎片自动整理、手动整理的过程

需要注意的是,redis4.0 的整个事件循环均是在同一个线程中执行的,因此,如果上述自动整理的触发频率过高,或 timelimit 过长,都会直接影响到 redis 本身的工作性能,所以相关的参数一定需要谨慎考虑,不宜将阈值设置过低

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

 






内存      memory      源码      redis      source      内存碎片     


京ICP备15018585号