2010年9月23日,Facebook遭遇了迄今爲止最嚴重的宕機事件之一,網站關閉了4個小時。爲恢復工作,不得不讓FB下線,影響了10億用戶。
在事後的故障報告中提到:
今天,我們修改了一個錯誤的配置,每個客戶端都看到這個錯誤的配置,然後試圖更新它。因爲更新數據需要查詢數據庫集羣,集羣很快就被每秒數十萬次的查詢拖垮。
簡單來說,是某個緩存配置失效,大量請求回源到數據庫,導致DB壓力過大,整體服務不可用,上游重試,整體雪崩。
在高併發系統中,我們或多或少都遇到過類似的緩存穿透到DB,導致壓力過大雪崩的問題。
一般是由於多個線程試圖並行訪問緩存,如果緩存值不存在,那麼線程將會同時嘗試從數據庫獲取數據。導致數據庫CPU飆升,發生崩潰,對上游表現爲超時。上游服務收到超時這種網絡錯誤後,會進行重試,從而放大問題,惡性循環繼續。
那怎麼解決這種緩存穿透導致的雪崩問題呢?
在止損角度來說有兩種方式:防止和減輕。
防止雪崩
防止雪崩最簡單的方法,就是增加多級的緩存。
L1 Cache是內存緩存,L2 Cache是遠程緩存。
這樣好的方式是可以將緩存不存在與失效情況做兩種獨立的控制,不至於所有流量同時大量湧入DB層。
有一點需要注意,內存緩存需要控制大小,做好淘汰,不然會引起頻繁GC問題。
第二種方式是加鎖。
緩存併發的本質在於併發,也就是同一時刻對於某個競態資源的爭搶,多線程搶奪共享資源。
在高併發場景下,爲解決這種資源被爭搶的方式一般是加鎖。進程內鎖解決的是線程的併發資源爭搶;分佈式鎖解決的是分佈式進程對資源的爭搶。
爲具備更高的併發吞吐能力,控制鎖粒度是我們需要關注的,通過對某個緩存鍵加鎖,每次只有一個調用者可以訪問這個緩存鍵。其他併發爭搶資源的進程必須等到鎖的釋放。
這裏也會有個問題,那些來爭搶鎖,但是沒有獲得鎖的線程應該怎麼處理呢?
一種方式是讓線程輪詢獲取鎖,但會造成繁忙的等待。
另一種方式是讓線程sleep一段時間,鎖釋放後發起notify,需要注意驚羣問題。
還有一種方式是緩存一個空值,不需要上層進行自動的重試,併發線程裏面放一個線程穿透去db獲取新值。
我一般的做法是採用雙key
+防禦限流
的方案。將多個key失效分散開,極端情況兜底保護db。
防禦限流就是採用斷路器,斷路器是反應式的,所以它們無法防止宕機,不過它們可以防止連鎖故障的發生。當事態失控時,它們提供了一個終止開關。如果 Facebook 使用了熔斷機制,就可以避免讓整個網站癱瘓下線。