天天看點

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

在雲栖社群舉辦的線上教育訓練中,具有十年以上系統底層開發經驗的阿裡雲技術專家魯振華帶來了題為《redis記憶體管理和優化》的精彩分享。在分享中,他以資料結構、過期機制和淘汰機制為原理,以記憶體分析為方法論,詳細講解了redis在使用過程需要注意的知識和難點。

以下内容根據直播視訊和幻燈片整理而成。

資料結構

通過了解資料結構,就能知道對象性能,邊界條件,就自然而然知道如何恰當的使用它們,做業務時就能選到最合适的對象。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

上圖是redis最基本的結構體,所有redis對象都被封裝在redisobject中。最基本的結構代碼往往是最精簡的。該結構中有5個成員,type 4 比特,encoding也是4比特。從代碼得知:

redis的資料類型不超過16種,編碼方式不超過16種,且類型跟編碼方式不一一對應,一種類型可能有多個編碼方式,資料也可以共享。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

首先看object的第一個成員type,實際上redis裡面一共有5種類型:字元串、清單、集合、有序集合、哈希,這幾種方式和type的對應關系見下表。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

表中第二列是一般情況下資料類型所用的編碼方式,字元串一般的編碼方式是raw,list對應的是連結清單,集合和哈希表用的是ht,sortedset用的是skiplist。很多情況下集合都是用紅黑樹表示,比如在c++中,是以性能複雜度都是log n,但redis裡面的set用哈希表實作,是以讀寫的複雜度都是o(1)。從表中也可以看出,所有的資料類型都有不止一種編碼方式。redis在資料量小的情況下,對資料存儲做了一些優化。string還有一種特殊類型,即int類型的編碼,接下來會介紹如何利用這些特性。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

string是redis裡用的最多的一個類型,所有的key都是字元串類型,一般情況下編碼方式是raw,它的ptr指向一個sds的資料結構,sds是redis用來儲存一般字元串的資料結構,包含len和free兩個成員的頭部。從len字段的存在可以看出,redis的字元串不依賴結尾’\0’字元的判斷,可以存二進制資料,而free字段的存在表明sds采用了某種預配置設定的機制。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

當字元串較小,redis裡字元串長度<=39時,會用embstr編碼方式。在這種編碼方式下,字元串跟object在連續的記憶體上,省去了多次記憶體配置設定。不過當字元串增長或者改變時,不能用該種方式,需要換成第一種,是以長度限制為39。

string類型還有一種特殊的編碼方式,即字元串數值是整數的時候,為特殊的int類型編碼。int類型不需要ptr指到字元空間,而是直接用指針的值代表字元串的值,是以ptr已經不是指針。這樣就省去了sds開銷,其記憶體占用最小。實際上在redis裡,程式啟動時直接建立了10000個redisobject,代表1-10000的整型,如果lru沒有意義,後面就沒有其他開銷,用預先配置設定好的值。簡單來說,整數類型的value比普通的value節省記憶體,其值為0-10000,lru無效情況下的string object可共享,而且一般情況下沒必要強求embstr。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

上圖是壓縮清單,它相當于把所有的成員都疊在一起,沒有額外的資料結構,空間占用比較小。缺點是讀寫的時候整個壓縮清單都需要修改,是以一般在資料量小的時候才使用,一般能達到10倍的壓縮比。資料量大小都可以通過配置檔案更改,hash和list的預設情況是512和64,需要利用時就對業務進行改造,可以按日期拆分,每天一個key,也可以按數值取模,或按字首拆分等。通過合理的拆分,充分利用壓縮清單特性,壓縮率可達10倍,平均為5倍。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

那其他容器在普通情況下用什麼樣的資料結構呢?算法類的資料結構裡用的最多的為哈希表。因為它的讀寫複雜度都是o(1),是所有資料結構裡面最快的一種。redis中的哈希表使用鍊位址法解決hash沖突問題,若有多個key的hash值一緻,通過周遊連結清單的形式找到目标key。當哈希表的負載因子過大時,沖突幾率變大,其性能就會下降。redis裡面哈希表槽的數目是動态增長的,ht預設初始大小為4。當負載因子超出合理範圍(0.1 – 5)時進行擴縮容(rehash),将原來哈希表裡面的數值rehash,放在新的哈希表裡面,也就是說同時存在兩個哈希表,一舊一新。不過一次性rehash太多的key可能導緻服務長時間不可用,redis采用漸進式rehash,分批進行。

redis裡用字典結構對redis進行封裝,主要就是兩個哈希表,讀寫複雜度均為o(1)。dict的讀寫效率最高。那什麼時間進行漸進式rehash的算法呢?每次對dict執行添加、删除、查找或者更新操作時,除了執行指定的操作以外,還會順帶将ht[0] 哈希表在rehashidx索引上的所有鍵值對rehash到ht[1],并将rehashidx的值增1;直到整個ht[0]全部完成rehash後,rehashindex設為-1,釋放ht[0],ht[1]置為ht[0],在ht[1]中建立一個新的空白表。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

跳躍表是哈希裡面用來做排序的,實作簡單,算法巧妙,算法效率和平衡樹一樣。算法核心為,每插入一個節點,節點是随機數,第n+1層的節點數目為第n層的1/4,性能如上表所示。

過期機制

接下來介紹redis裡的過期機制。redis作為記憶體資料庫,和普通的關系型資料庫相比,優勢在于快,劣勢是持久化稍差。實際上很多記憶體資料庫都是來存一些時效性資料,比如限時優惠活動,緩存或者驗證碼可以采用redis過期機制進行管理。這個時候千萬不要忘記設立過期時間,若不設,隻能等記憶體滿了,一個個檢視key有沒有使用。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

在redis裡設過期資料很簡單,直接用expire指令即可。在key過期的時候,redis會自動把這個key删除。“自動”是指有一個觸發時機,很容易想到用定時器。redis為了不影響正常的讀寫操作,一般隻會在必要或cpu空閑的時候做過期清理的動作;必要的時候即通路key的時候,cpu空閑的時候具體分為兩個,一是一次事件循環結束,進入事件偵聽前,二是系統空閑時做背景定期任務。且第二個是有時間限制的。時間限制為25%的cpu時間。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

redis背景清理任務預設1秒鐘執行10次,也就是100ms。相當于若沒有其他操作,全都用來做背景任務,背景任務如果把cpu占滿的話,一次可以執行100ms。25%的限制表示,背景任務裡面,100ms裡有25ms可以做過期key清理,還有一些其他redis管理方面的任務要做。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

過期key清理算法比較簡單。首先,依次周遊所有db;然後,從db中随機取20個key,判斷是否過期,若過期,則逐出;若有5個以上key過期,則重複上述步驟,否則周遊下一個db;在清理過程中,若達到了時間限制,超過了25ms,則退出清理過程。

總結一下算法的特點:

(1)這是一個基于機率的簡單算法,假設抽出的樣本能夠代表整個key空間(如果運氣不好,有20個key沒過期,剛好取到這20個。算法基礎為在cpu允許的時間内一直清理,一直清理到過期key占整個db中key的25%以下)。

(2)單次運作時間有25% cpu時間的限制。

(3)redis持續清理過期的資料,直至将要過期的key的百分比降到了25%以下。

(4)長期來看,任何給定的時刻已經過期但仍占據着記憶體空間的key的量,最多為每秒的寫操作量除以4。因為redis最大的特色就是高效,是以會犧牲一些無關緊要的部分。

那麼在實際中怎樣更好地發揮key呢?首先,淘汰過程以key為機關,如果有大key存在,有可能删除耗時太長,進而導緻長時間服務不可用。其次,可以調高hz參數,進而可以提升淘汰過期key的頻率,即删除的更加及時;相應的,每次淘汰最大時間限制将減少,可使系統響應時間變快;在一般情況下并不能降低過期key所占比率;另外,會導緻空閑時cpu占用率提高。不過,一般情況下可用預設值,不需要調整。

淘汰機制

機器的記憶體是有限的,當redis的記憶體超了允許的最大記憶體,redis會按照設定的淘汰政策删掉超出部分的内容。在進行淘汰時,先判斷是否設定了最大允許記憶體(server.maxmemory);若是,會調用函數freememoryifneeded,再判斷使用記憶體是否超出最大記憶體限制;若是,就按照設定的淘汰政策淘汰key,直到使用記憶體小于最大記憶體。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

可以設定不同的淘汰政策決定删除的方法。redis預設有6種淘汰政策,如上圖所示。前三種都是從已經設定過期時間的key中删除。key是臨時資料,若設了過期時間,相比于普通資料,優先級會低一些。這裡要注意,lru會影響前面所講的整數對象的共享。

<b>淘汰算法</b>

淘汰算法的步驟分為五步:首先,依次周遊所有db;然後,按照設定的淘汰政策挑選一個key進行淘汰;若政策是lru或ttl,采用近似算法,随機取n個樣本(預設為5,可配置),從中選出最佳值;周遊完後,計算目前使用記憶體量是否超過允許值;若是,則繼續步驟1。

因為大部分的操作都是以key為機關,若一個容器對象很大,對整個key操作,或直接删除key耗時過長,進而導緻其他指令長時間内沒法響應到。另外,寫入大key導緻記憶體超出太多,下次淘汰就需要淘汰很多記憶體。比如說最大記憶體是1g,寫1g記憶體時發現最大記憶體沒有超,但是若再寫入一個5g的資料,等下一個指令來,發現redis裡面有6g資料,說明有5g的資料要去删除,那麼删除時,不會一下就取到5g的數值,很有可能會把其他所有的key都删除之後,發現還不夠,然後再删除那5g的資料。還有一個情況,記憶體100%且無key可淘汰時,這些指令全都不允許再操作了。

記憶體分析

記憶體分析能提取業務特點,了解業務瓶頸,發現業務bug。比如一些使用者說他們的key并不多,但是記憶體卻已滿,分析後才發現,一個list有16g,相當于把售賣的資料都放到了同一個key裡面。業務量不多的時候沒問題,業務量增加的時候,key就已經很大了,說明目前的代碼已經遠遠跟不上業務的發展。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

記憶體分析方法有兩種,一種線上,一種離線。一般采用離線方法,離線記憶體分析是把redis的資料存下來,放到本地,不會有風險,資料可以任意操作,主要分為3步:一是生成rdb檔案(bgsave),二是生成記憶體快照,三是分析記憶體快照。每一步都很簡單,生成記憶體快照會用到一個開源的工具,redis-rdb-tools,用來做rdb檔案的分析。指令為db -c memory dump.rdb &gt; memory.csv。生成的記憶體快照為csv格式,包含:

 database:資料庫id

 type:資料類型

 key:

 size_in_bytes:理論記憶體值(比實際略低)

 encoding:編碼方式

 num_elements:成員個數

 len_largest_element:最大成員長度

其中每一個key都有六列資料,表明屬于第幾号資料庫,這裡要注意理論記憶體值,會比實際略低。程式員可以将生成後的快照導入到資料庫,可以用sqlite,然後進行資料分析,這裡簡單列舉幾條。

 查詢key個數:sqlite&gt; select count(*) from memory;

 查詢總的記憶體占用:sqlite&gt; select sum(size_in_bytes) from memory;

 查詢記憶體占用最高的10個key:sqlite&gt; select * from memory order by size_in_bytes desc limit 10;

 查詢成員個數1000個以上的list:sqlite&gt; select * from memory where type='list' and num_elements &gt; 1000 ;

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

接下來是線上記憶體分析,這裡以簡單的redis-cli為例,有一個-bigkeys的選項,可以把每種類型key的最大key找出來。

在這裡再分享兩個其他的記憶體分析問題。有時候redis産品可能并沒有多少東西,記憶體卻滿了,進而排查出來是其他方面的記憶體占用。一個可能是client占用記憶體的關系,因為用rdb分析的時候沒有client,線上的很多連接配接都會帶着一個很長的指令,redis裡面就會有緩存,dict rehash也會占用一些記憶體。記憶體分析時發現有一些哈希表的記憶體占據特别大,因為有些key很少被通路,導緻rehash被很少觸發,中間緩存的表會一直删不掉。這裡可以用一個腳本通路觸發rehash,看記憶體是否變化,把表删掉即可。

最佳實踐

最佳實踐就是分享一下平時遇到的一些好案例,或者從前面的原理導出的一些結論。首先,最重要的就是選擇正确的資料類型,主要以滿足業務,性能和場景為優先考量。一般資料量不大的業務,沒必要花很大的精力;但是對一些主要業務,就需要做比較細緻的優化。如string類型對象可以考慮使用整數,如果是浮點型,就改成整數,id等的值可以考慮用整數而不是字元串,小整數多的時候考慮使用lru無關逐出政策等。對于資料量大,比較重要,需要做深入優化業務,還可以充分利用壓縮清單,平均來說有5倍的壓縮比。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

這裡舉個官網上的例子,在存key - value的時候,以object後面加一個整數作為id,整個資料庫裡可能都是這種類型的資料,量又特别大,想對它優化的時候,比較通用的優化方法就是取模,相同字首的key單獨做hash。整個db空間相當于一個大的哈希表,這樣就把本來配置設定給整個db空間的key,按照不同的字首分成小的哈希表,每個哈希表裡面保證資料比較小,那這個哈希表就會用壓縮清單來儲存。

redis裡其實有7種資料類型,另外2種是普通的string,獨立出來用于特定場合,它是效率非常高且有用的一種方式。

原理、方法雙管齊下,大神帶你細解Redis記憶體管理和優化

第一個是位圖bitmap,它存儲起來實際上就是字元串,最大可以是512m,當用一些讀寫的位操作函數操作時,位操作很快。其原理是,若統計一個網站的uv,用來通路的使用者的id做辨別。其好處是節省空間。如果按傳統的集合方式實作,則記憶體占用大,且難以合并。如果每天的使用者通路都做了集合,那麼統計一周内的通路,做集合的并集操作,比較占時間。而用bitmap隻需将每天的字元串按位與,計算較快。比如要統計使用者每天有沒有通路網站,1000萬個使用者實際上隻需要430m,用集合會很大。若統計uv,一億兩千八百萬使用者做位操作基本上也是幾百毫秒。 bitmap需要注意的問題是,字元串的最大長度是使用者的id指定,使用者的id是多少,就把bitmap的相應位設為1,是以如果id取的很大,而使用者量很少,bitmap很稀疏的話,相比集合并不能展現其優勢。而且大的bit位一次配置設定會導緻setbit時間過長。那麼,需要合理的進行bit位的壓縮,可以用相對值不用絕對值,按位分開成多個bitmap,每10000個使用者單獨用一個bitmap,存相對值;再或者用哈希函數映射成定長的id如64位等。

第二是hyperloglog,它是一個算法,和日志沒關系,用于計數統計,操作很快,最大空間占用12k,可以記2^64個資料,誤差隻有0.81%。也就是說,要追求準确,就用bitmap,但是有些業務允許有一些誤差,可以使用hyperloglog。hyperloglog就是用來統計位有沒有記成1,不管ip是多少,隻要12k 的記憶體就夠了。如果用集合,一百萬個ip,64位元組,一年的資料可能就要5.4g,1億個ip,一年就要540g,hyperloglog隻要4m就可以。