緩存踩踏:Facebook 史上最嚴重的宕機事件分析
譯者 | 王者
今天,我們修改了一個錯誤的配置,每個客戶端都看到這個錯誤的配置,然後試圖更新它。因爲更新數據需要查詢數據庫集羣,集羣很快就被每秒數十萬次的查詢拖垮。
一個錯誤的配置導致大量的數據庫請求,這種蜂擁而至的請求被稱爲緩存踩踏(Cache Stampede)。這是困擾科技行業的一個常見問題,已經導致很多公司發生宕機事件,比如 2016 年的 “互聯網檔案館”(archive.org)事件。還有很多大型應用程序每天都在與之做鬥爭,比如 Instagram 和 DoorDash。
1 什麼是緩存踩踏?
-
大量的併發線程無法從緩存中獲得數據,然後直接調用數據庫。
-
數據庫由於巨大的 CPU 峯值發生崩潰,並導致超時錯誤。
-
收到超時錯誤後,所有的線程都會發起重試,從而導致另一次踩踏。
-
這個循環不斷持續。
即使你沒有 Facebook 那樣的規模,也會遇到這個問題,因爲它與規模無關。這個問題一直困擾着初創公司和科技巨頭。
2 如何防止緩存踩踏?
我在得知 Facebook 宕機事件後問了自己這個問題。不出所料,自 2010 年以來,關於如何防止緩存踩踏這個問題,人們進行了大量研究,我從頭到尾把它們看了一遍。
在本文中,我們將探索防止和減輕緩存踩踏影響的不同策略。畢竟,你不會希望等到發生宕機後纔去瞭解可以採取哪些安全措施。
一個簡單的解決方案就是增加更多的緩存。雖然這似乎有違直覺,但這與操作系統的工作原理是相似的。
操作系統利用了一個緩存層次結構,其中每個組件負責緩存自己的數據,以獲得更快的訪問速度。
你可以在應用程序中採用類似的模式,其中內存緩存是 Layer 1(L1) 緩存,遠程緩存是 Layer 2(L2) 緩存。
這對於防止被頻繁訪問的數據發生踩踏事件特別有用。即使 L2 緩存中的一個值過期,L1 緩存中可能仍然有緩存的值,避免了重新計算緩存值。
但這種方法有一些值得注意的地方。在應用服務器的內存中,緩存數據可能會導致內存不足,特別是在緩存大量數據的情況下。
舉一個跟隨者踩踏的例子:當一個名人上傳了新照片或視頻到他們的社交媒體賬戶,所有關注者都收到通知,這個時候,他們會急於去查看新上傳的內容。由於內容是新上傳的,還沒有被緩存,這個時候就會導致可怕的緩存踩踏。
那麼,我們該如何解決跟隨者踩踏問題呢?
4 鎖和 Promise
緩存踩踏最主要的核心問題竟態條件——多個線程爭奪共享資源。在這裏,共享資源就是緩存。
在高併發系統中,防止共享資源出現竟態條件的一種常見方法是使用鎖。鎖通常被用在同一臺機器的線程上,但也有一些方法可以將分佈式鎖用於遠程緩存。
通過給緩存鍵加鎖,每次只有一個調用者能夠訪問這個緩存鍵。如果鍵丟失或過期,調用者可以重新生成數據,並放到緩存中,同時保持持有鎖。其他任何試圖讀取同一個鍵的進程都必須等待,直到鎖被釋放。
使用鎖可以解決竟態條件問題,但它會帶來另一個問題,即如何處理所有等待鎖釋放的線程?使用自旋鎖並讓線程連續輪詢鎖?這造成了一種繁忙等待。
在檢查鎖是否可用前,讓線程隨機 sleep 一段時間?現在你要面對的是驚羣效應問題。
引入退避和抖動機制來防止驚羣效應?這可能行得通,但還有另外一個問題。持有鎖的線程必須重新計算值,並在釋放鎖之前更新緩存鍵。
https://www.baeldung.com/resilience4j-backoff-jitter
這個過程可能需要耗費一點時間,特別是當計算成本很高或存在網絡問題時。如果因爲計算緩存而耗盡了可用的連接池,仍然可能導致宕機。
所幸的是,一些頂級科技巨頭正在使用一種更簡單的解決方案:Promise。
如何通過 Promise 來避免自旋
引用 Instagram 工程博客的一篇文章 “驚羣效應和 Promise”:
在 Instagram,當我們啓動一個新集羣時,會遇到一個緩存踩踏問題,因爲集羣的緩存是空的。然後,我們使用 Promise 來解決這個問題:我們緩存的不是實際數據,而是最終會提供數據的 Promise。當訪問緩存但獲取不到數據時,我們不是立即去訪問後端,而是創建一個 Promise 並將其放到緩存中。這個 Promise 會去查詢後端。這樣做的好處是,其他併發請求也會拿到這個 Promise,而所有這些併發線程都將等待後端請求返回的實際數據。
通過緩存 Promise 而不是實際數據,就不需要自旋鎖。第一個獲取緩存數據失敗的線程將使用原子操作(例如 Java 的 computeIfAbsent)創建並緩存異步 Promise。所有後續的 fetch 請求都會立即返回這個 Promise。
你仍然需要使用鎖來防止多個線程訪問緩存鍵,但假設創建 Promise 是一個近乎即時的操作,那麼線程停留在自旋鎖中的時間長度就可以忽略不計了。
這就是 DoorDash 所採用的避免高速緩存踩踏的方法。
但是,如果重新計算緩存數據需要相當長的時間,那該怎麼辦?即使線程能夠立即獲取到緩存的 Promise,它們仍然需要等待異步進程完成後才能將數據返回。
雖然這種場景不一定會導致宕機,但仍然會導致尾部延遲和影響整體用戶體驗。如果保持較低的尾部延遲對於應用程序來說很重要,那麼就需要考慮另外一種策略。
5 預先重計算
預先重計算 (也稱爲提前過期) 背後的原理很簡單。在緩存鍵正式過期前,重新計算緩存值並延長過期時間。這可以確保緩存始終是最新的,並且不會發生緩存失效。
預先重計算最簡單的實現是使用後臺進程或 cron 作業。例如,假設有一個緩存鍵,它的 TTL 是一個小時,而重新計算緩存值需要兩分鐘。cron 作業可以在 TTL 到期前五分鐘運行,並在更新數值後將 TTL 延長一個小時。
雖然這個想法理論上很簡單,但它有一個明顯的不足。除非你確切地知道將使用哪些緩存鍵,否則你就需要重新計算緩存中所有的鍵,這可能是一個非常費時費力的過程。
由於這些原因,我無法在生產環境中找到這種預先重計算的例子,但有一個例外。
概率性預先重計算
2015 年,一組研究人員發表了一份白皮書,叫作 “最優概率性緩存踩踏預防”。在白皮書中,他們描述了一種算法,用於預測何時在緩存過期前重新計算緩存值。
https://cseweb.ucsd.edu/~avattani/papers/cache_stampede.pdf
雖然白皮書中提到了很多數學理論,但這個算法可以簡單地歸納爲:
1currentTime - ( timeToCompute * beta * log(rand()) ) > expiry
2
其思想是,每當線程從緩存中獲取數據時,都會執行這個算法。如果返回 true,那麼該線程將重新計算這個緩存值。離過期時間越近,這個算法返回 true 的幾率就會顯著增加。
雖然這個策略不是最容易理解的,但執行起來相當簡單,不需要任何額外的組件,也不需要重新計算緩存中所有的值。
在 2016 年的宕機事件後,archive.org 開始使用這種方法。RedisConf17 的一個演講對概率性預先重計算的工作原理進行了很好的概述,我強烈建議觀看
https://youtu.be/1sKn4gWesTw
當然,預先重計算假設有一個值需要重新計算,它本身並不能防止追隨者踩踏問題。爲此,你需要將其與鎖和 Promise 結合起來使用。
6 如何停止正在發生的緩存踩踏
Facebook 的緩存踩踏事件之所以如此具有破壞性,其原因之一是即使工程師找到了解決方案,也無法進行部署,因爲踩踏事件仍在進行當中。
更糟糕的是,每次客戶端在試圖查詢數據庫時出現錯誤,都會將其解釋爲無效值,並刪除相應的緩存鍵。這意味着即使原來的問題被修復,查詢請求流仍在繼續湧入。只要數據庫無法滿足某些請求的數據,就會帶來更多的請求。我們陷入了一個不讓數據庫恢復到正常狀態的循環中。
現實情況是,沒有人能保證預防總是有效的,所以在出現問題時你還需要知道如何降低影響。防禦性編程規定要制定好計劃,以防流量繞過屏障發生踩踏事件。
所幸的是,有一個已知的模式可用來處理這個問題。
迴路斷路器
在程序中使用斷路器的想法並不是什麼新鮮事。在 Michael Nygard 的《Release It!》於 2007 年出版後,斷路器模式就開始流行起來。Martin Fowler 在他的文章《迴路斷路器》中寫道:
斷路器背後的基本思想非常簡單。你將一個受保護的函數調用封裝在一個斷路器對象中,斷路器對象負責監控故障。一旦故障達到某一閾值,斷路器就跳閘,所有對斷路器的進一步調用都返回錯誤,根本調用不到受保護的函數。
斷路器是反應式的,所以它們無法防止宕機,不過它們可以防止連鎖故障的發生。當事態失控時,它們提供了一個終止開關。如果 Facebook 使用了熔斷機制,就可以避免讓整個網站癱瘓下線。
當然,斷路器不像在 2010 年那麼流行了。現在,有幾個庫附帶了斷路器,如 Resilience4j、Istio 和 Envoy。Netflix 和 Lyft 等公司在生產環境中使用了這些服務。
7Facebook 從中吸取了什麼教訓?
在本文中,我們討論了很多關於解決高速緩存踩踏問題的不同策略,以及其他技術公司是如何使用它們的。那麼 Facebook 呢?Facebook 從故障中吸取了什麼教訓?他們採取了什麼措施來防止故障再次發生?
Facebook 工程博客的一篇文章 “揭祕: 向數百萬人直播視頻” 討論了他們對 Facebook 網站架構所做出的改進。這篇文章討論了我們已經討論過的內容,比如緩存層次結構,但也提到了一些新的方法,比如 HTTP 請求合併。這篇文章值得一讀,如果你時間不夠,這個視頻爲你提供了一個全面的概述。
https://engineering.fb.com/2015/12/03/ios/under-the-hood-broadcasting-live-video-to-millions/
https://www.facebook.com/Engineering/videos/10153675295382200/?t=0
可以說,Facebook 已經從過去的錯誤中吸取了教訓。
8 寫在最後:
雖然我認爲有必要了解高速緩存踩踏是如對系統造成破壞的,但我不認爲每個技術團隊都一定要立即把文中提到的措施添加到自己的架構中。選擇處理高速緩存踩踏問題的策略取決於你的實際場景、架構和流量負載。但是,當你在面對大規模的流量時,瞭解高速緩存踩踏問題和可能的解決方案對你來說肯定是有好處的。
原文鏈接:
https://betterprogramming.pub/how-a-cache-stampede-caused-one-of-facebooks-biggest-outages-dbb964ffc8ed
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/edX7NuRRGN6C_2kfTwkfiA