Redis 記憶體壓縮實戰,學習了!

語言: CN / TW / HK

作者:Xie Zefan<br> 來源:https://xiezefan.me/2017/05/01/redis_in_action_ziplist/

在討論Redis記憶體壓縮的時候,我們需要了解一下幾個Redis的相關知識。

壓縮列表 ziplist

Redis的ziplist是用一段連續的記憶體來儲存列表資料的一個數據結構,它的結構示例如下圖

壓縮列表組成示例--截圖來自《Redis設計與實現》

  1. zlbytes: 記錄整個壓縮列表使用的記憶體大小
  2. zltail: 記錄壓縮列表表尾距離起始位置有多少位元組
  3. zllen: 記錄壓縮列表節點數量,值得注意的一點是,因為它只佔了2個位元組,所以最大值只能到65535,這意味著壓縮列表長度大於65535的時候,就只能通過遍歷整個列表來計算長度了
  4. zleng: 壓縮列表末端標誌位,固定值為OxFF
  5. entry1-N: 壓縮列表節點, 具體結構如下圖

壓縮列表節點組成示例--截圖來自《Redis設計與實現》

其中

  1. previous_entry_length: 上一個節點的長度
  2. encoding: content的編碼以及長度
  3. content: 節點資料

當我們查詢一個節點的時候,主要進行一下操作:

  1. 根據zltail獲取最後一個節點的位置
  2. 判斷當前節點是否是目標節點
  3. 如果是,則返回資料
  4. 如果不是,則根據previous_entry_length計算上一個節點的起始位置,然後重新進行步驟2判斷

通過上述的描述,我們可以知道,ziplist每次資料更新的複雜度大約是O(N),因為它需要對N個節點進行記憶體重分配,查詢一個數據的時候,複雜度是O(N),最壞情況下需要遍歷整個列表。

什麼情況下會使用到ziplist呢?

Redis會使用到ziplist的資料結構是Hash與List。

Hash結構使用ziplist作為底層儲存的兩個條件是:

  1. 所有的鍵與值的字串長度都小於64位元組的時候
  2. 鍵與值對資料小於512個

只要上述條件任何一個不滿足,Redis就會自動將這個Hash物件從ziplist轉換成hashtable。但這兩個閾值可以通過修改配置檔案中的hash-max-ziplist-valuehash-max-ziplist-entries來變更。

List結構使用ziplist的條件與Hash結構一樣,當條件不滿足的時候,會從ziplist轉換成linkedlist,同樣我們可以修改list-max-ziplist-valuehash-max-ziplist-entries來使用不同的閾值。

為什麼Hash與List會使用ziplist來儲存資料呢?

因為

  1. ziplist會比hashtable與ziplist節省跟多的記憶體
  2. 記憶體中以連續塊方式儲存的資料比起hashtable與linkedlist使用的連結串列可以更快的載入快取中
  3. 當ziplist的長度比較小的時候,從ziplist讀寫資料的效率比hashtable或者linkedlist的差異並不大。

本質上,使用ziplist就是以時間換空間的一種優化,但是他的時間損壞小到幾乎可以忽略不計,但卻能帶來可觀的記憶體減少,所以滿足條件時,Redis會使用ziplist作為Hash與List的儲存結構。

實戰

我們先丟擲問題,在廣告程式化交易的過程中,我們經常需要為一個廣告投放計劃定製人群包,其儲存的形式如下:

人群包ID => [裝置ID_1, 裝置ID_2 ... 裝置ID_N]

其中,人群包ID是Long型整數,裝置ID是經過MD5處理,長度為32。 在業務場景中,我們需要判斷一個裝置ID是否在一個人群包中,來決定是否投放廣告。

在傳統的使用Redis的場景, 我們可以使用標準的KV結構來儲存定向包資料,則儲存方式如下:

{人群包ID}_{裝置ID_1} => true
{人群包ID}_{裝置ID_2} => true

如果我們想使用ziplist來繼續記憶體壓縮的話,我們必須保證Hash物件的長度小於512,並且鍵值的長度小於64位元組。我們可以將KV結構的資料,儲存到預先分配好的bucket中。

我們先預估下,整個Redis叢集預計容納的資料條數為10億,那麼Bucket的數量的計算公式如下:

bucket_count = 10億 / 512 = 195W

那麼我們大概需要200W個Bucket(預估Bucket數量需要多預估一點,以防觸發臨界值問題) 我們先以下公式計算BucketID:

bucket_id = CRC32(人群包ID + "_" + 裝置ID) % 200W

那麼資料在Redis的儲存結構就變成

bucket_id => {
   {人群包ID}_{裝置ID_1} => true
   {人群包ID}_{裝置ID_2} => true
}

這樣我們保證每個bucket中的資料項都小於512,並且長度均小於64位元組。

我們以2000W資料進行測試,前後兩者的記憶體使用情況如下:

資料集大小 儲存模式 Bucket數量 所用記憶體 碎片率 Redis佔用的記憶體
2000W 壓縮列表 200W 928M 1.38 1.25G
2000W 壓縮列表 5W 785M 1.48 1.14G
2000W 直接儲存 - 1.44G 1.03 1.48G

在這裡需要額外引入一個概念 – 記憶體碎片率。

記憶體碎片率 = 作業系統給Redis分配的記憶體 / Redis儲存物件佔用的記憶體

因為壓縮列表在更新節點的時候,經常需要進行記憶體重分配,所以導致比較高的記憶體碎片率。我們在做技術方案比較的時候,記憶體碎片率也是非常需要關注的指標之一。

但有很多手段可以減少記憶體碎片率,比如記憶體對其,甚至更極端的直接重做整個Redis記憶體(利用快照或者從節點來重做記憶體)都能有效的減低記憶體碎片率。

我們在本次實驗中,因為儲存的數值比較大(單個KEY約34個位元組),所以實際節省記憶體不是很多,但依然能節約35%-50%的記憶體使用。

在實際的生產環境中,我們根據應用場景合理的設計壓縮儲存結構,部分業務甚至能達到節約70%的記憶體使用的效果。

壓縮列表能節省多少記憶體?

我們現在知道壓縮列表是通過將節點緊湊的排列在記憶體中,從而節省掉記憶體的。但他究竟節省了哪些記憶體從而能達到驚人的壓縮率呢?

首先為了明白這個細節,我們需要知道普通Key-Value結構在Redis中是如何儲存的。

typedef struct redisObject {
    unsigned type:4;        // 物件的型別
    unsigned encoding:4;    // 物件的編碼
    unsigned lru:LRU_BITS;  // LRU型別
    int refcount;           // 引用計數
    void *ptr;              // 指向底層資料結構的指標
} robj;

Redis所有的物件都是通過上述結構來儲存, 假設我儲存Hello=>World這樣一個健值對到Redis中,除了儲存本身鍵值的資料外,還需要額外的24個位元組來儲存redisObject物件。

而Redis儲存字串使用的SDS資料結構

struct sdshdr8 {
    uint8_t len;        // 所儲存字串的長度
    uint8_t alloc;      // 分配的記憶體數量
    unsigned char flags;// 標誌位,用於判斷sdshdr型別
    char buf[];         // 位元組陣列,使用者儲存字串
};

假如字串的長度無法用unsigned int8來表示的話,Redis會使用能表達更大長度的sdshdr16結構來儲存字串。

並且,為了減少修改字串帶來的記憶體重分類問題,Redis會進行記憶體預分配,所以可能你僅僅為了儲存五個字元,但Redis會為你預分配10 bytes的記憶體。

這意味著當我們儲存Hello這個字串的時候,你需要額外的3個以上的位元組。

Oh~~~,我只想儲存Hello=>World這十個字元的資料,竟然需要的30~40個位元組的資料來儲存額外的資訊,比儲存資料本身的大小還多一些。這還沒包括Redis維護字典表所需要的額外的記憶體空間。

那麼假設我們用ziplist來儲存這個資料,我們僅僅需要額外的2個位元組用於儲存previous_entry_length與encoding。具體的計算方式可以參考Redis原始碼或者《Redis設計與實現》第一部分第7章壓縮列表。

總結

從以上對比,我們可以看出,在儲存越小的資料的時候,使用ziplist來進行資料壓縮能得到更好的壓縮率。 但副作用也很明顯,ziplist的更新效率遠遠低於普通K-V模式,並且會造成額外的記憶體碎片率。

在Redis中儲存大量資料的實踐過程中,我們經常會做一些小技巧來儘可能壓榨Redis的儲存能力。接下來準備寫一篇Redis記憶體壓縮的小技巧。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2021最新版)

2.終於靠開源專案弄到 IntelliJ IDEA 啟用碼了,真香!

3.阿里 Mock 工具正式開源,幹掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式釋出,全新顛覆性版本!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!