Reddit是如何解決三個臭皮匠的快取首次更新問題?

banq發表於2021-07-20

本月,Reddit 的一名資深軟體工程師分享了一個真實世界的例子,說明微服務如何幫助提高 Reddit 的彈性——一個深思熟慮的案例研究,來自他自己處理搜尋請求突然激增的經驗。這是一個很好的例子,可以分享您的知識,透過解決您遇到的有趣問題和解決方案來幫助更大社群中的其他人。但這也是一個例子,說明長期存在的問題如何在微服務領域找到新的解決方案。
最重要的是,他用三個臭皮匠的有趣比喻完美地說明了這一切。

三個臭皮匠是一個鬧劇喜劇三重奏,他們經常嘗試在簡單的日常任務上進行協作,但最終總是妨礙對方並傷害對方。一個場景:他們一起試圖穿過一個門口。但是因為他們試圖同時並肩穿過,他們撞到了對方;最終,沒有人能夠透過。Reddit 也遇到過透過分散式微服務架構推送請求的類似模式。

 

問題
在 Reddit,我們在從中斷中恢復時遇到了一個有趣的規模問題。我們在微服務上游的 API 閘道器級別有一個響應快取;和快取的響應有一個 TTL。現在假設該站點停機的時間比 TTL 長,因此快取已被重新整理清零。
當站點恢復時,我們會收到大量請求 其中許多是重複請求同一個資源,都是在短時間內發生的。在正常操作期間,大多數這些重複請求將從快取中提供服務。但是當從這樣的中斷中恢復時,因為沒有快取任何東西,所有重複的請求都會同時命中我們的微服務、底層資料庫和搜尋引擎。這會導致流量氾濫,以至於在請求超時內沒有任何請求成功,因此沒也有響應被快取;並且該站點會立即再次進行修復。
我們將這種情況稱為“三個臭皮匠問題”,儘管它更常被稱為“雷霆萬鈞Thundering Herd”、 “狗堆效應”或“快取踩踏事件”。
 

解決方案
我們對三個臭皮匠問題的解決方案是在微服務級別對請求進行重複資料刪除和快取響應。請求重複資料刪除(也稱為請求摺疊或請求合併)意味著重新排序重複的請求,以便它們一次執行一個。此解決方案有效的原因是,從概念上講,它對請求重新排序,以便從不重複併發執行,甚至不在不同的後端例項上(分散式鎖強制執行此操作)。然後第一個請求被處理並且它的響應被快取。然後,該請求的所有後續重複項將被序列執行並從快取中得到滿足。這使我們能夠更有效地利用我們的快取,併為我們節省了底層資料庫和搜尋引擎上重複請求的負載。
將重複資料刪除/快取移至其堆疊中的不同點——特別是其微服務級別——以及“一個可以處理許多併發請求的網路堆疊”。然後工程師只需實現程式碼,確保不會無意中同時處理兩個重複項(使用分散式鎖)。剩下的就是檢查已經檢索到的響應是否存在,如果沒有找到快取的響應,則只建立新的響應。
將重複資料刪除視為迫使 三個臭皮匠在通往廚房的門口排成一條有秩序的隊伍。然後第一個臭皮匠進了廚房,拿著一碗扁豆湯離開,那碗湯就被收起來了。然後另外兩個臭皮匠得到了快取的碗湯。
 
Reddit 的 API 閘道器將來自不同平臺的所有傳入請求整理成標準形式,以便於處理(同時丟棄任何不相關的多餘變數)。但是,當它們達到微服務級別時,重複資料刪除最終會使用一種稱為雜湊表的簡單程式設計結構來處理——其中一個值與一個可以在以後檢索它的唯一識別符號(一個鍵)配對。這建立了一種發現重複值的簡單方法,因為它們已經被分配了一個識別符號。
Reddit 提供了一個示例中的程式碼片段,其中使用 Pottery 的Redis Redlock實現來實現分散式鎖。GitHub 儲存庫中,
重要的是,分散式鎖具有自動釋放超時以保持活性。想象一個執行緒獲取鎖然後在臨界區死亡的情況。如果沒有自動釋放超時,鎖將永遠不會被釋放,從而導致死鎖。
解決三個臭皮匠問題的最後一個構建塊是快取響應結果。同樣,我們可以實現一個裝飾器來包裝端點函式來快取響應,它使用請求雜湊作為快取鍵。它嘗試查詢該快取鍵,並在命中時返回快取的響應。如果未命中,它會呼叫底層端點函式,快取後續重複請求的響應,然後返回計算出的響應。
 

問答

  • 為什麼不在 CDN 或邊緣而是在微服務級別對請求和快取響應進行重複資料刪除?

請求來自不同的平臺和不同的形式。所有這些請求都由我們的 API 閘道器整理成標準格式。因此,透過在我們知道它們不相關的層扔掉不相關的變數,我們的更多請求看起來是一樣的。這提高了我們識別重複請求的能力並最大化我們的響應快取命中率。
此外,作為微服務所有者,我們的團隊可以更好地控制微服務中請求和響應發生的事情,而配置邊緣發生的事情的能力則更弱。這不僅僅是一種所有權權衡;它還允許我們在我們的微服務中進行許可權檢查、個性化等操作。
最後,透過在微服務級別進行重複資料刪除和快取,我們有更多機會為我們的原始請求流進一步檢測、記錄和觸發事件。
  • 在請求重複資料刪除期間,如果您的底層基礎架構出現問題並且分散式鎖自動超時,會發生什麼情況?

我們使用分散式鎖只是為了防止重複請求造成負載。我們不會使用鎖來強制執行資料一致性、防止競爭條件或出於任何其他原因。因此在最壞的情況下,如果鎖超時,一些重複的請求可以立即執行臨界區。即使在這種情況下,鎖也有助於透過防止所有重複同時執行來減輕我們苦苦掙扎的基礎設施的負載。
  • 為什麼不在您的微服務中更深入地刪除重複的函式呼叫和快取函式返回值?

這是一種有效的方法,您可能會考慮在微服務中執行此操作。您可以使用函式的引數來構造鎖定/快取鍵,並且可以快取昂貴函式的返回值。由於引數排列較少,在微服務中進行更深層次的重複資料刪除和快取可以提供更高的快取命中率。
另一方面,在您的微服務中更高層進行重複資料刪除和快取可以節省更多工作。您可能有一個昂貴的 I/O 繫結函式來查詢您的資料儲存,以及另一個昂貴的 CPU 繫結函式來呈現響應。更高階別的快取,例如圍繞端點函式,將節省對兩個昂貴函式的呼叫。
在此示例中,為簡單起見,我們對端點函式進行了重複資料刪除和快取。

 

相關文章