本頁面是進程內(nèi)的一個工作。目前它僅是一個當你的內(nèi)存出現(xiàn)問題時的檢查清單。
小型聚合數(shù)據(jù)類型的特殊編碼
從Redis2.2開始,許多數(shù)據(jù)類型被優(yōu)化為在一定大小下使用更少的空間。哈希,列表,僅由整數(shù)組成的集合,和有序集合,當元素數(shù)量小于一個給定數(shù)字,并且到元素最大尺寸,使用一個非常有效率的內(nèi)存編碼方式可以達到最高減少10倍內(nèi)存(平均節(jié)省5倍內(nèi)存)。
從用戶和API的視角這是完全是透明的。因為這是CPU/memory權(quán)衡,它可能會使用下面的redis.conf指令調(diào)優(yōu)特殊編碼的元素最大數(shù)字和最大尺寸。
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512
如果特殊編碼的值超過了配置中的最大尺寸,Redis將會自動將其轉(zhuǎn)換為普通編碼。這個操作對于小的值非常快,但如果你為了特別大的聚合類型使用特殊編碼更改配置,建議是運行一些基準測試,并且測試檢查下轉(zhuǎn)換時間。
使用32位實例
Redis為了每個鍵使用更少的內(nèi)存而使用32位編譯,因為指針式小的,但是一個實例的最大內(nèi)存使用將會限制在4GB。為了以32位編譯Redis,使用make 32bit。RDB和AOF文件兼容32位和64位實例(同樣也兼容小端字節(jié)序和大端字節(jié)序),因此你可以從32切換到64,或者相反,都沒有問題。
位和字節(jié)層級的操作
Redis2.2引入新的bit和byte等級的操作:GETRANGE, SETRANGE, GETBIT 和 SETBIT。使用這些命令,你可以將Redis字符串類型看成是隨機訪問的數(shù)組。例如,如果你有一個應(yīng)用,使用唯一漸增的證書標識,你可以使用位圖保存用戶的郵件列表訂閱信息,在用戶訂閱和取消訂閱時設(shè)置位,或者相反方向。保存1億用戶的這種信息僅需要占用12M的Redis RAM空間。你同樣可以使用GETRANGE 和 SETRANGE為每位用戶保存單字節(jié)的信息。這僅僅是一個例子,但是它確實可以使用新的基元在非常小的空間內(nèi)去建模一系列的問題。
盡可能使用哈希
小的哈希被編碼在非常小的空間內(nèi),因此你需要在可能的情況下去嘗試使用哈希來代表你的數(shù)據(jù)。例如,如果在網(wǎng)頁應(yīng)用中使用對象代表用戶,使用包含所有必須字段的一個哈希取代分別為name,surname,email,password字段設(shè)置一個不同的key。
如果你想要知道更多,閱讀下面的部分。
使用哈希將存儲在Redis上的普通鍵-值抽象成非常有內(nèi)存效率的
我知道這個部分的標題很嚇人,但是我將會詳細解釋它們包含什么。
基本上可以使用Redis建模一個普通的鍵-值存儲,其中值僅允許是字符串,它不僅比普通的Redis鍵更有存儲效率,同時也比memcached更有存儲效率。
讓我們從一些事實開始:幾個鍵比一個包含一些字段哈希的鍵占用更多的內(nèi)存。這怎么可能呢?我們使用了一個技巧。在理論上,為了確保能實現(xiàn)我們在常量時間內(nèi)執(zhí)行查找(在大O記法中也稱為O(1)),需要使用一個在平均情況下時間復(fù)雜度是常量的數(shù)據(jù)結(jié)構(gòu),例如哈希表。
但是很多時候哈希僅包含少量字段。當一個哈希很小的時,我們可以使用一個O(N)的數(shù)據(jù)結(jié)構(gòu)編碼它,像一個使用長度前綴的鍵值對的線性數(shù)組一樣。因為我們只有在N很小時才會這么使用,因此HGET和HSET命令的均攤時間依舊是O(1):當它包含的元素的數(shù)量增長到很大時,我們會將它轉(zhuǎn)換成一個真正的哈希(你可以在redis.conf里配置限制)。
這不僅僅從時間復(fù)雜度視角運行的很好,但也從常量時間視角看,因為一個鍵值對的線性數(shù)組能很好的處理CPU緩存(相比哈希表,它是一個更好的緩存地方)。
無論如何,因為哈希字段和值不總是代表所有特性的Redis對象,哈希字段不能像一個真實的鍵一樣關(guān)聯(lián)一個生存時間(live),并且只能包含字符串。但我們是沒問題的,這是我們設(shè)計哈希這種數(shù)據(jù)類型API的意圖(我們相信簡單比特性重要,所以嵌套的數(shù)據(jù)結(jié)構(gòu)不被允許,就像單個字段的過期時間不被允許一樣)。
所以,哈希是有內(nèi)存效率的。當我們使用哈希代表一個對象或建模其他有關(guān)聯(lián)的字段分組的問題時,這是非常有用。如果我們使用普通的鍵值業(yè)務(wù)時呢?
想象一下我們想要使用Redis來緩存很多小的對象,那些可以使用JSON編碼的對象,小的HTML片段,簡單的鍵key->布爾值等等?;旧先魏问挛锒际且粋€小的鍵和值的string->string映射。
現(xiàn)在,讓我們假設(shè)我們想要緩存的對象是有序的,例如:
- object:102393
- object:1234
- object:5
這是我們能做的。每次我們只行一個SET操作來設(shè)置一個新的值,我們實際上將鍵拆分為兩部分,一部分像key一樣使用,另外一部分作為哈希的字段名。例如,名為"object:1234"的對象實際上拆分為:
- 一個名為 object:12 的鍵
- 一個名為34的字段
所以,我們使用所有的的字符,但是最后兩位是鍵,并且最后兩個字符是哈希字段的名字。我們使用下面的命令設(shè)置鍵。
HSET object:12 34 somevalue
就像你看到的,每個哈希將包含100個字段結(jié)束,那是一個在CPU和內(nèi)存節(jié)省之間最優(yōu)的折衷。
有另外一件非常重要的事情需要注意,使用這個模式,不管我們緩存的對象的數(shù)字,每個哈希將會擁有100個左右的字段。這是因為,我們的對象總將以一個數(shù)字結(jié)尾,不是一個隨機字符串。從某種意義上說,最后的數(shù)字可以被認為是隱式預(yù)分片的形式。
小數(shù)字呢?像對象:2?我們只使用"object:"來處理這個案例,像一個鍵名,并且整個數(shù)字作為哈希字段名。因此object:2和object:10都將在"object:"鍵內(nèi)結(jié)束,但是一個以鍵名”2",另外一個以”10"。
這個方法我們節(jié)省了多少內(nèi)存?
我用下面的Ruby程序來測試這是如何工作的:
require 'rubygems'
require 'redis'
UseOptimization = true
def hash_get_key_field(key)
s = key.split(":")
if s[1].length > 2
{:key => s[0]+":"+s[1][0..-3], :field => s[1][-2..-1]}
else
{:key => s[0]+":", :field => s[1]}
end
end
def hash_set(r,key,value)
kf = hash_get_key_field(key)
r.hset(kf[:key],kf[:field],value)
end
def hash_get(r,key,value)
kf = hash_get_key_field(key)
r.hget(kf[:key],kf[:field],value)
end
r = Redis.new
(0..100000).each{|id|
key = "object:#{id}"
if UseOptimization
hash_set(r,key,"val")
else
r.set(key,"val")
end
}
這是一個使用Redis2.2的64位實例的結(jié)果:
- UseOptimization set to true: 1.7 MB of used memory
- UseOptimization設(shè)置為真(true):使用1.7MB
- UseOptimization set to false; 11 MB of used memory
- UseOptimization設(shè)置為假(false):使用11MB內(nèi)存
這個是一個數(shù)量級,我認為,這使Redis或多或少以內(nèi)存效率最高的存儲普通鍵值。
注意:為了使這個能工作,確認在你的redis.conf里面包含像這樣的內(nèi)容:
hash-max-zipmap-entries 256
也要記得根據(jù)你的鍵和值的最大大小設(shè)置的下面的字段:
hash-max-zipmap-value 1024
每次一個哈希超過指定的元素數(shù)量或元素尺寸,它將會轉(zhuǎn)化為一個真正的哈希表,并且內(nèi)存節(jié)省將會丟失。
你可能會問了,為什么你不在正常鍵空間隱式的做呢?這樣我就不必擔心了。有兩個原因:其一是我們傾向于做一個顯式的權(quán)衡,并且這是一個在很多事情之間清晰的權(quán)衡:CPU,內(nèi)存,最大元素尺寸。其二是頂級的鍵空間必須支持大量有趣的事情,比如過期時間,LRU數(shù)據(jù)等等,因此使用一般的方式做這些是不太現(xiàn)實的。
但是Redis的做法是,用戶必須理解事物如何運作,這樣他能夠挑選最好的這種方案,并且理解系統(tǒng)將如何精確的運轉(zhuǎn)。
內(nèi)存分配
為了保存用戶的鍵,Redis分配了maxmemory設(shè)置中允許的盡可能多的內(nèi)存(然而還有少量的額外分配可能性)。
精確的值可以在配置文件里面設(shè)置,或者稍后通過CONFIG SET設(shè)置(參見Using memory as an LRU cache for more info)。關(guān)于Redis如何管理內(nèi)存還有一些事情需要注意。
當鍵被移除時,Redis并不總是會釋放(返回)內(nèi)存給OS。這并不是Redis特有的,但這是大多數(shù)malloc()實現(xiàn)的工作方式。例如,如果你將5GB的數(shù)據(jù)填充到一個實例,然后移除2GB的數(shù)據(jù),駐留集(也被稱為RSS,進程消耗的內(nèi)存頁數(shù))將可能仍然是5GB左右,甚至當Redis宣稱用戶內(nèi)存是3GB左右。發(fā)生此事的原因是底層的分配器不能夠輕易的釋放內(nèi)存。例如,通常當大部分被移除的鍵被分配在與其他仍然存在的鍵相同的頁上。
前一點意味著,你需要在峰值內(nèi)存的基礎(chǔ)上提供內(nèi)存。如果你的工作負載時不時需要10GB內(nèi)存,即使大部分時間5GB就可以了,你仍需要提供10GB內(nèi)存。
然而,分配器是聰明的并且能夠復(fù)用空閑的內(nèi)存碎片,因此在你釋放5GB數(shù)據(jù)集中的2GB后,當你再次開始添加更多的鍵時,你將會看到RSS(駐留集)保持穩(wěn)定,當你添加最多2GB額外的鍵時,不會增加更多。內(nèi)存分配器基本上會嘗試復(fù)用之前(邏輯上)釋放的2GB內(nèi)存。
由于這些原因,當你內(nèi)存使用量峰值遠大于當前已使用的內(nèi)存時碎片率是不可靠的。內(nèi)存碎片是計算實際使用的物理內(nèi)存(RSS值)除以當前在用的內(nèi)存總量(Redis分配執(zhí)行的總和)。因為RSS反應(yīng)的是峰值內(nèi)存,當(虛擬的)已使用內(nèi)存低的時候,是因為大量的鍵/值被釋放,但是RSS是高的,
RSS/mem_used率將會非常高。
如果maxmemory沒有設(shè)置,Redis將會一直分配內(nèi)存直到發(fā)現(xiàn)合適,因此它可以(階梯式)吃掉你所有的空閑內(nèi)存。因此,通常建議配置一些限制。你可能也想設(shè)置maxmemory-policy到noeviction(在一些老版本的Redis中,這不是一個默認值)。
當Redis的寫命令達到內(nèi)存限制時,它會返回一個內(nèi)存不足錯誤--從而會引起應(yīng)用程序錯誤,但是不會因為內(nèi)存耗盡而導(dǎo)致整個機器掛掉。
工作進展
工作進展...更多提示很快就會添加。