來源:Zohaib Sibte Hassan from Doordash and RedisConf 2020 (redisconf.com/) organized by Redis Labs (redislabs.com)
翻譯:Wen Hui
轉載:中間件小哥
javascript
- Cache stampede問題:
Cache stampede問題又叫作cache miss storm,是指在高併發場景中,緩存同時失效致使大量請求透過緩存同時訪問數據庫的問題。
如上圖所示:
服務器a,b 訪問數據的前兩次請求由於redis緩存中的鍵尚未過時,因此會直接經過緩存獲取並返回(如上圖綠箭頭所示),但當緩存中的鍵過時後,大量請求會直接訪問數據庫來獲取數據,致使在沒有來得及更新緩存的狀況下重複進行數據庫讀請求 (如上圖的藍箭頭),從而致使系統比較大的時延。另外,由於緩存須要被應用程序更新,在這種狀況下,若是同時有多個併發請求,會重複更新緩存,致使重複的寫請求。
前端
針對以上問題,做者提出一下第一種比較簡單的解決方案,主要思路是經過在客戶端中,經過給每一個鍵的過時時間引入隨機因子來避免大量的客戶端請求在同一時間檢測到緩存過時並向數據庫發送讀數據請求。在以前,咱們定義鍵過時的條件爲:
Timestamp+ttl > now()
如今咱們定義一個gap值,表示每一個客戶端鍵最大的提早過時時間,並經過隨機化將每一個客戶端的提早過時時間映射到 0 到gap之間的一個值, 這樣以來,新的過時條件爲:
Timestamp+ttl +(rand()*gap)> now()
經過這種方式,因爲不一樣客戶端請求拿到的鍵過時時間不同,在緩存沒有被更新的狀況下,能夠在必定程度上避免同時有不少請求訪問數據庫。從而致使比較大的系統延時。
客戶端的實例程序以下:
java
另外一種更好的方法是將提早過時時間作一個小的更改,經過取隨機函數的對數來將每一個客戶端檢查的鍵提早過時時間更均勻的分佈在0到gap的區間內(由於隨機函數取對數爲負值,因此整個提早過時的時間也須要取反),從而得到更好的性能提高(具體的數學證實在Optimal Probabilistic Cache Stampede Prevention https://cseweb.ucsd.edu/~avattani/papers/cache_stampede.pdf 這篇文章中)。node
經過應用以上鍵的提早過時機制,咱們看到總體的cache miss現象有明顯的緩解。git
- Debouncing
Debouncing在這裏指的是在較短期內若是有多個相同key的數據讀請求,能夠合併成一個來處理,並同時等待數據的讀請求完成。做者在這裏介紹了可使用相似javascript 的promise機制來處理請求,具體的步驟以下:
1) 每一個讀請求提供一個L1 Cache Miss函數並返回一個promise,這個promise會去讀相應的L2 Cache或數據庫(若是L2 Cache也Miss的話)
2) 當多個讀請求使用debouncer訪問相同Key id時,只有第一個請求會調用L1 Cache Miss函數,並當即返回一個promise。
3) 當剩下的讀請求到達而且Promise沒有返回時,函數會當即返回第一個讀請求L1 Cache Miss函數所返回的promise。
4) 全部讀請求都會等待這個Promise完成。
5) 若是當前的Promise完成並返回,接下來的讀請求將重複這個過程。
總體流程以下圖程序所示:
在Java中,Caffeine Cache()緩存庫也用到相似的設計來實現。
做者經過使用benchmark tool進行比較,經過使用debouncing的設計使得系統吞吐量有了較大的提升。以下圖所示:
github
- Big Key
Big Key是指包含數據量很大的鍵,在具體應用中,有以下幾個例子:
1) 緩存過的編譯前的元數據(例如前端使用的試圖,菜單等)
2) 機器學習模型。
3) 消息隊列和具體消息。
4) 更多的關於Redis流(stream)的例子。
在這種狀況下,咱們能夠經過使用數據壓縮算法來解決big key的問題。選擇壓縮算法的時候咱們須要考慮如下幾點:
1) 壓縮率(compress ratio)
2) 是否輕量,不能耗費過多的資源
3) 穩定性,是否進行過詳盡的測試,以及社區支持等。
在選擇算法的時候咱們須要平衡上述幾點,例如不能爲了提升1% 的壓縮率而使用額外20%資源。
在比較壓縮算法的時候,可使用lzbench(https://github.com/inikep/lzbench)來比較各種壓縮算法的性能(https://morotti.github.io/lzbench-web/)。另外壓縮算法的性能和具體的數據有直接的關係,因此建議你們本身動手嘗試來比較各種壓縮算法的性能差別。
具體的例子(doordash):
Chick-Fil-A 的菜單: 64220 bytes(序列化json)
起司公司產品清單: 350333 bytes(序列化json)
即便單獨拿出來這些數據進行傳輸不會有太大問題,但若是有大量相似的公司須要屢次傳輸,那麼對網絡和CPU負載是至關高的。
在具體選擇壓縮算法過程當中,做者比較了LZ4和Snappy,並獲得瞭如下結論:
1) 在平均狀況下,LZ4比Snappy的壓縮率要高一點,但做者使用本身的數據做比較發現結論正好相反,LZ4 38.54% 和Snappy 39.71%
2) 壓縮速率相比二者差很少,LZ4會比Snappy慢一點點。
3) 再解壓方面,LZ4比Snappy快得多,在一些測試場景下會有兩倍的差距。
經過以上結論做者選擇LZ4 做爲菜單傳輸的壓縮算法,並進行Redis Benchmark測試,使用壓縮算法能夠對Redis的讀寫吞吐量有很大提升,具體以下:
另外整個系統的網絡流量使用和系統延時也有比較明顯的下降:web
因此做者建議若是使用Redis存儲Big Key時,可使用壓縮算法來提升系統吞吐量和下降網絡負載。redis
- Hot Key
Hot Key(熱鍵)問題指的是在系統中有多個分區(partition),但由於某一個特定的鍵頻繁的被訪問,致使全部的請求都會轉到某一個特定的分區中,從而致使某個特定分區資源耗盡而其餘分區閒置的問題。在一些狀況下不能使用L1緩存來解決這個問題,由於在這些場景下你須要不斷地從L2 cache或數據庫中獲取最新的數據。Hot Key問題主要出如今Read Intensive的應用當中。
解決Redis 的 Hot Key問題的一個潛在方案是能夠經過主從複製的方式來將讀請求分散到多個replica中。以下圖:
可是這種設計沒有從根本解決hot key的問題,因此咱們設計系統的目標是儘可能使每一個請求都分散到不一樣的cluster nodes中,以下圖所示:算法
因此做者提出了以下針對Redis Hot key的解決方案,主要是經過Redis特有的Key Hash Tag來實現的。咱們知道, 在Redis集羣模式下,Redis會對每一個鍵使用CRC16 算法並取模來決定這個鍵寫在哪一個Key Slot中,並存入相應的分區,但若是咱們在鍵的名字中使用大括號{},則只有大括號裏面的字符會用來計算鍵的槽和相應的分區,而不是整個鍵。舉個例子,若是咱們有個鍵:doordash,在正常狀況下redis會使用doordash來計算相應的key slot和分區,但若是咱們有另一個鍵:{copy:0} doordash,咱們則只會使用copy:0來計算key slot和分區。以此爲基礎,咱們能夠對Hot key作相應的copy以下:數據庫
Hot Key doordash如今有三個副本,咱們能夠把這三個副本均勻分佈在redis cluster中。而後在寫入數據的時候同時寫入這三個副本到每個分區中,在客戶端讀取過程當中,經過生成從0-2隨機值而後生成特定的副本key,再去相應的分區中讀取值。示例程序以下:
在這種方式中相同的鍵值須要被複制屢次在不一樣的分區中,但由於這個鍵值會被訪問屢次,因此這個複製操做也是值得的。Future在redis 6中,可使用RESP3協議和Redis服務器端對客戶端緩存的支持,來提升L1緩存的提早逐出時間,並減小使用網絡資源。另外,使用proxy可使客戶端請求路由變得更直接。第三點做者提到的是redis 6.0中引入了多線程io,能夠顯著提升cpu利用率和提升系統吞吐量。