Redis容量評估模型
計算Redis容量,并不只是僅僅計算key占多少字節(jié),value占多少字節(jié),因為Redis為了維護(hù)自身的數(shù)據(jù)結(jié)構(gòu),也會占用部分內(nèi)存,本文章簡單介紹每種數(shù)據(jù)類型(String、Hash、Set、ZSet、List)占用內(nèi)存量,供做Redis容量評估時使用。當(dāng)然,大多數(shù)情況下,key和value就是主要占用,能解大部分問題
在看這里之前,可以先看一下底層 - 數(shù)據(jù)結(jié)構(gòu) 這篇文章
jemalloc內(nèi)存分配規(guī)則
jemalloc是一種通用的內(nèi)存管理方法,著重于減少內(nèi)存碎片和支持可伸縮的并發(fā)性,做redis容量評估前必須對jemalloc的內(nèi)存分配規(guī)則有一定了解。
jemalloc基于申請內(nèi)存的大小把內(nèi)存分配分為三個等級:small,large,huge:
- Small Object 的size以8字節(jié),16字節(jié),32字節(jié)等分隔開,小于頁大小;
- Large Object 的size以分頁為單位,等差間隔排列,小于chunk的大小;
- Huge Object 的大小是chunk大小的整數(shù)倍。
對于64位系統(tǒng),一般chunk大小為4M,頁大小為4K,內(nèi)存分配的具體規(guī)則如下:

jemalloc在分配內(nèi)存塊時會分配大于實際值的2^n的值,例如實際值時6字節(jié),那么會分配8字節(jié)
| 數(shù)據(jù)類型 | 占用量 |
|---|---|
| dicEntry | 主要包括3個指針,key、value、哈希沖突時下個指針,耗費容量為8*3=24字節(jié),jemalloc會分配32字節(jié)的內(nèi)存塊 |
| dict結(jié)構(gòu) | 88字節(jié),jemalloc會分配 96 字節(jié)的內(nèi)存塊 |
| redisObject | type(4bit)、encoding(4bit)、lru(24bit)、int(8byte)、ptr指針(8byte)。因此redisObject結(jié)構(gòu)占用(4+4+24)/8 +4+8 = 16字節(jié)。 |
| key_SDS | key的長度 + 9,jemalloc分配 >= 該值的2^n的值 |
| val_SDS | value的長度 + 9,jemalloc分配 >= 該值的2^n的值 |
| key的個數(shù) | 所有的key的個數(shù) |
| bucket個數(shù) | 大于key的個數(shù)的2^n次方,例如key個數(shù)是2000,那么bucket=2048 |
| 指針大小 | 8 byte |
SDS中的主要包括兩個表示長度int占用大小為8字節(jié),redis中字符串還用“/0”表示結(jié)束占用1字節(jié),所以 sds占用大小為9字節(jié) + 數(shù)據(jù)長度
dict結(jié)構(gòu) 這里會分配96 字節(jié)的內(nèi)存塊?為什么不是128?
內(nèi)存劃分
Redis內(nèi)存占用主要可以劃分為如下幾個部分:
-
數(shù)據(jù):Redis數(shù)據(jù)占用內(nèi)存
dataset.bytes包括key-value占用內(nèi)存、dicEntry占用內(nèi)存、SDS占用內(nèi)存等。數(shù)據(jù)所占內(nèi)存 = 當(dāng)前所占總內(nèi)存
total.allocated- 額外內(nèi)存overhead.total -
初始化內(nèi)存:redis啟動初始化時使用的內(nèi)存
startup.allocated,屬于額外內(nèi)存overhead.total的一部分。 -
主從復(fù)制內(nèi)存:用于主從復(fù)制,屬于額外內(nèi)存一部分。
-
緩沖區(qū)內(nèi)存:緩沖內(nèi)存包括客戶端緩沖區(qū)、復(fù)制積壓緩沖區(qū)、AOF緩沖區(qū)等;其中,客戶端緩沖存儲客戶端連接的輸入輸出緩沖;復(fù)制積壓緩沖用于部分復(fù)制功能;AOF緩沖區(qū)用于在進(jìn)行AOF重寫時,保存最近的寫入命令。在了解相應(yīng)功能之前,不需要知道這些緩沖的細(xì)節(jié);這部分內(nèi)存由jemalloc分配,因此會統(tǒng)計在used_memory中。
-
內(nèi)存碎片:內(nèi)存碎片是Redis在分配、回收物理內(nèi)存過程中產(chǎn)生的。例如,如果對數(shù)據(jù)的更改頻繁,而且數(shù)據(jù)之間的大小相差很大,可能導(dǎo)致redis釋放的空間在物理內(nèi)存中并沒有釋放,但redis又無法有效利用,這就形成了內(nèi)存碎片。
內(nèi)存碎片率 = Redis進(jìn)程占用內(nèi)存 / 當(dāng)前所占內(nèi)存
total.allocated內(nèi)存碎片涉及到內(nèi)存碎片率
fragmentation,該值對于查看內(nèi)存是否夠用比較重要:- 該值一般>1,數(shù)值越大,說明內(nèi)存碎片越多。
- 如果<1,說明Redis占用了虛擬內(nèi)存,而虛擬內(nèi)存是基于磁盤的,速度會變慢,所以如果<1,就需要特別注意是否是內(nèi)存不足了。
- 一般來說,mem_fragmentation_ratio在1.03左右是比較健康的狀態(tài)(對于jemalloc來說);
redis數(shù)據(jù)內(nèi)存容量評估
redis容量評估模型根據(jù)key類型而有所不同。
string
一個簡單的set命令最終會產(chǎn)生4個消耗內(nèi)存的結(jié)構(gòu),中間free掉的不考慮:
- 1個dictEntry結(jié)構(gòu),24字節(jié),負(fù)責(zé)保存具體的鍵值對;
- 1個redisObject結(jié)構(gòu),16字節(jié),用作val對象;
- 1個SDS結(jié)構(gòu),(key長度 + 9)字節(jié),用作key字符串;
- 1個SDS結(jié)構(gòu),(val長度 + 9)字節(jié),用作val字符串;
當(dāng)key個數(shù)逐漸增多,redis還會以rehash的方式擴展哈希表節(jié)點數(shù)組,即增大哈希表的bucket個數(shù),每個bucket元素都是個指針(dictEntry*),占8字節(jié),bucket個數(shù)是超過key個數(shù)向上求整的2的n次方。
評估模型
真實情況下,每個結(jié)構(gòu)最終真正占用的內(nèi)存還要考慮jemalloc的內(nèi)存分配規(guī)則,綜上所述,string類型的容量評估模型為:
總內(nèi)存消耗 = (dictEntry大小 + redisObject大小 +key_SDS大小 + val_SDS大小)×key個數(shù) + bucket個數(shù) ×指針大小
即:
總內(nèi)存消耗 = (32 + 16 + key_SDS大小 + val_SDS大小)×key個數(shù) + bucket個數(shù) × 8
32是因為是24,但jemalloc會分配32字節(jié)的內(nèi)存塊
測試驗證
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個數(shù)為2000,根據(jù)上面總結(jié)的容量評估模型,容量預(yù)估值為2000 ×(32 + 16 + 32 + 32) + 2048× 8 = 240384
運行測試腳本,得到結(jié)果如下:

結(jié)果都是240384,說明模型預(yù)估的十分精確。
hash
哈希對象的底層實現(xiàn)數(shù)據(jù)結(jié)構(gòu)可能是listpack或者h(yuǎn)ashtable,當(dāng)同時滿足下面這兩個條件時,哈希對象使用listpack這種結(jié)構(gòu)(此處列出的條件都是redis默認(rèn)配置,可以更改):
- 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于64字節(jié);
- 哈希對象保存的鍵值對的數(shù)量都小于512個;
可以看出,業(yè)務(wù)側(cè)真實使用場景基本都不能滿足這兩個條件,所以哈希類型大部分都是hashtable結(jié)構(gòu),因此本篇文章只講hashtable。
與string類型不同的是,hash類型的值對象并不是指向一個SDS結(jié)構(gòu),而是指向又一個dict結(jié)構(gòu),dict結(jié)構(gòu)保存了哈希對象具體的鍵值對,hash類型結(jié)構(gòu)關(guān)系如圖所示:

一個hmset命令最終會產(chǎn)生以下幾個消耗內(nèi)存的結(jié)構(gòu):
- 1個dictEntry結(jié)構(gòu),24字節(jié),負(fù)責(zé)保存當(dāng)前的哈希對象;
- 1個SDS結(jié)構(gòu),(key長度 + 9)字節(jié),用作key字符串;
- 1個redisObject結(jié)構(gòu),16字節(jié),指向當(dāng)前key下屬的dict結(jié)構(gòu);
- 1個dict結(jié)構(gòu),88字節(jié),負(fù)責(zé)保存哈希對象的鍵值對;
- n個dictEntry結(jié)構(gòu),24×n 字節(jié),負(fù)責(zé)保存具體的field和value,n等于field個數(shù);
- n個redisObject結(jié)構(gòu),16×n 字節(jié),用作field對象;
- n個redisObject結(jié)構(gòu),16×n 字節(jié),用作value對象;
- n個SDS結(jié)構(gòu),(field長度 + 9)× n字節(jié),用作field字符串;
- n個SDS結(jié)構(gòu),(value長度 + 9)× n字節(jié),用作value字符串;
評估模型
因為hash類型內(nèi)部有兩個dict結(jié)構(gòu),所以最終會有產(chǎn)生兩種rehash,一種rehash基準(zhǔn)是field個數(shù),另一種rehash基準(zhǔn)是key個數(shù),結(jié)合jemalloc內(nèi)存分配規(guī)則,hash類型的容量評估模型為:
總內(nèi)存消耗 = [(redisObject大小 ×2 +field_SDS大小 + val_SDS大小 + dictEntry大小)× field個數(shù) + field_bucket個數(shù)× 指針大小 + dict大小 + redisObject大小 +key_SDS大小 + dictEntry大小 ] × key個數(shù) + key_bucket個數(shù) × 指針大小
即:
總內(nèi)存消耗 = [(16 ×2 +field_SDS大小 + val_SDS大小 + 32)× field個數(shù) + field_bucket個數(shù)× 8 + 96 + 16 +key_SDS大小 + 32 ] × key個數(shù) + key_bucket個數(shù) × 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個數(shù)為200,field個數(shù)為200,根據(jù)上面總結(jié)的容量評估模型,容量預(yù)估值為[(16 + 16 + 32 + 96 + 32)×200 + 256×8 + 96 + 16 + 32 + 32 ]× 200 + 256× 8 = 8126848
運行測試腳本,得到結(jié)果如下:

結(jié)果相差40,說明模型預(yù)測比較準(zhǔn)確。
zset
同哈希對象類似,有序集合對象的底層實現(xiàn)數(shù)據(jù)結(jié)構(gòu)也分兩種:listpack或者skiplist,當(dāng)同時滿足下面這兩個條件時,有序集合對象使用ziplist這種結(jié)構(gòu)(此處列出的條件都是redis默認(rèn)配置,可以更改):
- 有序集合對象保存的元素數(shù)量小于128個;
- 有序集合保存的所有元素成員的長度都小于64字節(jié);
業(yè)務(wù)側(cè)真實使用時基本都不能同時滿足這兩個條件,因此這里只講skiplist結(jié)構(gòu)的情況。skiplist類型的值對象指向一個zset結(jié)構(gòu),zset結(jié)構(gòu)同時包含一個字典和一個跳躍表,占用的總字節(jié)數(shù)為16,具體定義如下(redis.h/zset):
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
跳躍表按分值從小到大保存了所有集合元素,每個跳躍表節(jié)點都保存了一個集合元素,dict字典為有序集合創(chuàng)建了一個從成員到分值的映射,字典中的每個鍵值對都保存了一個集合元素,這兩種數(shù)據(jù)結(jié)構(gòu)會通過指針來共享相同元素的成員和分值,沒有浪費額外的內(nèi)存。zset類型的結(jié)構(gòu)關(guān)系如圖所示:

一個zadd命令最終會產(chǎn)生以下幾個消耗內(nèi)存的結(jié)構(gòu):
- 1個dictEntry結(jié)構(gòu),24字節(jié),負(fù)責(zé)保存當(dāng)前的有序集合對象;
- 1個SDS結(jié)構(gòu),(key長度 + 9)字節(jié),用作key字符串;
- 1個redisObject結(jié)構(gòu),16字節(jié),指向當(dāng)前key下屬的zset結(jié)構(gòu);
- 1個zset結(jié)構(gòu),16字節(jié),負(fù)責(zé)保存下屬的dict和zskiplist結(jié)構(gòu);
- 1個dict結(jié)構(gòu),88字節(jié),負(fù)責(zé)保存集合元素中成員到分值的映射;
- n個dictEntry結(jié)構(gòu),24×n字節(jié),負(fù)責(zé)保存具體的成員和分值,n等于集合成員個數(shù);
- 1個zskiplist結(jié)構(gòu),32字節(jié),負(fù)責(zé)保存跳躍表的相關(guān)信息;
- 1個32層的zskiplistNode結(jié)構(gòu),24+16×32=536字節(jié),用作跳躍表頭結(jié)點;
- n個zskiplistNode結(jié)構(gòu),(24+16×m)×n字節(jié),用作跳躍表節(jié)點,m等于節(jié)點層數(shù);
- n個redisObject結(jié)構(gòu),16×n字節(jié),用作集合中的成員對象;
- n個SDS結(jié)構(gòu),(value長度 + 9)×n字節(jié),用作成員字符串;
因為每個zskiplistNode節(jié)點的層數(shù)都是根據(jù)冪次定律隨機生成的,而容量評估需要確切值,因此這里采用概率中的期望值來代替單個節(jié)點的大小,結(jié)合jemalloc內(nèi)存分配規(guī)則,經(jīng)計算,單個zskiplistNode節(jié)點大小的期望值為53.336。
評估模型
zset類型內(nèi)部同樣包含兩個dict結(jié)構(gòu),所以最終會有產(chǎn)生兩種rehash,一種rehash基準(zhǔn)是成員個數(shù),另一種rehash基準(zhǔn)是key個數(shù),zset類型的容量評估模型為:
總內(nèi)存消耗 = [(val_SDS大小 + redisObject大小 + zskiplistNode大小 + dictEntry大小)×value個數(shù) +value_bucket個數(shù) ×指針大小 + 32層zskiplistNode大小 + zskiplist大小 + dict大小 + zset大小 + redisObject大小 + key_SDS大小 + dictEntry大小 ] ×key個數(shù) +key_bucket個數(shù) × 指針大小
即:
總內(nèi)存消耗 = [(val_SDS大小 + 16 + 53.336 + 32)×value個數(shù) +value_bucket個數(shù) × 8 + 640 +32 + 96 + 16 + 16 + key_SDS大小 + 32 ] ×key個數(shù) +key_bucket個數(shù) × 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個數(shù)為200,value個數(shù)為200,根據(jù)上面總結(jié)的容量評估模型,容量預(yù)估值為[(96 + 16 + 53.336 + 32)×200 + 256×8 + 640 + 32 + 96 + 16 + 16 + 32 + 32 ] ×200 + 256 × 8 = 8477888
運行測試腳本,得到結(jié)果如下:

結(jié)果相差672,說明模型預(yù)測比較準(zhǔn)確。
list
列表對象的底層實現(xiàn)數(shù)據(jù)結(jié)構(gòu)同樣分兩種:listpack或者linkedlist,當(dāng)同時滿足下面這兩個條件時,列表對象使用listpack這種結(jié)構(gòu)(此處列出的條件都是redis默認(rèn)配置,可以更改):
- 列表對象保存的所有字符串元素的長度都小于64字節(jié);
- 列表對象保存的元素數(shù)量小于512個;
因為實際使用情況,這里同樣只講linkedlist結(jié)構(gòu)。linkedlist類型的值對象指向一個list結(jié)構(gòu),具體結(jié)構(gòu)關(guān)系如圖所示:

一個rpush或者lpush命令最終會產(chǎn)生以下幾個消耗內(nèi)存的結(jié)構(gòu):
- 1個dictEntry結(jié)構(gòu),24字節(jié),負(fù)責(zé)保存當(dāng)前的列表對象;
- 1個SDS結(jié)構(gòu),(key長度 + 9)字節(jié),用作key字符串;
- 1個redisObject結(jié)構(gòu),16字節(jié),指向當(dāng)前key下屬的list結(jié)構(gòu);
- 1個list結(jié)構(gòu),48字節(jié),負(fù)責(zé)管理鏈表節(jié)點;
- n個listNode結(jié)構(gòu),24×n字節(jié),n等于value個數(shù);
- n個redisObject結(jié)構(gòu),16×n字節(jié),用作鏈表中的值對象;
- n個SDS結(jié)構(gòu),(value長度 + 9)×n字節(jié),用作值對象指向的字符串;
評估模型
list類型內(nèi)部只有一個dict結(jié)構(gòu),rehash基準(zhǔn)為key個數(shù),綜上,list類型的容量評估模型為:
總內(nèi)存消耗 = [(val_SDS大小 + redisObject大小 + listNode大小)× value個數(shù) + list大小 + redisObject大小 + key_SDS大小 + dictEntry大小 ] × key個數(shù) + key_bucket個數(shù) × 指針大小
即:
總內(nèi)存消耗 = [(val_SDS大小 +16 + 32)× value個數(shù) + 16 + 32 + key_SDS大小 + 32 ] × key個數(shù) + key_bucket個數(shù) × 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個數(shù)為200,value個數(shù)為200,根據(jù)上面總結(jié)的容量評估模型,容量預(yù)估值為[(96 + 16 + 32) ×200 + 48 + 16 + 32 + 32 ] × 200 + 256 ×8 = 5787648
運行測試腳本,得到結(jié)果如下:

結(jié)果都是5787648,說明模型預(yù)估的十分精確。
Set
一個sadd命令最終會產(chǎn)生以下幾個消耗內(nèi)存的結(jié)構(gòu):
- 1個dictEntry結(jié)構(gòu),24字節(jié),負(fù)責(zé)保存當(dāng)前的set對象;
- 1個SDS結(jié)構(gòu),(key長度 + 9)字節(jié),用作key字符串;
- 1個redisObject結(jié)構(gòu),16字節(jié),指向當(dāng)前key下屬的dict結(jié)構(gòu);
- 1個dict結(jié)構(gòu),88字節(jié),負(fù)責(zé)保存哈希對象的鍵值對;
- n個dictEntry結(jié)構(gòu),24×n 字節(jié),負(fù)責(zé)保存具體的member,n等于member個數(shù);
- n個redisObject結(jié)構(gòu),16×n 字節(jié),用作member對象;
- n個SDS結(jié)構(gòu),(field長度 + 9)× n字節(jié),用作member字符串;
評估模型
set與hash類似,只是value部分沒有具體的值。與hash類型一樣,內(nèi)部有兩個dict結(jié)構(gòu),所以最終會有產(chǎn)生兩種rehash,一種rehash基準(zhǔn)是member個數(shù),另一種rehash基準(zhǔn)是key個數(shù),結(jié)合jemalloc內(nèi)存分配規(guī)則,hash類型的容量評估模型為:
總內(nèi)存消耗 = [(redisObject大小 +member_SDS大小 + dictEntry大小)× member個數(shù) + member_bucket個數(shù)× 指針大小 + dict大小 + redisObject大小 +key_SDS大小 + dictEntry大小 ] × key個數(shù) + key_bucket個數(shù)×指針大小
即:
總內(nèi)存消耗 = [(16 +member_SDS大小 + 32)× member個數(shù) + member_bucket個數(shù)× 8 + 96 + 16 +key_SDS大小 + 32 ] × key個數(shù) + key_bucket個數(shù)×8
本文來自在線網(wǎng)站:seven的菜鳥成長之路,作者:seven,轉(zhuǎn)載請注明原文鏈接:www.seven97.top

浙公網(wǎng)安備 33010602011771號