欢迎光临
我们一直在努力

redis内存管理源码分析

文章目录

  • 相关文件
  • 内存碎片整理defrag.c
  • evict.c
  • expire.c
  • zmalloc内存管理

相关文件

文件功能
defrag.c 内存碎片整理
evict.c 内存淘汰策略
expire.c 过期键删除
zmalloc.c 内存管理
memtest.c 内存测试

redis的源代码有个特点,首先将所有要用到的函数写在server.h中。 但是server.h只实现部分最重要的函数。其他的函数放到不同的c文件中去实现。但是这样也导致了一个问题,就是找代码,分析源码带来了一定的困难。

内存碎片整理defrag.c

内存碎片整理的核心代码位于defrag.c中 在ae事件循环里,会调用static int processTimeEvents(aeEventLoop *eventLoop) 。 这里最终会调用server.c中的int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData).接下来是databasesCron()->activeDefragCycle(). 默认defrag是禁用的,只有开启后,才会defrag. 首先可以修改配置文件,在redis.conf的最末尾一段就是defrag的配置.放开注释,no改成yes就行了,也就是:

activedefrag yes

不过在beginDefragCycle里也并不会真正清理,而是创建一大堆清理任务。 beginDefragCycle里循环里所有数据库,为每个数据库创建里两个任务,一个keys的整理,另一个是过时key的清理。 真正的清理发生在defrag.c的activeDefragAlloc函数。其过程是将内存复制到新地址,然后返回新地址。

/* Defrag helper for generic allocations.
*
* returns NULL in case the allocation wasn't moved.
* when it returns a non-null value, the old pointer was already released
* and should NOT be accessed. */

void* activeDefragAlloc(void *ptr) {
size_t size;
void *newptr;
if(!je_get_defrag_hint(ptr)) {
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_usable_size(ptr);
newptr = zmalloc_no_tcache(size);
memcpy(newptr, ptr, size);
zfree_no_tcache(ptr);
server.stat_active_defrag_hits++;
return newptr;
}

所以总结一下:

  • redis的内存碎片整理默认是关闭的,需要通过配置开启;
  • 内存碎片整理是有aeMain事件主循环的定时任务启动的;
  • defrag定时任务启动后并不是马上清理内存,而是清理任务异步执行;
  • 每个db,有2个异步任务,一个整理keys,一个处理过期
  • 真正的清理是在activeDefragAlloc调用zmalloc,内容复制到新地址,删除旧内存

evict.c

redis在内存不足时会触发内存淘汰策略。

在server.c的命令处理过程里,有evict的入口:

if (server.maxmemory && !isInsideYieldingLongCommand()) {
int out_of_memory = (performEvictions() == EVICT_FAIL);
// 省略其他代码
}

在while循环里进行内存淘汰。

while (mem_freed < (long long)mem_tofree) {
// 省略代码
}

从宏定义源代码看redis总共定义了8种淘汰策略

#define MAXMEMORY_VOLATILE_LRU ((0<<8)|MAXMEMORY_FLAG_LRU)
#define MAXMEMORY_VOLATILE_LFU ((1<<8)|MAXMEMORY_FLAG_LFU)
#define MAXMEMORY_VOLATILE_TTL (2<<8)
#define MAXMEMORY_VOLATILE_RANDOM (3<<8)
#define MAXMEMORY_ALLKEYS_LRU ((4<<8)|MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_ALLKEYS)
#define MAXMEMORY_ALLKEYS_LFU ((5<<8)|MAXMEMORY_FLAG_LFU|MAXMEMORY_FLAG_ALLKEYS)
#define MAXMEMORY_ALLKEYS_RANDOM ((6<<8)|MAXMEMORY_FLAG_ALLKEYS)
#define MAXMEMORY_NO_EVICTION (7<<8)

如果是随机淘汰,那么用的是公平随机数,如果是64位机器用的是mt-19947-64算法(梅森旋转算法)。 如果是32位机器,则退化为系统函数。可以从宏定义看出:

/* If our unsigned long type can store a 64 bit number, use a 64 bit PRNG. */
#if ULONG_MAX >= 0xffffffffffffffff
#define randomULong() ((unsigned long) genrand64_int64())
#else
#define randomULong() random()
#endif

如果是LRU或LFU,在evictionPoolPopulate中,可以看到具体的策略执行方法。具体代码为:

/* Calculate the idle time according to the policy. This is called
* idle just because the code initially handled LRU, but is in fact
* just a score where a higher score means better candidate. */

if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
idle = estimateObjectIdleTime(o);
} else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
/* When we use an LRU policy, we sort the keys by idle time
* so that we expire keys starting from greater idle time.
* However when the policy is an LFU one, we have a frequency
* estimation, and we want to evict keys with lower frequency
* first. So inside the pool we put objects using the inverted
* frequency subtracting the actual frequency to the maximum
* frequency of 255. */

idle = 255LFUDecrAndReturn(o);
}

redis巧妙之处在于每个对象的对象头都维护有一个LRU属性。

struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */

int refcount;
void *ptr;
};

这24位,有16位存储的是LRU相关的“上次衰减时间”,8位存储的是LFU用的访问次数。 所以LFU和LRU是根据对象头来进行内存淘汰的。 总结一下:

  • redis服务器在命令执行时检查内存是否需要淘汰;
  • 内存淘汰策略分为8种;
  • 如果是随机策略,那么64位机器使用梅森旋转,32位机器使用系统随机数;
  • redis每个对象都有对象头,LRU或LFU算法依靠对象头里的lru属性进行;
  • 对象头里的lru属性共24位,8位用于存储LFU信息,16位用于存储lru信息;

expire.c

expire的触发也是serverCron->databasesCron() 不过在databasesCron里优先级很高,是第一个调用的,可以看看代码.

void databasesCron(void) {
/* Expire keys by random sampling. Not required for slaves
* as master will synthesize DELs for us. */

if (server.active_expire_enabled) {
if (iAmMaster()) {
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
} else {
expireSlaveKeys();
}
}
// 省略其他代码
}

不过expire还有一处触发的地方。 aeProcessEvents->beforeSleep. 在server.c的beforeSleep里有这么一段代码:

/* Run a fast expire cycle (the called function will return
* ASAP if a fast cycle is not needed). */

if (server.active_expire_enabled && iAmMaster())
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);

if (moduleCount()) {
moduleFireServerEvent(REDISMODULE_EVENT_EVENTLOOP,
REDISMODULE_SUBEVENT_EVENTLOOP_BEFORE_SLEEP,
NULL);
}

区别在于参数不同,一个是快,一个是慢。 ACTIVE_EXPIRE_CYCLE_SLOW 和ACTIVE_EXPIRE_CYCLE_FAST两种模式差别比较大。 比如时间限制的差别

timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
timelimit_exit = 0;
if (timelimit <= 0) timelimit = 1;

if (type == ACTIVE_EXPIRE_CYCLE_FAST)
timelimit = config_cycle_fast_duration; /* in microseconds. */

从执行时间上看,慢模式依赖server.hz这个重要的配置。server.hz可以理解为心跳频率,决定redis每秒可以做多少事情。

zmalloc内存管理

zmalloc.c 是redis的内存管理器适配文件,在zmalloc.c里,redis会根据操作系统类型、已有的内存管理框架,去选择适配的内存管理库。 其优先级是google tcmalloc > jemalloc > 操作系统实现. 但是一般不会机器上不会安装tcmalloc,而jemalloc又包含在redis的源码目录中,所以一般情况下使用的都是jemalloc.

defrag,evict和expire都依赖zmalloc.c里的函数。

赞(0)
未经允许不得转载:171主机测评 » redis内存管理源码分析
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址