1. 程式人生 > >Redis記憶體分析

Redis記憶體分析

在瞭解 Redis 的 5 種物件型別的用法和特點的基礎上,進一步瞭解 Redis 的記憶體模型,對 Redis 的使用有很大幫助。比如估算Redis記憶體使用量,記憶體優化佔用,阻塞問題處理。

一、Redis記憶體統計

Redis提供記憶體統計命令,在客戶端通過 redis-cli 連線伺服器後,通過 info 命令可以檢視記憶體使用情況:info memory。
這裡寫圖片描述

其中info命令可以顯示很多Redis伺服器資訊,包括伺服器基本資訊、CPU、記憶體、持久化、客戶端連線資訊等等;memory 是引數,表示只顯示記憶體相關的資訊。
其中幾個重要資訊:

  • used_memory
    :表示Redis分配器分配的記憶體總量,單位是位元組,包括使用的虛擬記憶體(swap)
  • used_memory_rss:表示Redis程序佔用系統的記憶體,單位是位元組。除了分配器分配的記憶體之外,used_memory_rss 還包括程序執行本身需要的記憶體、記憶體碎片等,但是不包括虛擬記憶體
  • mem_fragmentation_ratio:表示記憶體碎片比率,該值是 used_memory_rss / used_memory 的比值。mem_fragmentation_ratio 一般大於 1,且該值越大,記憶體碎片比例越大;mem_fragmentation_ratio<1,說明 Redis 使用了虛擬記憶體,由於虛擬記憶體的媒介是磁碟,比記憶體速度要慢很多。一般來說,mem_fragmentation_ratio 在 1.03 左右是比較健康的狀態(對於 jemalloc 來說)。
  • mem_allocator:Redis 使用的記憶體分配器,在編譯時指定;可以是 libc 、jemalloc 或者 tcmalloc,預設是 jemalloc。

二、Redis記憶體劃分

除了資料之外,Redis本身也會佔用記憶體,Redis 的記憶體佔用主要可以劃分為以下幾個部分:
1.資料
作為記憶體資料庫,資料是最主要的部分;這部分佔用的記憶體會統計在 used_memory 中。
Redis 使用鍵值對儲存資料,其中的值(物件)包括 5 種類型,即字串、雜湊、列表、集合、有序集合。
這 5 種類型是 Redis 對外提供的,實際上,在 Redis 內部,每種型別可能有 2 種或更多的內部編碼實現。
Redis 在儲存物件時,並不是直接將資料扔進記憶體,而是會對物件進行各種包裝:如 RedisObject、SDS 等。
2.Redis程序執行佔用的記憶體


Redis 主程序本身執行肯定需要佔用記憶體,如程式碼、常量池等等;這部分記憶體大約幾兆,在大多數生產環境中與 Redis 資料佔用的記憶體相比可以忽略。
除了主程序外,Redis 建立的子程序執行也會佔用記憶體,如 Redis 執行 AOF、RDB 重寫時建立的子程序。這部分記憶體不屬於 Redis 程序,也不會統計在 used_memory 和 used_memory_rss 中。
3.緩衝記憶體
緩衝記憶體包括客戶端緩衝區、複製積壓緩衝區、AOF 緩衝區等;其中,客戶端緩衝區儲存客戶端連線的輸入輸出緩衝;複製積壓緩衝區用於部分複製功能;AOF 緩衝區用於在進行 AOF 重寫時,儲存最近的寫入命令。
這部分記憶體由jemalloc分配,因此會統計在 used_memory 中。
4.記憶體碎片
記憶體碎片是 Redis 在分配、回收物理記憶體過程中產生的。記憶體碎片不會統計在 used_memory 中。
如果 Redis 伺服器中的記憶體碎片已經很大,可以通過安全重啟的方式減小記憶體碎片:因為重啟之後,Redis 重新從備份檔案中讀取資料,在記憶體中進行重排,為每個資料重新選擇合適的記憶體單元,減小記憶體碎片。

Redis資料儲存的細節

關於 Redis 資料儲存的細節,涉及到記憶體分配器(如 jemalloc)、簡單動態字串(SDS)、RedisObject、5 種物件型別及內部編碼。
下圖是執行命令 set hello world 時,所涉及到的資料模型:
這裡寫圖片描述
dictEntry:Redis 是 Key-Value 資料庫,因此對每個鍵值對都會有一個 dictEntry,裡面儲存了指向 Key 和 Value 的指標;next 指向下一個 dictEntry,與本 Key-Value 無關。
Key:Key(”hello”)並不是直接以字串儲存,而是儲存在 SDS 結構中。
RedisObject:Value(“world”)既不是直接以字串儲存,也不是像 Key 一樣直接儲存在 SDS 中,而是儲存在 RedisObject 中。實際上,不論 Value 是 5 種類型的哪一種,都是通過 RedisObject 來儲存的;而 RedisObject 中的 type 欄位指明瞭 Value 物件的型別,ptr 欄位則指向物件所在的地址。
jemalloc:預設的記憶體分配器。無論是 DictEntry 物件,還是 RedisObject、SDS 物件,都需要記憶體分配器(如 jemalloc)分配記憶體進行儲存。

下面來分別介紹 jemalloc、RedisObject、SDS、物件型別及內部編碼:
1.jemalloc
Redis 在編譯時便會指定記憶體分配器;記憶體分配器可以是 libc 、jemalloc 或者 tcmalloc,預設是 jemalloc。
jemalloc 的優勢體現在減小記憶體碎片方面。jemalloc 在 64 位系統中,將記憶體空間劃分為小、大、巨大三個範圍;每個範圍內又劃分了許多小的記憶體塊單位;當 Redis 儲存資料時,會選擇大小最合適的記憶體塊進行儲存。
jemalloc 劃分的記憶體單元如下圖所示:
這裡寫圖片描述
2.RedisObject
RedisObject 物件非常重要,Redis 物件的型別、內部編碼、記憶體回收、共享物件等功能,都需要 RedisObject 支援,由上一節知,redisObject包含5個欄位:
這裡寫圖片描述

  • type
    type 欄位表示物件的型別,佔 4 個位元;目前包括 REDIS_STRING(字串)、REDIS_LIST (列表)、REDIS_HASH(雜湊)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
    當我們執行 type 命令時,便是通過讀取 RedisObject 的 type 欄位獲得物件的型別;如下圖所示:
    這裡寫圖片描述

  • encoding
    encoding 表示物件的內部編碼,佔 4 個位元。對於 Redis 支援的每種型別,都有至少兩種內部編碼,例如對於字串,有 int、embstr、raw 三種編碼。
    通過 encoding 屬性,Redis 可以根據不同的使用場景來為物件設定不同的編碼,大大提高了 Redis 的靈活性和效率。
    通過 object encoding 命令,可以檢視物件採用的編碼方式,如下圖所示:
    這裡寫圖片描述

  • lru
    lru 記錄的是物件最後一次被命令程式訪問的時間,佔據的位元數不同的版本有所不同(如 4.0 版本佔 24 位元,2.6 版本佔 22 位元)。
    通過對比 lru 時間與當前時間,可以計算某個物件的空轉時間;object idletime 命令可以顯示該空轉時間(單位是秒)。object idletime 命令的一個特殊之處在於它不改變物件的 lru 值。
    這裡寫圖片描述
    lru 值還與 Redis 的記憶體回收有關係。如果 Redis 打開了 maxmemory 選項,且記憶體回收演算法選擇的是 volatile-lru 或 allkeys—lru,那麼當 Redis 記憶體佔用超過 maxmemory 指定的值時,Redis 會優先選擇空轉時間最長的物件進行釋放。
  • refcount
    refcount 記錄的是該物件被引用的次數,型別為整型。refcount 的作用,主要在於物件的引用計數和記憶體回收。
    當建立新物件時,refcount 初始化為 1;當有新程式使用該物件時,refcount 加 1;當物件不再被一個新程式使用時,refcount 減 1;當 refcount 變為 0 時,物件佔用的記憶體會被釋放。
    Redis 中被多次使用的物件(refcount>1),稱為共享物件。Redis 為了節省記憶體,當有一些物件重複出現時,新的程式不會建立新的物件,而是仍然使用原來的物件。
    這個被重複使用的物件,就是共享物件。目前共享物件僅支援整數值的字串物件。
    共享物件的具體實現:Redis 的共享物件目前只支援整數值的字串物件。之所以如此,實際上是對記憶體和 CPU(時間)的平衡:共享物件雖然會降低記憶體消耗,但是判斷兩個物件是否相等卻需要消耗額外的時間。
    對於整數值,判斷操作複雜度為 O(1);對於普通字串,判斷複雜度為 O(n);而對於雜湊、列表、集合和有序集合,判斷的複雜度為 O(n^2)。
    雖然共享物件只能是整數值的字串物件,但是5種類型都可能使用共享物件(如雜湊、列表等的元素可以使用)。
    就目前的實現來說,Redis 伺服器在初始化時,會建立 10000 個字串物件,值分別是 0~9999 的整數值;當 Redis 需要使用值為 0~9999 的字串物件時,可以直接使用這些共享物件。10000 這個數字可以通過調整引數 REDIS_SHARED_INTEGERS(4.0 中是 OBJ_SHARED_INTEGERS)的值進行改變。
    共享物件的引用次數可以通過 object refcount 命令檢視,如下圖所示。命令執行的結果頁佐證了只有 0~9999 之間的整數會作為共享物件。
    這裡寫圖片描述
  • ptr
    ptr 指標指向具體的資料,如前面的例子中,set hello world,ptr 指向包含字串 world 的 SDS。

    綜上所述,RedisObject 的結構與物件型別、編碼、記憶體回收、共享物件都有關係。
    一個 RedisObject 物件的大小為 16 位元組:4bit+4bit+24bit+4Byte+8Byte=16Byte。

3.SDS
Redis 沒有直接使用 C 字串(即以空字元’\0’結尾的字元陣列)作為預設的字串表示,而是使用了 SDS。SDS 是簡單動態字串(Simple Dynamic String)的縮寫。
SDS 的結構如下:
這裡寫圖片描述
其中,buf 表示位元組陣列,用來儲存字串;len 表示 buf 已使用的長度;free 表示 buf 未使用的長度。
這裡寫圖片描述
通過 SDS 的結構可以看出,buf 陣列的長度=free+len+1(其中 1 表示字串結尾的空字元)。
SDS 在 C 字串的基礎上加入了 free 和 len 欄位,帶來了很多好處:

  • 獲取字串長度:SDS 是 O(1),C 字串是 O(n)。
  • 緩衝區溢位:SDS 由於記錄了長度,相應的 API 在可能造成緩衝區溢位時會自動重新分配記憶體,杜絕了緩衝區溢位。
  • 修改字串時記憶體的重分配:對於 C 字串,如果要修改字串,必須要重新分配記憶體(先釋放再申請),因為如果沒有重新分配,字串長度增大時會造成記憶體緩衝區溢位,字串長度減小時會造成記憶體洩露。
    而對於 SDS,由於可以記錄 len 和 free,因此解除了字串長度和空間陣列長度之間的關聯,可以在此基礎上進行優化。
    空間預分配策略(即分配記憶體時比實際需要的多)使得字串長度增大時重新分配記憶體的概率大大減小;惰性空間釋放策略使得字串長度減小時重新分配記憶體的概率大大減小。
  • 存取二進位制資料:SDS 可以,C 字串不可以。因為 C 字串以空字元作為字串結束的標識,而對於一些二進位制檔案(如圖片等)。
    內容可能包括空字串,因此 C 字串無法正確存取;而 SDS 以字串長度 len 來作為字串結束標識,因此沒有這個問題。

在Redis物件儲存中,一律使用SDS代替c字串。除了儲存物件,SDS 還用於儲存各種緩衝區。只有在字串不會改變的情況下,如列印日誌時,才會使用 C 字串。