七大快取經典問題

CrazyZard 發表於 2020-06-29

原因分析

在寫快取時,我們一般會根據業務的訪問特點,給每種業務資料預置一個過期時間,在寫快取時把這個過期時間帶上,讓快取資料在這個固定的過期時間後被淘汰。一般情況下,因為快取資料是逐步寫入的,所以也是逐步過期被淘汰的。但在某些場景,一大批資料會被系統主動或被動從 DB 批量載入,然後寫入快取。這些資料寫入快取時,由於使用相同的過期時間,在經歷這個過期時間之後,這批資料就會一起到期,從而被快取淘汰。此時,對這批資料的所有請求,都會出現快取失效,從而都穿透到 DB,DB 由於查詢量太大,就很容易壓力大增,請求變慢。

解決方法

對於批量 key 快取失效的問題,原因既然是預置的固定過期時間,那解決方案也從這裡入手。設計快取的過期時間時,使用公式:過期時間 = base 時間 + 隨機時間。即相同業務資料寫快取時,在基礎過期時間之上,再加一個隨機的過期時間,讓資料在未來一段時間內慢慢過期,避免瞬時全部過期,對 DB 造成過大壓力。

原因分析

快取穿透存在的原因,就是因為我們在系統設計時,更多考慮的是正常訪問路徑,對特殊訪問路徑、異常訪問路徑考慮相對欠缺。
但是如果使用者訪問的是一個不存在的 key,查 DB 返回空(即一個 NULL),那就不會把這個空寫回 cache。那以後不管查詢多少次這個不存在的 key,都會 cache miss,都會查詢 DB。整個系統就會退化成一個“前端+DB“的系統,由於 DB 的吞吐只在 cache 的 1%~2% 以下,如果有特殊訪客,大量訪問這些不存在的 key,就會導致系統的效能嚴重退化,影響正常使用者的訪問。

解決方法

  1. 查詢這些不存在的資料時,第一次查 DB,雖然沒查到結果返回 NULL,仍然記錄這個 key 到快取,只是這個 key 對應的 value 是一個特殊設定的值。但是如果特殊訪客持續訪問大量的不存在的 key,這些 key 即便只存一個簡單的預設值,也會佔用大量的快取空間,導致正常 key 的命中率下降。所以進一步的改進措施是,對這些不存在的 key 只存較短的時間,讓它們儘快過期;或者將這些不存在的 key 存在一個獨立的公共快取,從快取查詢時,先查正常的快取元件,如果 miss,則查一下公共的非法 key 的快取,如果後者命中,直接返回,否則穿透 DB,如果查出來是空,則回種到非法 key 快取,否則回種到正常快取

  2. 構建一個 BloomFilter 快取過濾器,記錄全量資料,這樣訪問資料時,可以直接通過 BloomFilter 判斷這個 key 是否存在,如果不存在直接返回即可,根本無需查快取和 DB。但是BloomFilter 要快取全量的 key,這就要求全量的 key 數量不大,10億條資料以內最佳,因為 10億 條資料大概要佔用 1.2GB 的記憶體。也可以用 BloomFilter 快取非法 key,每次發現一個 key 是不存在的非法 key,就記錄到 BloomFilter 中,這種記錄方案,會導致 BloomFilter 儲存的 key 持續高速增長,為了避免記錄 key 太多而導致誤判率增大,需要定期清零處理。

原因分析

快取雪崩按照快取是否 rehash(即是否漂移)分兩種情況:

  1. 快取不支援 rehash 導致的系統雪崩不可用
  2. 快取支援 rehash 導致的快取雪崩不可用

在上述兩種情況中,快取不進行 rehash 時產生的雪崩,一般是由於較多快取節點不可用,請求穿透導致 DB 也過載不可用,最終整個系統雪崩不可用的。而快取支援 rehash 時產生的雪崩,則大多跟流量洪峰有關,流量洪峰到達,引發部分快取節點過載 Crash,然後因 rehash 擴散到其他快取節點,最終整個快取體系異常。

解決方法

  1. 對業務 DB 的訪問增加讀寫開關,當發現 DB 請求變慢、阻塞,慢請求超過閥值時,就會關閉讀開關,部分或所有讀 DB 的請求進行 failfast 立即返回,待 DB 恢復後再開啟讀開關,如下圖。
  2. 對快取增加多個副本,快取異常或請求 miss 後,再讀取其他快取副本,而且多個快取副本儘量部署在不同機架,從而確保在任何情況下,快取系統都會正常對外提供服務
  3. 對快取體系進行實時監控,當請求訪問的慢速比超過閥值時,及時報警,通過機器替換、服務替換進行及時恢復;也可以通過各種自動故障轉移策略,自動關閉異常介面、停止邊緣服務、停止部分非核心功能措施,確保在極端場景下,核心功能的正常執行。

原因分析

不一致的問題大多跟快取更新異常有關。比如更新 DB 後,寫快取失敗,從而導致快取中存的是老資料。另外,如果系統採用一致性 Hash 分佈,同時採用 rehash 自動漂移策略,在節點多次上下線之後,也會產生髒資料。快取有多個副本時,更新某個副本失敗,也會導致這個副本的資料是老資料。

解決方案

  1. cache 更新失敗後,可以進行重試,如果重試失敗,則將失敗的 key 寫入佇列機服務,待快取訪問恢復後,將這些 key 從快取刪除。這些 key 在再次被查詢時,重新從 DB 載入,從而保證資料的一致性。
  2. 快取時間適當調短,讓快取資料及早過期後,然後從 DB 重新載入,確保資料的最終一致性。
  3. 不採用 rehash 漂移策略,而採用快取分層策略,儘量避免髒資料產生

快取分層策略

我理解是 想 swoole 裡面的 table

原因分析

資料併發競爭,主要是由於多個程式/執行緒中,有大量併發請求獲取相同的資料,而這個資料 key 因為正好過期、被剔除等各種原因在快取中不存在,這些程式/執行緒之間沒有任何協調,然後一起併發查詢 DB,請求那個相同的 key,最終導致 DB 壓力大增

解決方法

  1. 全域性鎖。即當快取請求 miss 後,先嚐試加全域性鎖,只有加全域性鎖成功的執行緒,才可以到 DB 去載入資料。其他程式/執行緒在讀取快取資料 miss 時,如果發現這個 key 有全域性鎖,就進行等待,待之前的執行緒將資料從 DB 回種到快取後,再從快取獲取。
  2. 對快取資料保持多個備份,即便其中一個備份中的資料過期或被剔除了,還可以訪問其他備份,從而減少資料併發競爭的情況。

原因分析

Hot key 引發快取系統異常,主要是因為數十萬、數百萬的使用者同時請求同一個 key,流量集中打在一個快取節點機器,這個快取機器很容易被打到物理網路卡、頻寬、CPU 的極限,從而導致快取訪問變慢、卡頓。

解決方案

  1. 找到對應的熱點key,將這些熱 key 進行分散處理,比如一個熱 key 名字叫 hotkey,可以被分散為 hotkey#1、hotkey#2、hotkey#3,……hotkey#n,這 n 個 key 分散存在多個快取節點,然後 client 端請求時,隨機訪問其中某個字尾的 hotkey,這樣就可以把熱 key 的請求打散,避免一個快取節點過載

  2. 也可以 key 的名字不變,對快取提前進行多副本+多級結合的快取架構設計。再次,如果熱 key 較多,還可以通過監控體系對快取的 SLA 實時監控,通過快速擴容來減少熱 key 的衝擊。最後,業務端還可以使用本地快取,將這些熱 key 記錄在本地快取,來減少對遠端快取的衝擊

原因分析

造成這些大 key 慢查詢的原因很多。如果這些大 key 佔總體資料的比例很小,存 Mc,對應的 slab 較少,導致很容易被頻繁剔除,DB 反覆載入,從而導致查詢較慢。如果業務中這種大 key 很多,而這種 key 被大量訪問,快取元件的網路卡、頻寬很容易被打滿,也會導致較多的大 key 慢查詢。另外,如果大 key 快取的欄位較多,每個欄位的變更都會引發對這個快取資料的變更,同時這些 key 也會被頻繁地讀取,讀寫相互影響,也會導致慢查現象。最後,大 key 一旦被快取淘汰,DB 載入可能需要花費很多時間,這也會導致大 key 查詢慢的問題。

解決方案

  1. 如果資料存在 Mc 中,可以設計一個快取閥值,當 value 的長度超過閥值,則對內容啟用壓縮,讓 KV 儘量保持小的 size,其次評估大 key 所佔的比例,在 Mc 啟動之初,就立即預寫足夠資料的大 key,讓 Mc 預先分配足夠多的 trunk size 較大的 slab。確保後面系統執行時,大 key 有足夠的空間來進行快取。

  2. 如果資料存在 Redis 中,比如業務資料存 set 格式,大 key 對應的 set 結構有幾千幾萬個元素,這種寫入 Redis 時會消耗很長的時間,導致 Redis 卡頓。此時,可以擴充套件新的資料結構,同時讓 client 在這些大 key 寫快取之前,進行序列化構建,然後通過 restore 一次性寫入

  3. 將大 key 分拆為多個 key,儘量減少大 key 的存在。同時由於大 key 一旦穿透到 DB,載入耗時很大,所以可以對這些大 key 進行特殊照顧,比如設定較長的過期時間,比如快取內部在淘汰 key 時,同等條件下,儘量不淘汰這些大 key。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

快樂就是解決一個又一個的問題!