Redis Conf 2020之提高Redis訪問速度最佳實踐

來源:Zohaib Sibte Hassan from Doordash and RedisConf 2020 (redisconf.com/) organized by Redis Labs (redislabs.com)

翻譯:Wen Hui

轉(zhuǎn)載:中間件小哥

1. Cache stampede問題:

Cache stampede問題又叫做cache miss storm,是指在高并發(fā)場景中,緩存同時失效導致大量請求透過緩存同時訪問數(shù)據(jù)庫的問題。

image

如上圖所示:

服務器a,b 訪問數(shù)據(jù)的前兩次請求因為redis緩存中的鍵還沒有過期,所以會直接通過緩存獲取并返回(如上圖綠箭頭所示),但當緩存中的鍵過期后,大量請求會直接訪問數(shù)據(jù)庫來獲取數(shù)據(jù),導致在沒有來得及更新緩存的情況下重復進行數(shù)據(jù)庫讀請求 (如上圖的藍箭頭),從而導致系統(tǒng)比較大的時延。另外,因為緩存需要被應用程序更新,在這種情況下,如果同時有多個并發(fā)請求,會重復更新緩存,導致重復的寫請求。

image

針對以上問題,作者提出一下第一種比較簡單的解決方案,主要思路是通過在客戶端中,通過給每個鍵的過期時間引入隨機因子來避免大量的客戶端請求在同一時間檢測到緩存過期并向數(shù)據(jù)庫發(fā)送讀數(shù)據(jù)請求。在之前,我們定義鍵過期的條件為:

Timestamp+ttl > now()

現(xiàn)在我們定義一個gap值,表示每個客戶端鍵最大的提前過期時間,并通過隨機化將每個客戶端的提前過期時間映射到 0 到gap之間的一個值, 這樣以來,新的過期條件為:

Timestamp+ttl +(rand()*gap)> now()

通過這種方式,由于不同客戶端請求拿到的鍵過期時間不一樣,在緩存沒有被更新的情況下,可以在一定程度上避免同時有很多請求訪問數(shù)據(jù)庫。從而導致比較大的系統(tǒng)延時。

客戶端的實例程序如下:

image

另一種更好的方法是將提前過期時間做一個小的更改,通過取隨機函數(shù)的對數(shù)來將每個客戶端檢查的鍵提前過期時間更均勻的分布在0到gap的區(qū)間內(nèi)(因為隨機函數(shù)取對數(shù)為負值,所以整個提前過期的時間也需要取反),從而獲得更好的性能提升(具體的數(shù)學證明在Optimal Probabilistic Cache Stampede Prevention https://cseweb.ucsd.edu/~avattani/papers/cache_stampede.pdf 這篇文章中)。

image

通過應用以上鍵的提前過期機制,我們看到整體的cache miss現(xiàn)象有明顯的緩解。

image

2. Debouncing

Debouncing在這里指的是在較短時間內(nèi)如果有多個相同key的數(shù)據(jù)讀請求,可以合并成一個來處理,并同時等待數(shù)據(jù)的讀請求完成。作者在這里介紹了可以使用類似javascript 的promise機制來處理請求,具體的步驟如下:

  1. 每個讀請求提供一個L1 Cache Miss函數(shù)并返回一個promise,這個promise會去讀相應的L2 Cache或數(shù)據(jù)庫(如果L2 Cache也Miss的話)

  2. 當多個讀請求使用debouncer訪問相同Key id時,只有第一個請求會調(diào)用L1 Cache Miss函數(shù),并立即返回一個promise。

  3. 當剩下的讀請求到達并且Promise沒有返回時,函數(shù)會立即返回第一個讀請求L1 Cache Miss函數(shù)所返回的promise。

  4. 所有讀請求都會等待這個Promise完成。

  5. 如果當前的Promise完成并返回,接下來的讀請求將重復這個過程。

整體流程如下圖程序所示:

image

在Java中,Caffeine Cache()緩存庫也用到類似的設計來實現(xiàn)。

作者通過使用benchmark tool進行比較,通過使用debouncing的設計使得系統(tǒng)吞吐量有了較大的提高。如下圖所示:

image

3. Big Key

Big Key是指包含數(shù)據(jù)量很大的鍵,在具體應用中,有如下幾個例子:

  1. 緩存過的編譯前的元數(shù)據(jù)(例如前端使用的試圖,菜單等)

  2. 機器學習模型。

  3. 消息隊列和具體消息。

  4. 更多的關于Redis流(stream)的例子。

在這種情況下,我們可以通過使用數(shù)據(jù)壓縮算法來解決big key的問題。選擇壓縮算法的時候我們需要考慮以下幾點:

  1. 壓縮率(compress ratio)

  2. 是否輕量,不能耗費過多的資源

  3. 穩(wěn)定性,是否進行過詳盡的測試,以及社區(qū)支持等。

在選擇算法的時候我們需要平衡上述幾點,例如不能為了提高1% 的壓縮率而使用額外20%資源。

在比較壓縮算法的時候,可以使用lzbench(https://github.com/inikep/lzbench)來比較各類壓縮算法的性能(https://morotti.github.io/lzbench-web/)。另外壓縮算法的性能和具體的數(shù)據(jù)有直接的關系,所以建議大家自己動手嘗試來比較各類壓縮算法的性能差異。

具體的例子(doordash):

Chick-Fil-A 的菜單: 64220 bytes(序列化json)

起司公司產(chǎn)品清單: 350333 bytes(序列化json)

即使單獨拿出來這些數(shù)據(jù)進行傳輸不會有太大問題,但如果有大量類似的公司需要多次傳輸,那么對網(wǎng)絡和CPU負載是相當高的。

在具體選擇壓縮算法過程中,作者比較了LZ4和Snappy,并得到了以下結(jié)論:

  1. 在平均情況下,LZ4比Snappy的壓縮率要高一點,但作者使用自己的數(shù)據(jù)作比較發(fā)現(xiàn)結(jié)論正好相反,LZ4 38.54% 和Snappy 39.71%

  2. 壓縮速率相比兩者差不多,LZ4會比Snappy慢一點點。

  3. 再解壓方面,LZ4比Snappy快得多,在一些測試場景下會有兩倍的差距。

通過以上結(jié)論作者選擇LZ4 作為菜單傳輸?shù)膲嚎s算法,并進行Redis Benchmark測試,使用壓縮算法可以對Redis的讀寫吞吐量有很大提高,具體如下:

image

另外整個系統(tǒng)的網(wǎng)絡流量使用和系統(tǒng)延時也有比較明顯的降低:

image
image

所以作者建議如果使用Redis存儲Big Key時,可以使用壓縮算法來提高系統(tǒng)吞吐量和降低網(wǎng)絡負載。

4. Hot Key

Hot Key(熱鍵)問題指的是在系統(tǒng)中有多個分區(qū)(partition),但因為某一個特定的鍵頻繁的被訪問,導致所有的請求都會轉(zhuǎn)到某一個特定的分區(qū)中,從而導致某個特定分區(qū)資源耗盡而其他分區(qū)閑置的問題。在一些情況下不能使用L1緩存來解決這個問題,因為在這些場景下你需要不斷地從L2 cache或數(shù)據(jù)庫中獲取最新的數(shù)據(jù)。Hot Key問題主要出現(xiàn)在Read Intensive的應用當中。

解決Redis 的 Hot Key問題的一個潛在方案是可以通過主從復制的方式來將讀請求分散到多個replica中。如下圖:

image

但是這種設計沒有從根本解決hot key的問題,所以我們設計系統(tǒng)的目標是盡量使每個請求都分散到不同的cluster nodes中,如下圖所示:

image

所以作者提出了如下針對Redis Hot key的解決方案,主要是通過Redis特有的Key Hash Tag來實現(xiàn)的。我們知道, 在Redis集群模式下,Redis會對每個鍵使用CRC16 算法并取模來決定這個鍵寫在哪個Key Slot中,并存入相應的分區(qū),但如果我們在鍵的名字中使用大括號{},則只有大括號里面的字符會用來計算鍵的槽和相應的分區(qū),而不是整個鍵。舉個例子,如果我們有個鍵:doordash,在正常情況下redis會使用doordash來計算相應的key slot和分區(qū),但如果我們有另外一個鍵:{copy:0} doordash,我們則只會使用copy:0來計算key slot和分區(qū)。以此為基礎,我們可以對Hot key做相應的copy如下:

image

Hot Key doordash現(xiàn)在有三個副本,我們可以把這三個副本均勻分布在redis cluster中。然后在寫入數(shù)據(jù)的時候同時寫入這三個副本到每一個分區(qū)中,在客戶端讀取過程中,通過生成從0-2隨機值然后生成特定的副本key,再去相應的分區(qū)中讀取值。示例程序如下:

image

在這種方式中相同的鍵值需要被復制多次在不同的分區(qū)中,但因為這個鍵值會被訪問多次,所以這個復制操作也是值得的。

Future

在redis 6中,可以使用RESP3協(xié)議和Redis服務器端對客戶端緩存的支持,來提高L1緩存的提前逐出時間,并減少使用網(wǎng)絡資源。另外,使用proxy可以使客戶端請求路由變得更直接。第三點作者提到的是redis 6.0中引入了多線程io,可以顯著提高cpu利用率和提高系統(tǒng)吞吐量。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容