Redis 4.0 自动内存碎片整理(Active Defrag)源码分析

阅读本文前建议先阅读此篇博客: <a href = "http://zhangtielei.com/posts/blog-redis-how-to-start.html">Redis源码从哪里读起</a>html

Redis 4.0 版本增长了许多不错的新功能,其中自动内存碎片整理功能 activedefrag 确定是很是诱人的一个,这让 Redis 集群回收内存碎片相比 Redis 3.0 更加优雅,便利。咱们升级 Redis 4.0 后直接开启了activedefrag,通过删除部分 key 测试,发现它确实能有效的释放内存碎片,可是并无测试它其余相关参数。git

1、问题现象

因为业务须要,咱们删除了集群中占内存 2/3 的 Key,删除后集群平均碎片率在 1.3 ~ 1.4,内存明显降低,可是此时服务的响应猛然增高,咱们经过 redis.cli -c -h 127.0.0.1 -p 5020 --latency 在服务端测试集群性能,发现响应(网络+排队)达到了 2-3ms,这对于 redis 来讲已经很是高了,咱们其余集群响应通常都在 0.2ms 左右。通过排查后,咱们尝试将 activedefrag 功能关闭,并测试,发现 redis 服务端响应立刻恢复正常,线上服务响应也降了下来,打开 activedefrag 响应立刻飙高。github

2、Redis 4.0 源码分析(基于分支 4.0)

Active Defrag 功能的核心代码都在 defrag.c 中的activeDefragCycle(void)函数redis

1. Active Defrag 介绍及相关参数

咱们先看一下redis.conf 中关于 activedefrag 的注释(google 翻译)shell

功能介绍服务器

警告此功能是实验性的。然而,即便在生产中也进行了压力测试,而且由多个工程师手动测试了一段时间。
什么是主动碎片整理?
-------------------------------
自动(实时)碎片整理容许Redis服务器压缩内存中小数据分配和数据释放之间的空间,从而容许回收内存。

碎片化是每一个分配器都会发生的一个天然过程(幸运的是,对于Jemalloc来讲却不那么重要)和某些工做负载。一般须要从新启动服务器以下降碎片,或者至少刷新全部数据并再次建立。
可是,因为Oran Agra为Redis 4.0实现了这一功能,这个过程能够在运行时以“热”的方式发生,而服务器正在运行。

基本上当碎片超过必定水平时(参见下面的配置选项),Redis将开始经过利用某些特定的Jemalloc功能在相邻的内存区域中建立值的新副本(以便了解分配是否致使碎片并分配它在一个更好的地方),同时,将释放数据的旧副本。对于全部键,以递增方式重复此过程将致使碎片回退到正常值。
须要了解的重要事项:
1.默认状况下,此功能处于禁用状态,仅在您编译Redis以使用咱们随Redis源代码提供的Jemalloc副本时才有效。这是Linux版本的默认设置。
2.若是没有碎片问题,则永远不须要启用此功能。
3.一旦遇到碎片,能够在须要时使用命令“CONFIG SET activedefrag yes”启用此功能。配置参数可以微调其行为碎片整理过程。若是您不肯定它们的含义,最好保持默认设置不变。

参数介绍网络

# 开启自动内存碎片整理(总开关)
activedefrag yes
# 当碎片达到 100mb 时,开启内存碎片整理
active-defrag-ignore-bytes 100mb
# 当碎片超过 10% 时,开启内存碎片整理
active-defrag-threshold-lower 10
# 内存碎片超过 100%,则尽最大努力整理
active-defrag-threshold-upper 100
# 内存自动整理占用资源最小百分比
active-defrag-cycle-min 25
# 内存自动整理占用资源最大百分比
active-defrag-cycle-max 75

2. Active Defrag Timer 在那个线程中执行的?

Redis 是基于事件驱动的,Timer事件和I/O事件会注册到主线程当中,其中内存碎片整理Timer也是在主线程当中执行的。数据结构

<a href = "http://zhangtielei.com/posts/blog-redis-how-to-start.html">原文引用[1]</a>app

  • 注册timer事件回调。Redis做为一个单线程(single-threaded)的程序,它若是想调度一些异步执行的任务,好比周期性地执行过时key的回收动做,除了依赖事件循环机制,没有其它的办法。这一步就是向前面刚刚建立好的事件循环中注册一个timer事件,并配置成能够周期性地执行一个回调函数:serverCron。因为Redis只有一个主线程,所以这个函数周期性的执行也是在这个线程内,它由事件循环来驱动(即在合适的时机调用),但不影响同一个线程上其它逻辑的执行(至关于按时间分片了)。serverCron函数到底作了什么呢?实际上,它除了周期性地执行过时key的回收动做,还执行了不少其它任务,好比主从重连、Cluster节点间的重连、BGSAVE和AOF rewrite的触发执行,等等。这个不是本文的重点,这里就不展开描述了。
  • 注册I/O事件回调。Redis服务端最主要的工做就是监听I/O事件,从中分析出来自客户端的命令请求,执行命令,而后返回响应结果。对于I/O事件的监听,天然也是依赖事件循环。前面提到过,Redis能够打开两种监听:对于TCP链接的监听和对于Unix domain socket的监听。所以,这里就包含对于这两种I/O事件的回调的注册,两个回调函数分别是acceptTcpHandleracceptUnixHandler。对于来自Redis客户端的请求的处理,就会走到这两个函数中去。咱们在下一部分就会讨论到这个处理过程。另外,其实Redis在这里还会注册一个I/O事件,用于经过管道(pipe)机制与module进行双向通讯。这个也不是本文的重点,咱们暂时忽略它。
  • 初始化后台线程。Redis会建立一些额外的线程,在后台运行,专门用于处理一些耗时的而且能够被延迟执行的任务(通常是一些清理工做)。在Redis里面这些后台线程被称为bio(Background I/O service)。它们负责的任务包括:能够延迟执行的文件关闭操做(好比unlink命令的执行),AOF的持久化写库操做(即fsync调用,但注意只有能够被延迟执行的fsync操做才在后台线程执行),还有一些大key的清除操做(好比flushdb async命令的执行)。可见bio这个名字有点名存实亡,它作的事情不必定跟I/O有关。对于这些后台线程,咱们可能还会产生一个疑问:前面的初始化过程,已经注册了一个timer事件回调,即serverCron函数,按说后台线程执行的这些任务彷佛也能够放在serverCron中去执行。由于serverCron函数也是能够用来执行后台任务的。实际上这样作是不行的。前面咱们已经提到过,serverCron由事件循环来驱动,执行仍是在Redis主线程上,至关于和主线程上执行的其它操做(主要是对于命令请求的执行)按时间进行分片了。这样的话,serverCron里面就不能执行过于耗时的操做,不然它就会影响Redis执行命令的响应时间。所以,对于耗时的、而且能够被延迟执行的任务,就只能放到单独的线程中去执行了。

3.Active Defrag Timer 的逻辑何时会执行?

在参数介绍中咱们能看出,activedefrag 是一个总开关,当开启时才有可能执行,而是否真正执行则须要下面几个参数控制。dom

void activeDefragCycle(void) {
    /* ... */

    /* 每隔一秒,检查碎片状况,决定是否执行*/
    run_with_period(1000) {
        size_t frag_bytes;
        /* 计算碎片率和碎片大小*/
        float frag_pct = getAllocatorFragmentation(&frag_bytes);
        /* 若是没有运行或碎片低于阈值,则不执行 */
        if (!server.active_defrag_running) {
            /* 根据计算的碎片率和大小与咱们设置的参数进行比较判断,决定是否执行 */
            if(frag_pct < server.active_defrag_threshold_lower || frag_bytes < server.active_defrag_ignore_bytes)
                return;
        }
    /* ... */
}

经过源码,咱们能够看出碎片整理是否执行主要是经过active_defrag_running, active-defrag-ignore-bytes, active-defrag-threshold-lower 这几个参数共同决定的。
官方默认设置内存碎片率大于10%且内存碎片大小超过100mb。

4.Active Defrag 为何会影响Redis集群的响应?

咱们将 Redis 集群2/3的数据都删除了,碎片率很快降到 1.3 左右,内存也被很快释放,可是为何 Redis 响应会变高呢?

首先,咱们内存碎片整理是在主线程中执行的,经过源码发现,内存碎片整理操做会 scan (经过迭代进行)整个 redis 节点,并进行内存复制、转移等操做,由于 redis 是单线程的,因此这确定会致使 redis 性能降低(经过调整相关配置能够控制内存整理对 redis 集群的影响,后面会详细说明)。

经过 redis 日志发现,碎片整理还在不停地执行,并使用了75%的CPU(咱们将其解释为 redis 主线程资源的 75%),每次执行耗时82s(此处注意,虽然耗时82s,可是并非 redis 主线程阻塞的这么久的时间,而是从第一次迭代到最后一次迭代之间的时间,在此时间以内主线程可能还会处理命令请求)。
从日志中可见frag=14%,咱们配置的参数一直能达到内存碎片整理的阈值,主线程会不停的去进行内存碎片整理,致使redis集群性能变差。

/* redis 配置及日志
 * activedefrag yes
 * active-defrag-ignore-bytes 100mb
 * active-defrag-threshold-lower 10
 * active-defrag-threshold-upper 100
 * active-defrag-cycle-min 25
 * active-defrag-cycle-max 75 */
11:M 28 May 06:37:17.430 - Starting active defrag, frag=14%, frag_bytes=484401800, cpu=75%
11:M 28 May 06:38:40.424 - Active defrag done in 82993ms, reallocated=50, frag=14%, frag_bytes=484365248

# redis 性能
[service@bigdata src]$ ./redis-cli -h 127.0.0.1 -p 5020 --latency
min: 0, max: 74, avg: 7.38 (110 samples)

咱们先将 activedefrag 置为 no,此时响应立刻恢复正常。

# redis 性能
min: 0, max: 1, avg: 0.14 (197 samples)

5.Active Defrag 相关参数该怎么调整?

内存碎片整理的功能咱们仍是须要的,那么咱们该如何调整参数才能在redis性能和内存碎片整理之间找到一个平衡点呢?因而我对这几个参数进行调整测试。

(1) 调整active-defrag-ignore-bytesactive-defrag-threshold-lower
此调整是相对简单的,仅用来判断是否进入内存碎片整理逻辑,若是将碎片率或碎片大小调大至一个能接受的阈值,redis 不进行内存碎片整理,则不会对集群有过多的影响。从下面的代码咱们能够发现,当两个条件都知足时,则会进入内存碎片整理逻辑。

if (!server.active_defrag_running) {
    if(frag_pct < server.active_defrag_threshold_lower || frag_bytes < server.active_defrag_ignore_bytes)
        return;
}

此处须要注意,frag_pctfrag_bytes 并不等于 info 命令中的 mem_fragmentation_ratio,好比这次问题出现时,mem_fragmentation_ratio = 1.31, 而经过frag_pct计算的碎片率是 1.14,因此设置参数时不能彻底参考info中的mem_fragmentation_ratio信息。

/* frag_pct 是从 jemalloc 获取的 */
/* Utility function to get the fragmentation ratio from jemalloc.
 * It is critical to do that by comparing only heap maps that belown to
 * jemalloc, and skip ones the jemalloc keeps as spare. Since we use this
 * fragmentation ratio in order to decide if a defrag action should be taken
 * or not, a false detection can cause the defragmenter to waste a lot of CPU
 * without the possibility of getting any results. */
float getAllocatorFragmentation(size_t *out_frag_bytes) {
    size_t epoch = 1, allocated = 0, resident = 0, active = 0, sz = sizeof(size_t);
    /* Update the statistics cached by mallctl. */
    je_mallctl("epoch", &epoch, &sz, &epoch, sz);
    /* Unlike RSS, this does not include RSS from shared libraries and other non
     * heap mappings. */
    je_mallctl("stats.resident", &resident, &sz, NULL, 0);
    /* Unlike resident, this doesn't not include the pages jemalloc reserves
     * for re-use (purge will clean that). */
    je_mallctl("stats.active", &active, &sz, NULL, 0);
    /* Unlike zmalloc_used_memory, this matches the stats.resident by taking
     * into account all allocations done by this process (not only zmalloc). */
    je_mallctl("stats.allocated", &allocated, &sz, NULL, 0);
    float frag_pct = ((float)active / allocated)*100 - 100;
    size_t frag_bytes = active - allocated;
    float rss_pct = ((float)resident / allocated)*100 - 100;
    size_t rss_bytes = resident - allocated;
    if(out_frag_bytes)
        *out_frag_bytes = frag_bytes;
    serverLog(LL_DEBUG,
        "allocated=%zu, active=%zu, resident=%zu, frag=%.0f%% (%.0f%% rss), frag_bytes=%zu (%zu%% rss)",
        allocated, active, resident, frag_pct, rss_pct, frag_bytes, rss_bytes);
    return frag_pct;
}
/* mem_fragmentation_ratio */
/* Fragmentation = RSS / allocated-bytes */
float zmalloc_get_fragmentation_ratio(size_t rss) {
    return (float)rss/zmalloc_used_memory();
}

(2)调整active-defrag-cycle-minactive-defrag-cycle-max
这两个参数是占用主线程资源比率的上下限,若是想保证内存碎片整理功能不过分影响 redis 集群性能,则须要仔细斟酌着两个参数的配置。
当我调整这两个参数时,我经过观察内存整理时的耗时、资源占用、redis响应等状况发现——当资源占用越多时,内存碎片整理力度越大,时间越短,固然对redis性能的影响也更大。

# active-defrag-cycle-min 10
# active-defrag-cycle-max 10

# 日志记录-耗时、资源占用
11:M 28 May 08:37:39.458 - Starting active defrag, frag=15%, frag_bytes=502210608, cpu=10%
11:M 28 May 08:45:26.160 - Active defrag done in 466700ms, reallocated=187804, frag=14%, frag_bytes=493183888

# redis 响应
min: 0, max: 27, avg: 2.69 (295 samples)
# active-defrag-cycle-min 5
# active-defrag-cycle-max 10

# 日志记录-耗时、资源占用
11:M 28 May 07:08:29.988 - Starting active defrag, frag=14%, frag_bytes=487298400, cpu=5%
11:M 28 May 07:22:58.225 - Active defrag done in 868237ms, reallocated=4555, frag=14%, frag_bytes=484875424

# redis 响应
min: 0, max: 6, avg: 0.44 (251 samples)

(3) 综合调整
在此以前,咱们还须要再看一下activeDefragCycle(void)这个函数的具体逻辑 <a href="https://github.com/antirez/redis/blob/4.0/src/defrag.c">defrag.c</a>
Tips: C 语言中被 static 修饰的变量是全局的,以下代码中的cursor

/* 从serverCron执行增量碎片整理工做。
 * 这与activeExpireCycle的工做方式相似,咱们在调用之间进行增量工做。 */
void activeDefragCycle(void) {
    static int current_db = -1;
    /* 游标,经过迭代scan 整个 redis 节点*/
    static unsigned long cursor = 0;
    static redisDb *db = NULL;
    static long long start_scan, start_stat;
    /* 迭代计数器 */
    unsigned int iterations = 0;
    unsigned long long defragged = server.stat_active_defrag_hits;
    long long start, timelimit;

    if (server.aof_child_pid!=-1 || server.rdb_child_pid!=-1)
        return; /* Defragging memory while there's a fork will just do damage. */

    /* 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;
        }

        /* 计算内存碎片整理所须要占用的主线程资源 */
        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;

    /* See activeExpireCycle for how timelimit is handled. */
    start = ustime();
    /* 计算每次迭代的时间限制 */
    timelimit = 1000000*server.active_defrag_running/server.hz/100;
    if (timelimit <= 0) timelimit = 1;

    do {
        if (!cursor) {
            /* Move on to next database, and stop if we reached the last one. */
            if (++current_db >= server.dbnum) {
                long long now = ustime();
                size_t frag_bytes;
                float frag_pct = getAllocatorFragmentation(&frag_bytes);
                serverLog(LL_VERBOSE,
                    "Active defrag done in %dms, reallocated=%d, frag=%.0f%%, frag_bytes=%zu",
                    (int)((now - start_scan)/1000), (int)(server.stat_active_defrag_hits - start_stat), frag_pct, frag_bytes);

                start_scan = now;
                current_db = -1;
                cursor = 0;
                db = NULL;
                server.active_defrag_running = 0;
                return;
            }
            else if (current_db==0) {
                /* Start a scan from the first database. */
                start_scan = ustime();
                start_stat = server.stat_active_defrag_hits;
            }

            db = &server.db[current_db];
            cursor = 0;
        }

        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. */
            /* 一旦进入16次扫描迭代,或1000次指针从新分配(若是咱们在一个散列桶中有不少指针),检查咱们是否达到了tiem限制。*/
            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);
    } while(1);
}

经过代码逻辑分析,咱们注意到有两个计算cpu_pct(资源占用率)的函数

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);

/* 插值运算函数 */
#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)))

假设咱们设置参数以下(产线配置)

active-defrag-ignore-bytes 500mb
active-defrag-threshold-lower 50
active-defrag-threshold-upper 100
active-defrag-cycle-min 5
active-defrag-cycle-max 10

(1) 咱们能够得出第一个计算 cpu_pct的第一个函数 y = 0.1x
(2) 假设此时的 frag_pct = 100 & frag_bytes > 500mb, 则cpu_pct = 10
(3) 在通过求极值函数计算后,获得最后的 cpu_pct的值 10
(4) 而后经过这个值进而计算出timelimit = 1000000*server.active_defrag_running(10)/server.hz(in redis.conf 10)/100 = 10000μs = 10ms
(5) 最后 Redis 自动内存碎片整理功能经过timelimit的值来尽量的保证不集中性地占用主线程资源

6.Memory Purge 手动整理内存碎片

此处顺便介绍一下 Memory Purge 功能。
memory purge是手动触发整理内存碎片的 Command,它会以一个I/O事件的形式注册到主线程当中去执行。值得注意的是,它和 activedefrag回收的并非同一块区域的内存,它尝试清除脏页以便内存分配器回收使用
具体逻辑,咱们来看一下源码中的实现,<a href="https://github.com/antirez/redis/blob/4.0/src/object.c">object.c</a>

/*必须是使用jemalloc内存分配器时才可用*/
#if defined(USE_JEMALLOC)
    char tmp[32];
    unsigned narenas = 0;
    size_t sz = sizeof(unsigned);
    /*获取arenas的个数,而后调用jemalloc的接口进行清理 */
    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

关于arenas相关的知识,能够参考这篇文章的解释。<a href = "https://blog.csdn.net/txx_683/article/details/53469175">原文引用[2]</a>

从产线实际使用的状况中来看,memory purge 的效果相比于activedefrag并无那么的理想,这也是其机制决定的,可是某些内存碎片率比较极端的状况下,也会起到必定的做用。建议根据实际状况,和activedefrag配合使用。

3、Active Defrag 参数调整建议

综上,咱们总结出,咱们经过active-defrag-ignore-bytesactive-defrag-threshold-lower来控制是否进行内存碎片整理,经过active-defrag-cycle-minactive-defrag-cycle-max来控制整理内存碎片的力度。 因为各个公司的Redis集群大小,存储的数据结构都会存在差别,因此在开启自动的内存碎片整理的开关后,必定要依据自身的实际状况来设置整理内存碎片的力度的参数。

参考文章: [1] <a href = "http://zhangtielei.com/posts/blog-redis-how-to-start.html">Redis源码从哪里读起</a> [2] <a href = "https://my.oschina.net/watliu/blog/1620705">redis4支持内存碎片清理功能实现分析</a> [3] <a href = "https://blog.csdn.net/txx_683/article/details/53469175">jemalloc 3.6.0源码详解—[1]Arena</a>

相关文章
相关标签/搜索