Redis的內存優化方式

redis內存優化

1.壓縮值

Redis和客戶端通常受IO約束,並且相對於請求/應答序列的其餘部分,IO成本通常至少爲2個數量級。默認情況下,Redis不會壓縮存儲在其中的任何值,因此在存儲到Redis中之前壓縮數據變得非常重要。這有助於減少有效負載,從而爲你帶來更高的吞吐量、更低的延遲和更高的成本節省。

1.1如何壓縮字符串

有多種壓縮算法可供選擇,每種算法都有其自身的權衡。

  1. Google的Snappy旨在實現極高的速度和合理的壓縮。
  2. LZO壓縮速度快和解壓縮速度快。
  3. 其它的諸如Gzip應用更爲廣泛。

GZIP壓縮比Snappy或LZO使用更多的CPU資源,但提供了更高的壓縮比。對應不經常訪問的冷數據,GZip通常是一個不錯選擇。對於經常訪問的熱數據,Snappy或LZO是更好的選擇。

壓縮字符串需要更改代碼。有些庫可以透明地壓縮對象,你只需要配置庫即可。在其它情況下,你可能必須手動壓縮數據。

1.2優勢

壓縮字符串可以節省30-50%的內存。通過壓縮字符串,還可以減少應用程序和Redis數據庫之間的網絡帶寬。

1.3權衡

壓縮/解壓縮需要你的應用程序執行額外的工作。這種權衡通常是值得的。如果你擔心額外的CPU負載,請切換到一個更快的算法,例如snappy或LZO。

1.4何時避免壓縮

壓縮不應盲目地遵循,有時壓縮不能幫助你減少內存,反而會提高CPU利用率。在少數情況下應避免壓縮:

  1. 對於較短的字符串,可能會浪費時間。短字符串通常不會壓縮太多,因此增益太小。
  2. 當數據結構不合理時,則應避免壓縮。JSON和XML擅長壓縮,因爲它們有重複的字符和標籤。

2.使用較小的鍵

Redis鍵在增加Redis實例的內存消耗方面起到了魔鬼般的作用。通常,你應該始終偏愛描述性鍵,但是如果你擁有一個包含數百萬個鍵的大型數據集,那麼這些大鍵可能會花費你的大量金錢。

2.1如何轉換爲較小的鍵

在編寫良好的應用程序中,切換到較短的鍵通常涉及更新應用程序代碼中的幾個常量字符串。

你必須識別Redis實例中的所有大鍵,並通過刪除其中的額外字符來縮短它。你可以通過兩種方式實現:

  1. 你可以使用RedisInsight識別Redis實例中的大鍵。這爲你提供了有關所有鍵的詳細信息,以及一種根據鍵的長度對數據進行排序的方法。
  2. 或者,你可以運行命令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位計算機的以下統計信息。

  1. 一個空的實例使用~3MB的內存。
  2. 1百萬個小的鍵->字符串值對使用~85MB的內存。
  3. 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是已發包的最新版本。與以前的版本相比,它包含各種重大改進。

  1. 它支持RDB+AOF混合格式。
  2. 改善內存使用和性能。
  3. 引入了新的Memory命令。
  4. 活動內存碎片整理。
  5. 更快地創建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數據類型:

  1. 該值至少大於100個字節
  2. 將編碼的數據存儲在字符串中——JSON編碼或Protocol buffer
  3. 使用字符串數據類型作爲數組或位集

如果你沒有執行上述任何操作,請使用哈希

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:1post: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的權衡:

  1. 從HyperLogLog獲得的結果不是100%準確的,它們的標準誤差約爲0.81%。
  2. 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

哈希有兩種編碼類型:HashTableZiplist。根據Redis提供的兩種配置——hash-max-ziplist-entrieshash-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

以下是應避免將哈希轉換爲列表的情況:

  1. 當對象少於50,000個時。
  2. 對象是不規則的,即一些用戶擁有很多信息,而其他用戶則很少。

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緩存增長並失去控制。加載腳本會消耗內存。內存消耗是由於以下因素造成的。

  1. 存儲原始文本的server.lua_scripts字典使用的內存
  2. Lua內部使用的內存,用於保存編譯後的字節碼。因此,如果你必須使用動態腳本,則只需使用簡單的EVAL,因爲沒有必要先加載它們。

13.1使用動態Lua腳本的注意事項

  1. 記住要跟蹤Lua內存消耗,並使用SCRIPT FLUSH定期刷新緩存。
  2. 不要在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什麼是壓縮深度

壓縮深度是在開始壓縮內部節點之前,列表兩端保持不變的列表節點的數量。

例:

  1. depth = 1表示壓縮除列表的頭和尾之外的每個列表節點。
  2. depth = 2表示永遠不要壓縮head或head->next或tail或tail->prev。
  3. 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如何檢測過期後是否未回收內存

  1. 運行INFO命令,找到所有數據庫的total_memory_used以及所有鍵的總和。
  2. 然後獲取一個Redis Dump(RDB)並找出總內存和鍵總數。

查看不同之處,你可以清楚地指出,已過期的鍵仍然沒有回收大量內存。

15.2如何更快地回收過期的鍵內存

你可以按照以下三個步驟之一來回收內存:

  1. 重新啓動redis服務器
  2. 在redis.conf中增加maxmemory-samples。(默認值爲5,最大爲10),以便更快地回收過期的鍵。
  3. 可以設置一個cron作業,該作業在一定間隔後運行scan命令,這有助於回收過期鍵的內存。
  4. 另外,增加鍵的過期時間也有幫助。

15.3權衡

如果我們增加maxmemory-samples配置,它將使鍵更快地過期,但是會花費更多的CPU週期,從而增加了命令的延遲。其次,增加鍵的過期時間有所幫助,但這需要對應用程序邏輯進行重大更改。

本文參考:
memory-optimizations

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章