Redis 記憶體管理賞析
宣告:本文是對 4.0.6版本 Redis的 記憶體管理部分的(xuexi)總結,有些YY的成分,作者本意不想誤導, 如有錯誤, 敬請諒解。
一、凡事先問個為什麼
Redis是網際網路公司主流的分散式快取解決方案,快取的本質是“ 就近暫存 ”,解決的是“ 減少去較遠的地方拿我想要的東西要耗費較大成本 ”的問題。本小節我們要探究的不是為什麼要用快取,而是Redis為什麼要做記憶體管理這件事情。決定一件事情做不做的理由 絕對不是 “這件事情可以做”,我認為Redis做記憶體管理的主要原因是: 記憶體(RAM)資源寶貴,記憶體管理讓記憶體的使用情況可控 。
二、遵循底線、條條大路通羅馬
那麼怎麼做記憶體管理呢?管理包括對個體的管理和對總體的管理,個體上比如對Redis中儲存的每個物件的大小,質量等進行管理;Redis的做法遵循 簡單原則 ,在記憶體使用的總量上劃一道“ 允許最大使用記憶體量 ”這條底線。為了遵循這條底線,Redis在實現上是這麼做的:當使用記憶體量高於這條底線的時候,所有的寫操作將會直接失敗,絕不手軟,但是這個時候仍然理智地支援讀操作。在底線的基礎之上Redis提供的管理手段是“淘汰機制”,注意 淘汰機制是管理的一種手段 。那麼怎麼淘汰呢?按大小?按年齡?Redis提供以下幾種選擇淘汰KEY的策略:
LRU: 看看哪個KEY最長時間沒有被使用啦,就選你吧,你看你這麼長時間都沒被使用了,就別站著地方了。
LFU: 如果說上面的方法較為片面,那這裡就加上一個期限,看看哪個KEY在一段時間內訪問的頻率最低好吧,誰最低,淘汰誰。
RANDOM: 不想那麼多了,整個世界都是隨機的,隨機的世界就隨機選擇一個KEY吧,愛咋咋地。
TTL: 選擇最近即將過期的KEY進行淘汰。
最後一種比較有意思,那就是—— 不淘汰 ,哈哈, 對於一個問題不去解決往往也是一種解決方案 。
三、難易相成、長短相形
在主從模式且主庫開啟maxmemory策略的情況下,對於與主庫來說,如果用於從庫同步的緩衝區也被作為記憶體使用量計算進來,在某些極端情況下,比如:主從之間網路出現異常,這個時候記憶體使用又達到閾值,顯然Redis此時會進行eviction操作,eviction操作本身會產生大量的DELs操作日誌用於同步給從庫,這部分日誌有會填充在主從同步快取中,由於這部分記憶體會被計算進記憶體使用量,因此又會觸發eviction操作,然後繼續惡性迴圈……,直至最後記憶體被清空。 引入一種方案時長會伴隨著引入新的問題和憂患,這需要我們提高警惕 ,那麼怎麼解決這個問題呢?Redis的做法很簡單,就是用於從庫同步的緩衝區記憶體大小不作為使用記憶體計算的,這樣就不會存在上述問題了。在實際使用中,當以主從模式使用Redis且開啟了maxmemory策略的時候,建議maxmemory這個值設定稍微小一些,預留一部分給主動同步緩衝區使用。
四、損有餘、補不足
在實際實現LRU、LFU和最小TTL演算法的時候面臨一些問題,比如LRU演算法要求淘汰最長時間沒有使用的KEY,如果要精準滿足“ 最長時間沒有使用 ”這個條件,我們必須要遍歷所有的KEY然後才能根據其上次使用的時間計算出哪個KEY是最長時間沒有被訪問的。問題來了,實際上Redis中可能存在成千上萬個KEY,每次都要挨個遍歷豈不是要瘋了? 實際想一想精確真的是我們想要的嗎? 實際Redis實現的LRU、LFU和最小TTL演算法都是基於近似演算法計算的,用 精確度換時間 (擴充套件一下hyperloglog是用準確性換空間),關鍵演算法是:預設情況下Redis每次只隨機選擇5(可以通過maxmemory-samples引數設定,maxmemory-samples越大的時候越消耗記憶體,maxmemory-samples越小的時候越快,但是越不精確)個KEY,然後從這5個KEY中找到最符合條件的,這個思想其實也類似選擇擇區域性最優,放棄全域性最優的貪心演算法。
五、精益求精、追求卓越
無論是LRU、LFU還是TTL都是基於系統時間的,在一般作業系統中,讀取系統的當前時間需要進行system call,system call意味著作業系統需要陷入核心態,陷入核心態意味著使用者態與核心態的上下文切換,上下文切換意味著成本……。為了實現記憶體淘汰,Redis需要為每個KEY都維護著一個這樣的時間,而且在KEY被訪問的時候需要讀取作業系統當前時間將其更新,試想一下,在上W QPS的應用場景下,每次都要去system call在高效能的場景下是一件多麼恐怖的事情。實際Redis是怎麼處理的呢?這裡有個背景,Redis所謂的單執行緒指的是Redis對於客戶端讀寫的處理和內部的一些日常事務處理都是由一個eventloop執行緒來處理的,這裡的日常事務包括一項:每次讀取當前系統時間記錄在一個變數裡面。一般情況Redis所讀取的時間都是從這裡讀取的,也就是由於eventloop執行緒提前計算好的,當然如果eventloop的loop頻率比較低的話這個值將會具有一定延遲。
