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>

相關文章
相關標籤/搜索