內存管理

8.2.內存管理

        Redis主要是通過控制內存上限和相應的回收策略實現內存管理,本節將圍繞這兩個方面來介紹Redis如何管理內存。

8.2.1 內存上限

        Redis使用maxmemory參數限制最大可用內存。限制內存的目的主要有:

  • 用於緩存場景,當超出內存上限maxmemory時使用LRU等刪除策略釋放空間。

  • 防止所用內存超過服務器物理內存。

        需要注意maxmemory限制的是Redis內存實際使用的內存量,也就是used_memory統計項對應的內存。由於內存碎片率的存在,實際消耗的內存可能會比maxmemory設置的更大,實際使用時要小心這部分內存溢出。通過內存上限可以非常方便的實現一臺服務器部署多個Redis進程的內存控制。比如一臺24GB內存的服務器,爲系統預留4GB內存,預留4GB空閒內存給其他進程或Redis fork進程,留給Redis 16GB內存,這樣可以部署4maxmemory=4GBRedis進程。得益於Redis單線程架構和內存限制機制,即使沒有采用虛擬化不同的Redis進程之間也可以很好的實現CPU和內存的隔離性,如圖8-2所示。

spacer.gif

8-2:服務器分配44GBRedis進程

8.2.2 動態調整內存上限

        Redis的內存上限可以通過config set maxmemory {bytes} 動態修改最大可用內存。例如之前的示例,當發現Redis-1沒有做好內存預估實際只用了不到2GB內存,而Redis-2進程需要擴容到6GB內存纔夠用,這時可以分別執行如下命令調整:

Redis-1>config set maxmemory 2GB
Redis-2>config set maxmemory 6GB

        通過動態修改maxmemory,可以實現在當前服務器下動態伸縮Redis內存的目的,如圖8-3所示。

spacer.gif

8-3:redis進程之間調整maxmemory伸縮內存

        這個例子過於理想化,如果此時Redis-3Redis-4進程也需要分別擴容到6Gb,這時超出系統物理內存限制就不能簡單的通過調整maxmemory來達到擴容的目的,需要採用在線遷移數據或者基於複製切換服務器來達到擴容的目的,具體細節見集羣章節和哨兵章節。

運維提示:
         1:Redis默認無限使用服務器內存,爲防止極端情況系統內存耗盡,建議所有的Redis進程都要配置maxmemory。
         2:在保證物理內存足夠的情況下,服務器上所有的redis進程可以調整maxmemory參數來達到自由伸縮最大可用內存的目的。

8.2.3 內存回收策略

Redis的內存回收機制主要體現在以下兩個方面:

  • 刪除到達過期時間的鍵對象

  • 內存使用達到maxmemory上線時觸發內存溢出控制策略

1:刪除過期鍵對象

   Redis所有的鍵都可以設置過期屬性,內部保存在過期字典中。由於進程內保存大量的鍵,維護每個鍵精準的過期刪除機制會導致消耗大量的CPU,對於單線程的Redis來說成本過高,因此Redis採用惰性刪除和定時任務刪除機制實現過期健的內存回收。

1)惰性刪除:

  惰性刪除用於當客戶端讀取帶有超時屬性的鍵時,如果已經超過鍵設置的過期時間,會執行刪除操作並返回空,這種策略是出於節省CPU成本考慮,不需要單獨維護TTL鏈表來處理過期鍵的刪除。但是這種方式存在內存泄露的問題,當過期鍵一直沒有訪問將無法得到及時刪除,從而導致內存不能及時釋放。正因爲如此,Redis還提供另一種定時任務刪除機制作爲惰性刪除的補充。

2)定時任務刪除:

Redis內部維護一個的定時任務,默認每秒運行10(通過參數hz控制)。定時任務中刪除過期鍵邏輯採用了自適應算法,根據鍵的過期比例使用快慢兩種速率模式回收鍵,流程如圖8-4:

spacer.gif

8-4:定時任務刪除過期鍵邏輯

流程說明:

  • 定時任務在每個數據庫空間隨機檢查20個過期鍵,當發現過期時刪除對應的鍵。

  • 如果超過檢查數25%的鍵過期,循環執行回收邏輯直到不足25%或運行超時爲止,慢模式下超時時間爲25毫秒。

  • 如果之前回收鍵邏輯運行超過,則在Redis觸發內部事件之前再次以快模式運行回收過期鍵任務,快模式下超時時間爲1毫秒且2秒內只能運行1次。

  • 快慢兩種模式內部刪除邏輯相同,只是執行的超時時間不同。

Icon

開發提示:

  1. Redis定時任務通過hz參數控制週期,定時任務內部涉及很多邏輯如:關閉超時連接,刪除過期鍵,AOF寫文件頻率,集羣定時通信等,修改這個參數影響範圍非常廣,不建議調大hz參數來加速回收內存頻率。

  2. 避免大量鍵設置相同的過期時間,否則容易產生同一時刻超過25%的鍵過期場景,觸發循環刪除過期鍵邏輯從而拖慢Redis響應速度。

2:內存溢出控制策略

     Redis所用內存達到maxmemory上限時會觸發相應的溢出控制策略。具體策略受maxmemory-policy參數控制,Redis支持6種策略如下:

  • noeviction:默認策略,不會刪除任何數據,拒絕所有寫入操作並返回客戶端錯誤信息"(error) OOM command not allowed when used memory",此時Redis只響應讀操作。

  • volatile-lru:根據LRU算法刪除設置了超時屬性(expire)的鍵,直到騰出足夠空間爲止。如果沒有可刪除的鍵對象,回退到noeviction策略。

  • allkeys-lru:根據LRU算法刪除鍵,不管數據有沒有設置超時屬性,直到騰出足夠空間爲止。

  • allkeys-random:隨機刪除所有鍵,直到騰出足夠空間爲止。

  • volatile-random:隨機刪除過期鍵,直到騰出足夠空間爲止。

  • volatile-ttl:根據鍵值對象的ttl屬性,刪除最近將要過期數據。如果沒有,回退到noeviction策略。

        內存溢出控制策略可以採用config set maxmemory-policy {policy} 動態配置。Redis支持豐富的內存溢出應對策略,可以根據實際需求靈活定製,比如當設置volatile-lru策略時,保證具有過期屬性的鍵可以根據LRU剔除,而未設置超時的鍵可以永久保留。還可以採用allkeys-lru策略把Redis變爲純緩存服務器使用。當Redis因爲內存溢出刪除鍵時,可以通過執行info stats命令查看evicted_keys指標找出當前Redis服務器已剔除的鍵數量。

   每次Redis執行命令時如果設置了maxmemory參數,都會嘗試執行回收內存操作。當Redis一直工作在內存溢出(used_memory>maxmemory)的狀態下且設置非noeviction策略時,會頻繁的觸發回收內存的操作,影響Redis服務器的性能。回收內存邏輯僞代碼如下:

def freeMemoryIfNeeded() :
    int mem_used, mem_tofree, mem_freed;
    // 計算當前內存總量,排除從節點輸出緩衝區和AOF緩衝區的內存佔用
    int slaves = server.slaves;
    mem_used = used_memory()-slave_output_buffer_size(slaves)-aof_rewrite_buffer_size();
    // 如果當前使用小於等於maxmemory退出
    if (mem_used <= server.maxmemory) :
        return REDIS_OK;
    // 如果設置內存溢出策略爲noeviction(不淘汰),返回錯誤。
    if (server.maxmemory_policy == 'noeviction') :
        return REDIS_ERR;
    // 計算需要釋放多少內存
    mem_tofree = mem_used - server.maxmemory;
    // 初始化已釋放內存量
    mem_freed = 0;
    // 根據maxmemory-policy策略循環刪除鍵釋放內存
    while (mem_freed < mem_tofree) :
        // 迭代Redis所有數據庫空間
        for (int j = 0; j < server.dbnum; j++) :
            String bestkey = null;
            dict dict;
            if (server.maxmemory_policy == 'allkeys-lru' ||
                server.maxmemory_policy == 'allkeys-random'):
                //如果策略是 allkeys-lru/allkeys-random 回收內存目標爲所有的數據庫鍵
                dict = server.db[j].dict;
            else :
                // 如果策略是volatile-lru/volatile-random/volatile-ttl回收內存目標爲帶過期時間的數據庫鍵
                dict = server.db[j].expires;
            
            // 如果使用的是隨機策略,那麼從目標字典中隨機選出鍵
            if (server.maxmemory_policy == 'allkeys-random' ||
                server.maxmemory_policy == 'volatile-random') :
                //隨機返回被刪除鍵
                bestkey = get_random_key(dict);
            else if (server.maxmemory_policy == 'allkeys-lru' ||
                server.maxmemory_policy == 'volatile-lru') :
                //循環隨機採樣maxmemory_samples次(默認5次),返回相對空閒時間最長的鍵
                bestkey = get_lru_key(dict);
            else if (server.maxmemory_policy == 'volatile-ttl') :
                //循環隨機採樣maxmemory_samples次,返回最近將要過期的鍵
                bestkey = get_ttl_key(dict);
       
            // 刪除被選中的鍵
            if (bestkey != null) :
                long delta = used_memory();
                deleteKey(bestkey);
                                   // 計算刪除鍵所釋放的內存量
                delta -= used_memory();
                mem_freed += delta;
                //刪除操作同步給從節點
                if (slaves):
                    flushSlavesOutputBuffers();
            
    return REDIS_OK;

   從僞代碼可以看到,頻繁執行回收內存成本很高,主要包括查找可回收鍵和刪除鍵的開銷,如果當前Redis有從節點,回收內存操作對應的刪除命令會同步到從節點,導致寫放大的問題,如圖8-5所示。

spacer.gif

8-5:回收內存觸發刪除邏輯

Icon

開發提示:建議線上Redis內存工作在maxmemory>used_memory 狀態下,避免頻繁內存回收開銷。

       對於需要收縮Redis內存的場景,可以通過調小maxmemory來實現快速回收。比如對一個實際佔用6GB內存的進程設置maxmemory=4GB,之後第一次執行命令時,如果使用非noeviction策略,它會一次性回收到maxmemory指定的內存量,從而達到快速回收內存的目的,注意此操作會導致數據丟失,一般在緩存場景下使用。

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章