動態

詳情 返回 返回

Redis容量評估模型 - 動態 詳情

計算Redis容量,並不只是僅僅計算key佔多少字節,value佔多少字節,因為Redis為了維護自身的數據結構,也會佔用部分內存,本文章簡單介紹每種數據類型(StringHashSetZSetList)佔用內存量,供做Redis容量評估時使用。當然,大多數情況下,key和value就是主要佔用,能解大部分問題

在看這裏之前,可以先看一下底層 - 數據結構 這篇文章

jemalloc內存分配規則

jemalloc是一種通用的內存管理方法,着重於減少內存碎片和支持可伸縮的併發性,做redis容量評估前必須對jemalloc的內存分配規則有一定了解。

jemalloc基於申請內存的大小把內存分配分為三個等級:small,large,huge:

  • Small Object 的size以8字節,16字節,32字節等分隔開,小於頁大小;
  • Large Object 的size以分頁為單位,等差間隔排列,小於chunk的大小;
  • Huge Object 的大小是chunk大小的整數倍。

對於64位系統,一般chunk大小為4M,頁大小為4K,內存分配的具體規則如下:

jemalloc在分配內存塊時會分配大於實際值的2^n的值,例如實際值時6字節,那麼會分配8字節

數據類型 佔用量
dicEntry 主要包括3個指針,key、value、哈希衝突時下個指針,耗費容量為8*3=24字節,jemalloc會分配32字節的內存塊
dict結構 88字節,jemalloc會分配 96 字節的內存塊
redisObject type(4bit)、encoding(4bit)、lru(24bit)、int(8byte)、ptr指針(8byte)。因此redisObject結構佔用(4+4+24)/8 +4+8 = 16字節。
key_SDS key的長度 + 9,jemalloc分配 >= 該值的2^n的值
val_SDS value的長度 + 9,jemalloc分配 >= 該值的2^n的值
key的個數 所有的key的個數
bucket個數 大於key的個數的2^n次方,例如key個數是2000,那麼bucket=2048
指針大小 8 byte
SDS中的主要包括兩個表示長度int佔用大小為8字節,redis中字符串還用“/0”表示結束佔用1字節,所以 sds佔用大小為9字節 + 數據長度
dict結構 這裏會分配96 字節的內存塊?為什麼不是128?

內存劃分

Redis內存佔用主要可以劃分為如下幾個部分:

  • 數據:Redis數據佔用內存dataset.bytes包括key-value佔用內存、dicEntry佔用內存、SDS佔用內存等。

    數據所佔內存 = 當前所佔總內存total.allocated - 額外內存overhead.total
  • 初始化內存:redis啓動初始化時使用的內存startup.allocated,屬於額外內存overhead.total的一部分。
  • 主從複製內存:用於主從複製,屬於額外內存一部分。
  • 緩衝區內存:緩衝內存包括客户端緩衝區、複製積壓緩衝區、AOF緩衝區等;其中,客户端緩衝存儲客户端連接的輸入輸出緩衝;複製積壓緩衝用於部分複製功能;AOF緩衝區用於在進行AOF重寫時,保存最近的寫入命令。在瞭解相應功能之前,不需要知道這些緩衝的細節;這部分內存由jemalloc分配,因此會統計在used_memory中。
  • 內存碎片:內存碎片是Redis在分配、回收物理內存過程中產生的。例如,如果對數據的更改頻繁,而且數據之間的大小相差很大,可能導致redis釋放的空間在物理內存中並沒有釋放,但redis又無法有效利用,這就形成了內存碎片。

    內存碎片率 = Redis進程佔用內存 / 當前所佔內存total.allocated

    內存碎片涉及到內存碎片率fragmentation,該值對於查看內存是否夠用比較重要:

    • 該值一般>1,數值越大,説明內存碎片越多。
    • 如果<1,説明Redis佔用了虛擬內存,而虛擬內存是基於磁盤的,速度會變慢,所以如果<1,就需要特別注意是否是內存不足了。
    • 一般來説,mem_fragmentation_ratio在1.03左右是比較健康的狀態(對於jemalloc來説);

redis數據內存容量評估

redis容量評估模型根據key類型而有所不同。

string

一個簡單的set命令最終會產生4個消耗內存的結構,中間free掉的不考慮:

  • 1個dictEntry結構,24字節,負責保存具體的鍵值對;
  • 1個redisObject結構,16字節,用作val對象;
  • 1個SDS結構,(key長度 + 9)字節,用作key字符串;
  • 1個SDS結構,(val長度 + 9)字節,用作val字符串;

當key個數逐漸增多,redis還會以rehash的方式擴展哈希表節點數組,即增大哈希表的bucket個數,每個bucket元素都是個指針(dictEntry*),佔8字節,bucket個數是超過key個數向上求整的2的n次方。

評估模型

真實情況下,每個結構最終真正佔用的內存還要考慮jemalloc的內存分配規則,綜上所述,string類型的容量評估模型為:

總內存消耗 = (dictEntry大小 + redisObject大小 +key_SDS大小 + val_SDS大小)×key個數 + bucket個數 ×指針大小

即:

總內存消耗 = (32 + 16 + key_SDS大小 + val_SDS大小)×key個數 + bucket個數 × 8

32是因為是24,但jemalloc會分配32字節的內存塊

測試驗證

string類型容量評估測試腳本如下:

#!/bin/sh

old_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "before test, memory used: $old_memory"

for((i=1000; i<3000; i++))
do
    ./redis-cli -h 0 -p 10009 set test_key_$i test_value_$i > /dev/null
    sleep 0.2
done

new_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "after test, memory used: $new_memory"

let difference=new_memory-old_memory
echo "difference is: $difference"

測試用例中,key長度為 13,value長度為15,key個數為2000,根據上面總結的容量評估模型,容量預估值為2000 ×(32 + 16 + 32 + 32) + 2048× 8 = 240384

運行測試腳本,得到結果如下:

img

結果都是240384,説明模型預估的十分精確。

hash

哈希對象的底層實現數據結構可能是listpack或者hashtable,當同時滿足下面這兩個條件時,哈希對象使用listpack這種結構(此處列出的條件都是redis默認配置,可以更改):

  • 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小於64字節;
  • 哈希對象保存的鍵值對的數量都小於512個;

可以看出,業務側真實使用場景基本都不能滿足這兩個條件,所以哈希類型大部分都是hashtable結構,因此本篇文章只講hashtable。

與string類型不同的是,hash類型的值對象並不是指向一個SDS結構,而是指向又一個dict結構,dict結構保存了哈希對象具體的鍵值對,hash類型結構關係如圖所示:

一個hmset命令最終會產生以下幾個消耗內存的結構:

  • 1個dictEntry結構,24字節,負責保存當前的哈希對象;
  • 1個SDS結構,(key長度 + 9)字節,用作key字符串;
  • 1個redisObject結構,16字節,指向當前key下屬的dict結構;
  • 1個dict結構,88字節,負責保存哈希對象的鍵值對;
  • n個dictEntry結構,24×n 字節,負責保存具體的field和value,n等於field個數;
  • n個redisObject結構,16×n 字節,用作field對象;
  • n個redisObject結構,16×n 字節,用作value對象;
  • n個SDS結構,(field長度 + 9)× n字節,用作field字符串;
  • n個SDS結構,(value長度 + 9)× n字節,用作value字符串;

評估模型

因為hash類型內部有兩個dict結構,所以最終會有產生兩種rehash,一種rehash基準是field個數,另一種rehash基準是key個數,結合jemalloc內存分配規則,hash類型的容量評估模型為:

總內存消耗 = [(redisObject大小 ×2 +field_SDS大小 + val_SDS大小 + dictEntry大小)× field個數 + field_bucket個數× 指針大小 + dict大小 + redisObject大小 +key_SDS大小 + dictEntry大小 ] × key個數 + key_bucket個數 × 指針大小

即:

總內存消耗 = [(16 ×2 +field_SDS大小 + val_SDS大小 + 32)× field個數 + field_bucket個數× 8 + 96 + 16 +key_SDS大小 + 32 ] × key個數 + key_bucket個數 × 8

測試驗證

hash類型容量評估測試腳本如下:

#!/bin/sh

value_prefix="test_value_123456789012345678901234567890123456789012345678901234567890_"

old_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "before test, memory used: $old_memory"

for((i=100; i<300; i++))
do
    for((j=100; j<300; j++))
    do
        ./redis-cli -h 0 -p 10009 hset test_key_$i test_field_$j $value_prefix$j > /dev/null
    done
    sleep 0.5
done

new_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "after test, memory used: $new_memory"

let difference=new_memory-old_memory
echo "difference is: $difference"

測試用例中,key長度為 12,field長度為14,value長度為75,key個數為200,field個數為200,根據上面總結的容量評估模型,容量預估值為[(16 + 16 + 32 + 96 + 32)×200 + 256×8 + 96 + 16 + 32 + 32 ]× 200 + 256× 8 = 8126848

運行測試腳本,得到結果如下:

結果相差40,説明模型預測比較準確。

zset

同哈希對象類似,有序集合對象的底層實現數據結構也分兩種:listpack或者skiplist,當同時滿足下面這兩個條件時,有序集合對象使用ziplist這種結構(此處列出的條件都是redis默認配置,可以更改):

  • 有序集合對象保存的元素數量小於128個;
  • 有序集合保存的所有元素成員的長度都小於64字節;

業務側真實使用時基本都不能同時滿足這兩個條件,因此這裏只講skiplist結構的情況。skiplist類型的值對象指向一個zset結構,zset結構同時包含一個字典和一個跳躍表,佔用的總字節數為16,具體定義如下(redis.h/zset):

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

跳躍表按分值從小到大保存了所有集合元素,每個跳躍表節點都保存了一個集合元素,dict字典為有序集合創建了一個從成員到分值的映射,字典中的每個鍵值對都保存了一個集合元素,這兩種數據結構會通過指針來共享相同元素的成員和分值,沒有浪費額外的內存。zset類型的結構關係如圖所示:

一個zadd命令最終會產生以下幾個消耗內存的結構:

  • 1個dictEntry結構,24字節,負責保存當前的有序集合對象;
  • 1個SDS結構,(key長度 + 9)字節,用作key字符串;
  • 1個redisObject結構,16字節,指向當前key下屬的zset結構;
  • 1個zset結構,16字節,負責保存下屬的dict和zskiplist結構;
  • 1個dict結構,88字節,負責保存集合元素中成員到分值的映射;
  • n個dictEntry結構,24×n字節,負責保存具體的成員和分值,n等於集合成員個數;
  • 1個zskiplist結構,32字節,負責保存跳躍表的相關信息;
  • 1個32層的zskiplistNode結構,24+16×32=536字節,用作跳躍表頭結點;
  • n個zskiplistNode結構,(24+16×m)×n字節,用作跳躍表節點,m等於節點層數;
  • n個redisObject結構,16×n字節,用作集合中的成員對象;
  • n個SDS結構,(value長度 + 9)×n字節,用作成員字符串;

因為每個zskiplistNode節點的層數都是根據冪次定律隨機生成的,而容量評估需要確切值,因此這裏採用概率中的期望值來代替單個節點的大小,結合jemalloc內存分配規則,經計算,單個zskiplistNode節點大小的期望值為53.336。

評估模型

zset類型內部同樣包含兩個dict結構,所以最終會有產生兩種rehash,一種rehash基準是成員個數,另一種rehash基準是key個數,zset類型的容量評估模型為:

總內存消耗 = [(val_SDS大小 + redisObject大小 + zskiplistNode大小 + dictEntry大小)×value個數 +value_bucket個數 ×指針大小 + 32層zskiplistNode大小 + zskiplist大小 + dict大小 + zset大小 + redisObject大小 + key_SDS大小 + dictEntry大小 ] ×key個數 +key_bucket個數 × 指針大小

即:

總內存消耗 = [(val_SDS大小 + 16 + 53.336 + 32)×value個數 +value_bucket個數 × 8 + 640 +32 + 96 + 16 + 16 + key_SDS大小 + 32 ] ×key個數 +key_bucket個數 × 8

測試驗證

zset類型容量評估測試腳本如下:

#!/bin/sh

value_prefix="test_value_123456789012345678901234567890123456789012345678901234567890_"

old_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "before test, memory used: $old_memory"

for((i=100; i<300; i++))
do
    for((j=100; j<300; j++))
    do
        ./redis-cli -h 0 -p 10009 zadd test_key_$i $j $value_prefix$j > /dev/null
    done
    sleep 0.5
done

new_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "after test, memory used: $new_memory"

let difference=new_memory-old_memory
echo "difference is: $difference"

測試用例中,key長度為 12,value長度為75,key個數為200,value個數為200,根據上面總結的容量評估模型,容量預估值為[(96 + 16 + 53.336 + 32)×200 + 256×8 + 640 + 32 + 96 + 16 + 16 + 32 + 32 ] ×200 + 256 × 8 = 8477888

運行測試腳本,得到結果如下:

結果相差672,説明模型預測比較準確。

list

列表對象的底層實現數據結構同樣分兩種:listpack或者linkedlist,當同時滿足下面這兩個條件時,列表對象使用listpack這種結構(此處列出的條件都是redis默認配置,可以更改):

  • 列表對象保存的所有字符串元素的長度都小於64字節;
  • 列表對象保存的元素數量小於512個;

因為實際使用情況,這裏同樣只講linkedlist結構。linkedlist類型的值對象指向一個list結構,具體結構關係如圖所示:

一個rpush或者lpush命令最終會產生以下幾個消耗內存的結構:

  • 1個dictEntry結構,24字節,負責保存當前的列表對象;
  • 1個SDS結構,(key長度 + 9)字節,用作key字符串;
  • 1個redisObject結構,16字節,指向當前key下屬的list結構;
  • 1個list結構,48字節,負責管理鏈表節點;
  • n個listNode結構,24×n字節,n等於value個數;
  • n個redisObject結構,16×n字節,用作鏈表中的值對象;
  • n個SDS結構,(value長度 + 9)×n字節,用作值對象指向的字符串;

評估模型

list類型內部只有一個dict結構,rehash基準為key個數,綜上,list類型的容量評估模型為:

總內存消耗 = [(val_SDS大小 + redisObject大小 + listNode大小)× value個數 + list大小 + redisObject大小 + key_SDS大小 + dictEntry大小 ] × key個數 + key_bucket個數 × 指針大小

即:

總內存消耗 = [(val_SDS大小 +16 + 32)× value個數 + 16 + 32 + key_SDS大小 + 32 ] × key個數 + key_bucket個數 × 8

測試驗證

list類型容量評估測試腳本如下:

#!/bin/sh

value_prefix="test_value_123456789012345678901234567890123456789012345678901234567890_"

old_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "before test, memory used: $old_memory"

for((i=100; i<300; i++))
do
    for((j=100; j<300; j++))
    do
        ./redis-cli -h 0 -p 10009 rpush test_key_$i $value_prefix$j > /dev/null
    done
    sleep 0.5
done

new_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "after test, memory used: $new_memory"

let difference=new_memory-old_memory
echo "difference is: $difference"

測試用例中,key長度為 12,value長度為75,key個數為200,value個數為200,根據上面總結的容量評估模型,容量預估值為[(96 + 16 + 32) ×200 + 48 + 16 + 32 + 32 ] × 200 + 256 ×8 = 5787648

運行測試腳本,得到結果如下:

結果都是5787648,説明模型預估的十分精確。

Set

一個sadd命令最終會產生以下幾個消耗內存的結構:

  • 1個dictEntry結構,24字節,負責保存當前的set對象;
  • 1個SDS結構,(key長度 + 9)字節,用作key字符串;
  • 1個redisObject結構,16字節,指向當前key下屬的dict結構;
  • 1個dict結構,88字節,負責保存哈希對象的鍵值對;
  • n個dictEntry結構,24×n 字節,負責保存具體的member,n等於member個數;
  • n個redisObject結構,16×n 字節,用作member對象;
  • n個SDS結構,(field長度 + 9)× n字節,用作member字符串;

評估模型

set與hash類似,只是value部分沒有具體的值。與hash類型一樣,內部有兩個dict結構,所以最終會有產生兩種rehash,一種rehash基準是member個數,另一種rehash基準是key個數,結合jemalloc內存分配規則,hash類型的容量評估模型為:

總內存消耗 = [(redisObject大小 +member_SDS大小 + dictEntry大小)× member個數 + member_bucket個數× 指針大小 + dict大小 + redisObject大小 +key_SDS大小 + dictEntry大小 ] × key個數 + key_bucket個數×指針大小

即:

總內存消耗 = [(16 +member_SDS大小 + 32)× member個數 + member_bucket個數× 8 + 96 + 16 +key_SDS大小 + 32 ] × key個數 + key_bucket個數×8

user avatar fecify 頭像 jidcoo 頭像
點贊 2 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.