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

Akay發表於2018-12-19

概述

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

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

由於 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.事後

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

謝謝閱讀,歡迎交流。


相關文章