1. 程式人生 > >如何處理redis叢集中hot key和big key

如何處理redis叢集中hot key和big key

概述

redis 叢集部署方式大部分採用類 Twemproxy 的方式進行部署。即通過 Twemproxy 對 redis key 進行分片計算,將 redis key 進行分片計算,分配到多個 redis 例項中的其中一個。tewmproxy 架構圖如下:


由於 Twemproxy 背後的多個 redis 例項在記憶體配置和 cpu 配置上都是一致的,所以一旦出現訪問量傾斜或者資料量傾斜,則可能會導致某個 redis 例項達到效能瓶頸,從而使整個叢集達到效能瓶頸。

hot key出現造成叢集訪問量傾斜

Hot key ,即熱點 key,指的是在一段時間內,該 key 的訪問量遠遠高於其他的 redis key, 導致大部分的訪問流量在經過 proxy 分片之後,都集中訪問到某一個 redis 例項上。hot key 通常在不同業務中,儲存著不同的熱點資訊。比如

  1. 新聞應用中的熱點新聞內容;

  2. 活動系統中某個使用者瘋狂參與的活動的活動配置;

  3. 商城秒殺系統中,最吸引使用者眼球,價效比最高的商品資訊;
    ……

解決方案

1. 使用本地快取

在 client 端使用本地快取,從而降低了redis叢集對hot key的訪問量,但是同時帶來兩個問題:1、如果對可能成為 hot key 的 key 都進行本地快取,那麼本地快取是否會過大,從而影響應用程式本身所需的快取開銷。2、如何保證本地快取和redis叢集資料的有效期的一致性。
針對這兩個問題,先不展開講,先將第二個解決方案。

2. 利用分片演算法的特性,對key進行打散處理

我們知道 hot key 之所以是 hot key,是因為它只有一個key,落地到一個例項上。所以我們可以給hot key加上字首或者字尾,把一個hotkey 的數量變成 redis 例項個數N的倍數M,從而由訪問一個 redis key 變成訪問 N * M 個redis key。
N*M 個 redis key 經過分片分佈到不同的例項上,將訪問量均攤到所有例項。

程式碼如下:

//redis 例項數
const M = 16

//redis 例項數倍數(按需設計,2^n倍,n一般為1到4的整數)
const N = 2

func main() {
//獲取 redis 例項 
    c, err := redis.Dial("tcp"
, "127.0.0.1:6379") if err != nil { fmt.Println("Connect to redis error", err) return } defer c.Close() hotKey := "hotKey:abc" //隨機數 randNum := GenerateRangeNum(1, N*M) //得到對 hot key 進行打散的 key tmpHotKey := hotKey + "_" + strconv.Itoa(randNum) //hot key 過期時間 expireTime := 50 //過期時間平緩化的一個時間隨機值 randExpireTime := GenerateRangeNum(0, 5) data, err := redis.String(c.Do("GET", tmpHotKey)) if err != nil { data, err = redis.String(c.Do("GET", hotKey)) if err != nil { data = GetDataFromDb() c.Do("SET", "hotKey", data, expireTime) c.Do("SET", tmpHotKey, data, expireTime + randExpireTime) } else { c.Do("SET", tmpHotKey, data, expireTime + randExpireTime) } } } 複製程式碼

在這個程式碼中,通過一個大於等於 1 小於 M * N 的隨機數,得到一個 tmp key,程式會優先訪問tmp key,在得不到資料的情況下,再訪問原來的 hot key,並將 hot key的內容寫回 tmp key。值得注意的是,tmp key的過期時間是 hot key 的過期時間加上一個較小的隨機正整數,保證在 hot key 過期時,所有 tmp key 不會同時過期而造成快取雪崩。這是一種通過坡度過期的方式來避免雪崩的思路,同時也可以利用原子鎖來寫入資料就更加的完美,減小db的壓力。

另外還有一件事值得一提,預設情況下,我們在生成 tmp key的時候,會把隨機數作為 hot key 的字尾,這樣符合redis的名稱空間,方便 key 的收歸和管理。但是存在一種極端的情況,就是hot key的長度很長,這個時候隨機數不能作為字尾新增,原因是 Twemproxy 的分片演算法在計算過程中,越靠前的字元權重越大,考後的字元權重則越小。也就是說對於key名,前面的字元差異越大,算出來的分片值差異也越大,更有可能分配到不同的例項(具體演算法這裡不展開講)。所以,對於很長 key 名的 hot key,要對隨機數的放入做謹慎處理,比如放在在最後一個命令空間的最前面(eg:由原來的 space1:space2:space3_rand 改成 space1:space2:rand_space3)。

big key 造成叢集資料量傾斜

big key ,即資料量大的 key ,由於其資料大小遠大於其他key,導致經過分片之後,某個具體儲存這個 big key 的例項記憶體使用量遠大於其他例項,造成,記憶體不足,拖累整個叢集的使用。big key 在不同業務上,通常體現為不同的資料,比如:

  1. 論壇中的大型持久蓋樓活動;

  2. 聊天室系統中熱門聊天室的訊息列表;
    ……

解決方案

對 big key 進行拆分

對 big key 儲存的資料 (big value)進行拆分,變成value1,value2… valueN,

  1. 如果big value 是個大json 通過 mset 的方式,將這個 key 的內容打散到各個例項中,減小big key 對資料量傾斜造成的影響。

//存
mset key1, vlaue1, key2, vlaue2 ... keyN, valueN
//取
mget key1, key2 ... keyN
複製程式碼
  1. 如果big value 是個大list,可以拆成將list拆成。= list_1, list_2, list3, listN

  2. 其他資料型別同理。

既是big key 也是 hot key

在開發過程中,有些 key 不只是訪問量大,資料量也很大,這個時候就要考慮這個 key 使用的場景,儲存在redis叢集中是否是合理的,是否使用其他元件來儲存更合適;如果堅持要用 redis 來儲存,可能考慮遷移出叢集,採用一主一備(或1主多備)的架構來儲存。

其他

如何發現 hot key,big key

1. 事前-預判

在業務開發階段,就要對可能變成 hot key ,big key 的資料進行判斷,提前處理,這需要的是對產品業務的理解,對運營節奏的把握,對資料設計的經驗。

2.事中-監控和自動處理

監控
  1. 在應用程式端,對每次請求 redis 的操作進行收集上報;不推薦,但是在運維資源缺少的場景下可以考慮。開發可以繞過運維搞定);

  2. 在proxy層,對每一個 redis 請求進行收集上報;(推薦,運維來做自然是最好的方案);

  3. 對 redis 例項使用monitor命令統計熱點key(不推薦,高併發條件下會有造成redis 記憶體爆掉的隱患);

  4. 機器層面,Redis客戶端使用TCP協議與服務端進行互動,通訊協議採用的是RESP。如果站在機器的角度,可以通過對機器上所有Redis埠的TCP資料包進行抓取完成熱點key的統計(不推薦,公司每臺機器上的基本元件已經很多了,別再添亂了);

自動處理

通過監控之後,程式可以獲取 big key 和 hot key,再報警的同時,程式對 big key 和 hot key 進行自動處理。或者通知程式猿利用一定的工具進行定製化處理(在程式中對特定的key 執行前面提到的解決方案)

3.事後

儘量還是不要事後了吧,都是血和淚的教訓,不展開講。

謝謝閱讀,歡迎交流。