一個緩存穿透引發的血案

2010年9月23日,Facebook遭遇了迄今爲止最嚴重的宕機事件之一,網站關閉了4個小時。爲恢復工作,不得不讓FB下線,影響了10億用戶。

在事後的故障報告中提到:

今天,我們修改了一個錯誤的配置,每個客戶端都看到這個錯誤的配置,然後試圖更新它。因爲更新數據需要查詢數據庫集羣,集羣很快就被每秒數十萬次的查詢拖垮。

簡單來說,是某個緩存配置失效,大量請求回源到數據庫,導致DB壓力過大,整體服務不可用,上游重試,整體雪崩。

在高併發系統中,我們或多或少都遇到過類似的緩存穿透到DB,導致壓力過大雪崩的問題。

一般是由於多個線程試圖並行訪問緩存,如果緩存值不存在,那麼線程將會同時嘗試從數據庫獲取數據。導致數據庫CPU飆升,發生崩潰,對上游表現爲超時。上游服務收到超時這種網絡錯誤後,會進行重試,從而放大問題,惡性循環繼續。

那怎麼解決這種緩存穿透導致的雪崩問題呢?

在止損角度來說有兩種方式:防止和減輕。

防止雪崩

防止雪崩最簡單的方法,就是增加多級的緩存。

L1 Cache是內存緩存,L2 Cache是遠程緩存。

這樣好的方式是可以將緩存不存在與失效情況做兩種獨立的控制,不至於所有流量同時大量湧入DB層。

有一點需要注意,內存緩存需要控制大小,做好淘汰,不然會引起頻繁GC問題。

第二種方式是加鎖。

緩存併發的本質在於併發,也就是同一時刻對於某個競態資源的爭搶,多線程搶奪共享資源。

在高併發場景下,爲解決這種資源被爭搶的方式一般是加鎖。進程內鎖解決的是線程的併發資源爭搶;分佈式鎖解決的是分佈式進程對資源的爭搶。

爲具備更高的併發吞吐能力,控制鎖粒度是我們需要關注的,通過對某個緩存鍵加鎖,每次只有一個調用者可以訪問這個緩存鍵。其他併發爭搶資源的進程必須等到鎖的釋放。

這裏也會有個問題,那些來爭搶鎖,但是沒有獲得鎖的線程應該怎麼處理呢?

一種方式是讓線程輪詢獲取鎖,但會造成繁忙的等待。

另一種方式是讓線程sleep一段時間,鎖釋放後發起notify,需要注意驚羣問題。

還有一種方式是緩存一個空值,不需要上層進行自動的重試,併發線程裏面放一個線程穿透去db獲取新值。

我一般的做法是採用雙key+防禦限流的方案。將多個key失效分散開,極端情況兜底保護db。

防禦限流就是採用斷路器,斷路器是反應式的,所以它們無法防止宕機,不過它們可以防止連鎖故障的發生。當事態失控時,它們提供了一個終止開關。如果 Facebook 使用了熔斷機制,就可以避免讓整個網站癱瘓下線。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章