文章目錄
redis內存優化
1.壓縮值
Redis和客戶端通常受IO約束,並且相對於請求/應答序列的其餘部分,IO成本通常至少爲2個數量級。默認情況下,Redis不會壓縮存儲在其中的任何值,因此在存儲到Redis中之前壓縮數據變得非常重要。這有助於減少有效負載,從而爲你帶來更高的吞吐量、更低的延遲和更高的成本節省。
1.1如何壓縮字符串
有多種壓縮算法可供選擇,每種算法都有其自身的權衡。
- Google的Snappy旨在實現極高的速度和合理的壓縮。
- LZO壓縮速度快和解壓縮速度快。
- 其它的諸如Gzip應用更爲廣泛。
GZIP壓縮比Snappy或LZO使用更多的CPU資源,但提供了更高的壓縮比。對應不經常訪問的冷數據,GZip通常是一個不錯選擇。對於經常訪問的熱數據,Snappy或LZO是更好的選擇。
壓縮字符串需要更改代碼。有些庫可以透明地壓縮對象,你只需要配置庫即可。在其它情況下,你可能必須手動壓縮數據。
1.2優勢
壓縮字符串可以節省30-50%的內存。通過壓縮字符串,還可以減少應用程序和Redis數據庫之間的網絡帶寬。
1.3權衡
壓縮/解壓縮需要你的應用程序執行額外的工作。這種權衡通常是值得的。如果你擔心額外的CPU負載,請切換到一個更快的算法,例如snappy或LZO。
1.4何時避免壓縮
壓縮不應盲目地遵循,有時壓縮不能幫助你減少內存,反而會提高CPU利用率。在少數情況下應避免壓縮:
- 對於較短的字符串,可能會浪費時間。短字符串通常不會壓縮太多,因此增益太小。
- 當數據結構不合理時,則應避免壓縮。JSON和XML擅長壓縮,因爲它們有重複的字符和標籤。
2.使用較小的鍵
Redis鍵在增加Redis實例的內存消耗方面起到了魔鬼般的作用。通常,你應該始終偏愛描述性鍵,但是如果你擁有一個包含數百萬個鍵的大型數據集,那麼這些大鍵可能會花費你的大量金錢。
2.1如何轉換爲較小的鍵
在編寫良好的應用程序中,切換到較短的鍵通常涉及更新應用程序代碼中的幾個常量字符串。
你必須識別Redis實例中的所有大鍵,並通過刪除其中的額外字符來縮短它。你可以通過兩種方式實現:
- 你可以使用RedisInsight識別Redis實例中的大鍵。這爲你提供了有關所有鍵的詳細信息,以及一種根據鍵的長度對數據進行排序的方法。
- 或者,你可以運行命令
redis-cli --bigkeys
使用RedisInsight的優勢在於,它爲你提供了整個數據集中的大鍵,而bigkeys
命令運行在特定的記錄集上,並從該記錄集中返回大鍵,因此,使用bigkeys
很難從整個數據集中識別大鍵。
2.2優勢
舉個例子:假設你有100,000,000個鍵名稱,例如
my-descriptive-large-keyname (28個字符)
現在,如果你將鍵名稱縮短爲
my-des-lg-kn (12個字符)
通過縮短鍵來節省16個字符,即16個字節,這可以節省1,000,000,000*16 = 1.6GB的RAM內存!
2.3權衡
大鍵比短鍵更具描述性,因此,在讀取數據庫時,你可能會發現鍵的相關性較差,但與這種痛苦相比,節省內存和成本更高效。
3.切換到32位
Redis爲你提供了有關64位計算機的以下統計信息。
- 一個空的實例使用~3MB的內存。
- 1百萬個小的鍵->字符串值對使用~85MB的內存。
- 1百萬個鍵->哈希值,表示一個有5個字段的對象,使用~160MB的內存。
與32位計算機相比,64位有更多可用內存。但是,如果你確定數據大小不超過3 GB,那麼存儲在32位是一個不錯的選擇。
64位系統比32位系統使用更多的內存來存儲相同的鍵,尤其是在鍵和值較小的情況下。這是因爲爲小鍵分配了完整的64位,導致浪費了未使用的位。
3.1優勢
從64位計算機切換到32位可以大大降低所用計算機的成本,並可以優化內存的使用。
3.2權衡
對於32位Redis變體,任何大於32位的鍵名都要求該鍵跨越多個字節,從而增加了內存使用量。
3.3何時避免切換到32位
如果你的數據大小預計會增加超過3 GB,則應避免切換。
4.升級Redis版本
Redis 4.0是已發包的最新版本。與以前的版本相比,它包含各種重大改進。
- 它支持
RDB+AOF
混合格式。 - 改善內存使用和性能。
- 引入了新的Memory命令。
- 活動內存碎片整理。
- 更快地創建Redis集羣鍵。
4.1權衡
Redis 4.0仍然不是一個穩定的發行版,而是經過嚴格測試的發行版,因此Redis 3.2是關鍵應用程序的更好選擇,直到Redis 4.0在接下來的幾個月中更加成熟爲止。
5.使用更好的序列化器
Redis沒有任何特定的數據類型來存儲序列化對象,它們作爲字節數組存儲在Redis中。如果我們使用常規的方法來序列化java、python和PHP對象,則它們的大小可能會更大,從而影響內存消耗和延遲。
5.1使用哪些序列化器
代替你的編程語言的默認序列化程序(java序列化對象、python pickle、PHP序列化等),切換到更好的庫。有各種庫,例如——協議緩衝區Protocol Buffers,消息包MessagePack等。
5.1.1MessagePack
MessagePack是一種有效的二進制序列化格式。它使你可以在多種語言(如JSON)之間交換數據。但是它更快,更小。小整數被編碼爲一個字節,典型的短字符串除字符串本身外僅需要一個額外的字節。
正如Redis的創建者Salvatore Sanfilippo所說:
Redis scripting has support for MessagePack because it is a fast and compact serialization format with a simple to implement specification. I liked it so much that I implemented a MessagePack C extension for Lua just to include it into Redis.
Redis腳本支持MessagePack,因爲它是一種快速緊湊的序列化格式,具有易於實現的規範。我非常喜歡它,所以我爲Lua實現了一個MessagePack C擴展,只是將其包含在Redis中。
5.1.2Protocol Buffers
Protocol buffers(通常稱爲Protobuf)是Google開發的一種協議,用於允許對結構化數據進行序列化和反序列化。Google開發它的目的是提供一種比XML更好的方式來使系統進行通信。因此,他們專注於使其比XML更簡單、更小、更快和更可維護。
6.將較小的字符串組合爲哈希
在64位計算機上,字符串數據類型的開銷約爲90字節。換句話說,調用set foo bar
使用大約96個字節,其中90個字節是開銷。只有在以下情況下才應使用String數據類型:
- 該值至少大於100個字節
- 將編碼的數據存儲在字符串中——JSON編碼或Protocol buffer
- 使用字符串數據類型作爲數組或位集
如果你沒有執行上述任何操作,請使用哈希。
6.1如何將字符串轉換爲哈希
假設我們必須在用戶的帖子上存儲評論數,我們可以有一個鍵名,例如user:{userId}:post:{postId}:comments
。
這樣,我們每個用戶的每個帖子都有一個鍵。所以現在,如果我們需要查找整個應用程序的評論總數,我們可以做
Redis::mget("user:{$userId}:post:1", "user:{$userId}:post:2", ...);
要將其轉換爲哈希,你可以執行以下操作:
Redis::hmset("user:{$userId}:comments", "post:1", 20, "post:2", 50);
這將構建一個Redis哈希,其中包含兩個字段post:1
和post:2
,其值分別爲20和50。
6.2優勢
將小字符串組合爲哈希可以減少使用的內存,從而節省成本。
哈希可以在一個非常小的內存空間中有效地編碼,因此Redis製作者建議我們儘可能使用哈希,因爲“幾個鍵比一個包含幾個字段的哈希的單個鍵使用更多的內存”,一個鍵表示一個Redis對象包含的信息比它的值要多的多,另一方面,一個哈希字段只包含指定的值,這就是爲什麼它更高效的原因。
6.3權衡
性能是有代價的。通過將字符串轉換爲哈希,我們節省了內存,因爲它只保存字符串值,而沒有額外的信息,例如:空閒時間idle time
、過期時間expiration
、對象引用計數object reference count
以及與之相關的編碼encoding
。但是,如果我們希望鍵具有過期值,則不能將其與哈希結構相關聯,因爲過期不可用。
6.4何時避免將字符串組合爲哈希
這個決定取決於字符串的數量,如果少於100萬且內存消耗不高,則轉換不會受到很大影響,並且沒有必要增加代碼的複雜性。
但是,如果字符串超過100萬,並且內存消耗很高,那麼肯定應該遵循這種方法。
7.從Set切換爲Intset
僅包含整數的Set集合在內存方面非常節省。如果你的Set集合包含字符串,請嘗試通過將字符串標識符映射到整數來使用整數。
你可以在編程語言中使用枚舉,也可以使用redis哈希數據結構將值映射爲整數。切換爲整數後,Redis將在內部使用IntSet編碼。
這種編碼非常節省內存。默認情況下,set-max-intset-entries
的值爲512,但是你可以在redis.conf中設置此值。
7.1權衡
通過增加set-max-intset-entries
的值,Set集合操作的延遲會增加,並且Redis服務器上的CPU利用率也會增加。你可以在進行此更改之前和之後運行此命令來進行檢查。
運行 `info commandstats`
8.切換到bloom filter或hyperloglog
唯一項可能難以計數。通常,這意味着存儲每個唯一項,然後以某種方式調用這些信息。使用Redis,可以通過使用set和單個命令來完成此操作,但是對於非常大的set,存儲和時間複雜性都是令人望而卻步的。HyperLogLog提供了一種概率替代方法。
如果你的set集合包含大量元素,並且只使用集合進行存在性檢查或消除重複項,那麼使用布隆過濾器bloom filter將使你受益。
Bloom過濾器本身不受支持,但是你可以在redis之上找到幾種解決方案。如果你只使用該set集合來計算唯一元素的數量(例如,唯一IP地址、用戶訪問的唯一頁面等),那麼切換到hyperloglog可以節省大量內存。
8.1權衡
以下是使用HyperLogLog的權衡:
- 從HyperLogLog獲得的結果不是100%準確的,它們的標準誤差約爲0.81%。
- Hyperloglog只告訴你唯一計數。它不能告訴你集合中的元素。
例如,如果你想維護今天有多少個唯一的ipaddress進行了一次API調用。HyperLogLog告訴你今天有46966個唯一IPs
。
但是,如果你想顯示這些46966個IP地址
——它就不能顯示。爲此,你需要在一個set集合中維護所有IP地址。
9.大哈希分片成小哈希
如果哈希中包含大量鍵值對,並且每個鍵值對都足夠小,則將其分解爲較小的哈希以節省內存。要切分HASH表,我們需要選擇一種對數據進行分區的方法。
哈希本身有鍵,可用於將鍵劃分爲不同的分片。分片的數量取決於我們要存儲的鍵總數和分片的大小。使用分片數量和哈希值,我們可以確定鍵所在的分片ID。
9.1分片如何發生
-
數字鍵——對於數字鍵,將根據鍵的數字鍵值將鍵分配給分片ID(在同一分片中保持數字相似的鍵)。
-
非數字鍵——對於非數字鍵,使用CRC32校驗和。在這種情況下,使用CRC32是因爲它返回一個簡單的整數而無需進行額外工作,並且計算速度很快(比MD5或SHA1哈希算法快得多)。
9.2注意事項
在進行分片時,你應該對預期元素的總數
和分片大小
保持一致,因爲這兩個信息是減低分片數量說必需的。理想情況下,你不應該更改這些值,因爲這會更改分片的數量。
如果你要更改任何一個值,則應該有一個將數據從舊數據分片移動到新數據分片的過程(通常稱爲重新分片resharding)。
9.3權衡
將大哈希轉換爲小哈希的唯一權衡是,這會增加代碼的複雜性。
10.將哈希的Hashtable轉換爲Ziplist
哈希有兩種編碼類型:HashTable
和Ziplist
。根據Redis提供的兩種配置——hash-max-ziplist-entries
和hash-max-ziplist-values
,完成存儲哪種數據結構的決定。
默認情況下,redis.conf具有以下設置:
- hash-max-ziplist-entries = 512
- hash-max-ziplist-values = 64
因此,如果一個鍵的任何值超過這兩個配置,它將自動存儲爲Hashtable
而不是Ziplist
。可以觀察到,與Ziplist
相比,HashTable
消耗幾乎兩倍的的內存,因此爲了節省內存,你可以增加這兩個配置並將哈希錶轉換爲ziplist。
10.1爲什麼Ziplist使用更少的內存
Redis中的ziplist實現通過每個條目只存儲三段數據來實現其較小的內存大小; 第一段是前一個條目的長度,第二段是當前條目的長度,第三段是存儲的數據。因此,ziplist消耗更少的內存。
10.2權衡
簡潔是有代價的,因爲更改大小和檢索條目需要更多時間。因此,redis服務器上的延遲增加了,CPU使用率也可能增加了。
注意: 同樣,對於sorted set,也可以將其轉換爲ziplist,但是唯一的區別是zset-max-ziplist-entries
是128,這比哈希的條目少。
11.轉換爲List而不是Hash
Redis哈希存儲字段名和值。如果你有成千上萬個具有相似字段名的小型哈希對象,則字段名使用的內存將累加起來。爲避免這種情況,請考慮使用List而不是Hash。字段名成爲List列表的索引。
雖然這樣可以節省內存,但是僅當你具有成千上萬個哈希並且每個哈希有相似的字段時,才應使用此方法。壓縮字段名是減少字段名使用的內存的另一種方法。
舉個例子。假設你要在Redis中設置用戶詳細信息。你像這樣做:
hmset user:123 id 123 firstname Bob lastname Lee location CA twitter bob_lee
現在,Redis 2.6將其內部存儲爲Zip List;你可以通過運行調試對象user:123
並查看encoding編碼字段進行確認。在這種編碼中,鍵值對是按順序存儲的,因此我們在上面創建的用戶對象將大致如下所示:["firstname", "Bob", "lastname", "Lee", "location", "CA", "twitter", "bob_lee"]
現在,如果你創建第二個用戶,則鍵將被複制。如果你有一百萬個用戶,那麼,再次重複這些鍵是很大的浪費。爲了解決這個問題,我們可以從Python——NamedTuples中借用一個概念。
11.1NamedTuple如何工作
NamedTuple只是一個只讀列表,但是它有一些魔術使該列表看起來像字典。你的應用程序需要維護從字段名到索引的映射,例如"firstname" => 0, "lastname" => 1
,依此類推。
然後,你只需創建一個List列表而不是一個Hash哈希,例如lpush user:123 Bob Lee CA bob_lee
。在應用程序中使用正確的抽象,可以節省大量內存。
11.2權衡
唯一的權衡與代碼複雜性有關。對於小哈希和小列表,Redis內部使用相同的編碼(ziplist),因此切換到列表時不會對性能造成影響。 但是,如果你的哈希中的字段超過512個,則不建議使用此方法。
11.3何時避免將Hash轉換爲List
以下是應避免將哈希轉換爲列表的情況:
- 當對象少於50,000個時。
- 對象是不規則的,即一些用戶擁有很多信息,而其他用戶則很少。
12.壓縮字段名
Redis哈希由字段及其值組成。與值一樣,字段名也消耗內存,因此在分配字段名時需要牢記。如果你有大量具有相似字段名的哈希,則內存將顯着增加。爲了減少內存使用,可以使用較小的字段名稱。
12.1壓縮字段名是什麼意思
參考前面的將哈希轉換爲列表的示例,我們有一個包含用戶詳細信息的哈希。
hmset user:123 id 123 firstname Bob lastname Lee location CA twitter bob_lee
在這種情況下,firstname, lastname, location, twitter
是全字段名,可以縮寫爲:fn, ln, loc, twi
。這樣做可以節省字段名使用的一些內存。
13.避免動態Lua腳本
不要生成動態腳本,這會導致Lua緩存增長並失去控制。加載腳本會消耗內存。內存消耗是由於以下因素造成的。
- 存儲原始文本的server.lua_scripts字典使用的內存
- Lua內部使用的內存,用於保存編譯後的字節碼。因此,如果你必須使用動態腳本,則只需使用簡單的EVAL,因爲沒有必要先加載它們。
13.1使用動態Lua腳本的注意事項
- 記住要跟蹤Lua內存消耗,並使用SCRIPT FLUSH定期刷新緩存。
- 不要在Lua腳本中硬編碼和/或以編程方式生成鍵名,因爲這會使鍵名在集羣Redis設置中無用。
14.啓用List壓縮
List只是數組的鏈接列表,其中沒有一個數組被壓縮。默認情況下,redis不會壓縮列表中的元素。但是,如果使用長列表,並且大多數情況下只從頭和尾訪問元素,則可以啓用壓縮。
我們有兩種配置:list-max-ziplist-size
:-2(8Kb大小,默認);list-compression-depth
:0,1,2(默認爲0)
redis.conf
文件list-compression-depth
=1中的配置更改可幫助你實現壓縮。
14.1什麼是壓縮深度
壓縮深度是在開始壓縮內部節點之前,列表兩端保持不變的列表節點的數量。
例:
- depth = 1表示壓縮除列表的頭和尾之外的每個列表節點。
- depth = 2表示永遠不要壓縮head或head->next或tail或tail->prev。
- depth = 3開始壓縮head->next->next之後以及tail->prev->prev之前,等等。
14.2權衡
對於較小的值(例如,每個列表條目爲40個字節),壓縮對性能的影響很小。當使用最大ziplist大小爲8k的40個字節值時,每個ziplist大約包含200個單獨的元素。創建新的ziplist時,你只需支付額外的“壓縮開銷”費用(在這種情況下,每200個插入一次)。
對於較大的值(例如,每個列表條目爲1024字節),壓縮確實會對性能產生明顯影響,但是對於ziplist大小的所有良好值(-2),Redis仍以每秒150,000次操作以上的速度運行。當使用1024字節值且最大ziplist大小爲8k時,每個ziplist最多可以包含7個元素。在這種情況下,每7個插入一次,將支付額外的壓縮開銷。這就是爲什麼在1024字節元素的情況下性能略有下降的原因。
15.更快地回收過期的鍵內存
當你在一個鍵上設置了過期時間時,redis不會在該瞬間使它過期。相反,它使用隨機算法來找出應該過期的鍵。由於此算法是隨機的,因此鍵可能沒有過期。這意味着redis消耗內存來保存已經過期的鍵。一旦訪問鍵,就將其刪除。
如果只有幾個鍵已過期並且Redis還沒有刪除它們——它是好的。只有當大量鍵還沒有過期時,它是一個問題。
15.1如何檢測過期後是否未回收內存
- 運行
INFO
命令,找到所有數據庫的total_memory_used以及所有鍵的總和。 - 然後獲取一個Redis Dump(RDB)並找出總內存和鍵總數。
查看不同之處,你可以清楚地指出,已過期的鍵仍然沒有回收大量內存。
15.2如何更快地回收過期的鍵內存
你可以按照以下三個步驟之一來回收內存:
- 重新啓動redis服務器
- 在redis.conf中增加maxmemory-samples。(默認值爲5,最大爲10),以便更快地回收過期的鍵。
- 可以設置一個cron作業,該作業在一定間隔後運行scan命令,這有助於回收過期鍵的內存。
- 另外,增加鍵的過期時間也有幫助。
15.3權衡
如果我們增加maxmemory-samples配置,它將使鍵更快地過期,但是會花費更多的CPU週期,從而增加了命令的延遲。其次,增加鍵的過期時間有所幫助,但這需要對應用程序邏輯進行重大更改。
本文參考:
memory-optimizations