【轉自聊聊架構公眾號】 Redis大key圖形化統計及展示
- 2019 年 10 月 4 日
- 筆記
原標題:不管你的Redis集群規模有多大,都是時候思考下如何提升資源利用率了
內容來自「聊聊架構」公眾號。
董明鑫,雪球 SRE 工程師,主要負責保障雪球穩定性、提升資源利用率及提高開發效率等方向。
Redis 是互聯網產品開發中不可缺少的常備武器,它性能高、數據結構豐富、簡單易用,但同時也是因為太容易用了,我們的開發同學不管什麼數據、不管這數據有多大、不管數據有多少通通塞進去,最後導致的問題就是 Redis 記憶體使用持續上升,但是又不知道裡面的數據是不是有用,是否可以拆分和清理。
為了更好地使用 Redis,除了對 Redis 做一些使用規範,還需要對線上使用的 Redis 有充分的了解。那麼問題來了:一個 Redis 的實例用了那麼大的記憶體,裡邊到底存了啥?都有哪些 key?每個 key 佔用了多少空間?
雪球當前有幾十個 Redis 集群,近千個 Redis 實例,5T 的記憶體數據,我們也想要分析業務是否有誤用,以提高資源利用率。當然,曾經我們也深深地被這個問題所困擾,今天我就來和大家分享下我是如何解決這個問題的,希望能給各位一些啟發。
那有沒有什麼辦法讓我們安全高效的看到 Redis 記憶體消耗的詳細報表呢?辦法總比問題多,有需求就有解決方案。雪球 SRE 團隊針對這個問題實現了一個 Redis 數據可視化平台 RDR (Redis Data Reveal)。RDR 可以非常方便的對 Reids 的記憶體進行分析,了解一個 Redis 實例里都有哪些 key,哪類 key 佔用的空間是多少,最耗記憶體的 key 有哪些,佔比如何,非常直觀。
設計思路
我們先梳理下,有什麼辦法可以拿到 Redis 的所有數據。從我的角度看,大概有以下幾種方法,我們分析一下個字的優缺點:
1. 先通過 keys * 命令,拿到所有的 key,然後根據 key 再獲取所有的內容。
- 優點:可以不使用 Redis 機器的硬碟,直接網路傳輸
- 缺點:如果 key 數量特別多,keys 命令可能會導致 Redis 卡住影響業務;需要對 Redis 請求非常多次,資源消耗多;遍曆數據太慢
2. 開啟 aof,通過 aof 文件獲取到所有數據。
- 優點:無需影響 Redis 服務,完全離線操作,足夠安全;
- 缺點:有一些 Redis 實例寫入頻繁,不適合開啟 aof,普適性不強;aof 文件有可能特別大,傳輸、解析起來太慢,效率低。
3. 使用 bgsave,獲取 rdb 文件,解析後獲取數據。
- 優點:機製成熟,可靠性好;文件相對小,傳輸、解析效率高;
- 缺點:bgsave 雖然會 fork 子進程,但還是有可能導致主進程卡住一段時間,對業務有產生影響的風險;
以上幾種方式我們評估之後,決定採用低峰期在從節點做 bgsave 獲取 rdb 文件,相對安全可靠,也可以覆蓋所有業務的 Redis 集群。也就是說每個實例每天在低峰期自動生成一個 rdb 文件,即使報表數據有一天的延遲也是可以接受的。
拿到了 rdb 文件就相當於拿到了 Redis 實例的所有數據,接下來就是生成報表的過程了:
- 解析 rdb 文件,獲取到 Key 和 Value 的內容;
- 根據相對應的數據結構及內容,估算記憶體消耗等;
- 統計並生成報表;
邏輯很簡單,所以設計思路很清晰。數據流圖如下:

我們再看下具體該如何實現,首先是語言選型,雪球 SRE 自研的組件基本都是用 Go 語言做的後端,所以語言選型沒什麼糾結,直接用 Go。然後就是剛剛說的那幾個流程。
1. 解析 RDB
按照 Redis 的協議來做就可以了,這個在 GitHub 上搜索 parse rdb 就可以找到許多解析 rdb 文件的庫,拿過來使用即可。我們使用了 https://github.com/cupcake/rdb 。
2. 估算記憶體消耗
一條記錄會有哪些記憶體使用呢?
我們知道 Redis 的實現裡面有一些基礎的數據結構,就是用這些結構來實現了對外暴露的各種數據類型:比如 sds、dict、intset、zipmap、adlist、ziplist、quicklist、skiplist 等等。只要根據這條記錄的數據類型,找出使用了哪些數據結構,再計算出這些基礎數據結構的記憶體消耗,再加上數據的記憶體使用,以及一些額外開銷比如過期時間等,就可以估算出一條記錄到底使用了多少記憶體。
但是由於 Redis 做了非常多的優化,同樣的一種數據類型,在不同場景下使用的數據結構有可能是不同的。比如 List ,比較老版本的 Redis,會根據 list 元素的數量來決定來使用哪種結構,較短的時候使用 adlist,長之後使用 ziplist,數值可以通過 list-max-ziplist-entries 來配置。
3.2 版本以後全都使用了 quicklist。而不同結構對於記憶體的使用其實是有區別的,我們計算的時候也沒辦法拿到具體的配置,所以都按默認配置來計算,最後得出的值是一個估算的值,不過也基本可以反應使用情況了。如果大家對於 Redis 使用的各種數據結構感興趣,想了解其設計及適用場景,可以多搜索一下相關的資料以及閱讀 Redis 源碼。
舉個計算記憶體使用的例子:
假如我們通過解析 rdb,獲取到了一個 key 為 hello,value 為 world,類型為 string ,ttl 為 1440 的一條記錄,它的記憶體使用是這樣的:
- 一個 dictEntry 的消耗,Redis db 就是一個大 dict,每對 kv 都是其中的一個 entry ;
- 一 個 robj 的消耗,robj 是為了在同一個 dict 內能夠存儲不同類型的 value,而使用的一個通用的數據結構,全名是 Redis Object;
- 存儲 key 的 sds 消耗,sds 是 Redis 中存儲字元串使用的數據結構;
- 存儲過期時間消耗;
- 存儲 value 的 sds 消耗;
前四項基本是存儲任何一個 key 都需要消耗的,最後一項根據 value 的數據結構不同而不同。
- 一個 dictEntry 有 2 個指針,一個 int64 的記憶體消耗;
- 一個 robj 有 1 指針,一個 int,以及幾個使用位域的欄位共消耗 4 位元組;
- 過期時間也是存儲為一個 dictEntry,時間戳為 int64;
- 存儲 sds 需要存儲 header 以及字元串長度 +1 的空間,header 長度根據字元串長度不同也會有所不同;
我們根據以上資訊可以算出,向作業系統申請這些記憶體,真正需要多少記憶體。由於 Redis 支援多種 malloc 演算法,我們就按 jemalloc 的分配方式算,這裡也是可能存在誤差的點。
所以最後 key 為 hello 的這條記錄在 64 位作業系統上一共會消耗 92 位元組。

其他類型的計算也大致是同樣的思路,只不過根據不同的數據結構需要計算不同的記憶體消耗,計算的時候要記得考慮記憶體對齊的情況。還有由於 zset 的演算法涉及到了隨機生成層數,我們也使用同樣的演算法來隨機,但是算出來的值肯定不是精確的,也是一個誤差點。
3. 統計計數
終於可以拿到任何一個 key 的記憶體使用了,哪些是最有意義最有價值的數據呢?
- top N,毫無疑問最大的前 N 個 key 一定是要關注的;
- 不同數據類型的 key 數量元素數量分布以及記憶體使用情況;
- 按照前綴分類,統一的前綴一般意味著某個特定的業務在使用,計算各個分類的 key 數量及記憶體使用情況;
這幾個需求實現起來也都很容易:
- 維護一個小頂堆來存儲前 N 個最大的即可,最後取出堆中的數據即可;
- 計數即可;
- 一般都會有特定的分隔符,比如 :|._ 等字元,按照這些字元切出公共前綴再統計,同時把所有的數字都替換為 0,便於分類;
4. 報表數據


可以每天打開個網頁就可以看到某個 Redis 實例的記憶體使用的詳細情況,是件非常幸福的事情,Redis 的記憶體使用再也不是黑盒。
這個系統上線一年以來對我們優化 Redis 資源使用、提高效率、節約成本提供了非常重要的數據支撐,而且在內部完全自動化,開發同學自己就可以看到當前 Redis 的使用情況是否符合預期,對於保障業務穩定也起到了非常重要的作用。這也是雪球的工程師團隊一貫的做法,SRE 提供高效的工具,開發工程師可以自己運維自己的業務系統,可以極大的提高生產效率。
這個項目參考了 redis-rdb-tool 這個開源項目,但是性能上比它高效幾倍,為了回饋社區,也希望有機會幫到大家,所以我們決定開源出來。
雪球的內部系統根據自己的特殊場景做了自動化獲取 rdb 文件並備份的邏輯,開源出來的版本去除了訂製化,只保留了獲取到 rdb 之後的分析邏輯以及頁面。
項目地址為 https://github.com/xueqiu/rdr。有任何問題可以提 issue,也可以直接郵件聯繫我:[email protected]。