如何在一週內使用Kafka+Redis構建分散式排行榜系統? - Aritra

banq發表於2021-11-19

這是 T20 世界盃賽季,我們想為我們的使用者建立一個測驗系統,用於短期預言預測。在比賽開始時要求使用者預測場景,最後,主持人將提交所有預測場景中實際發生的情況。評分將根據誰回答正確以及回答所花費的時間進行。

鑑於我們系統的規模,我們估計可能有 5 萬人參與測驗。所以我們的目標是為100 萬使用者構建。這大致為我們提供了 20k 的目標 qps。

我的經驗法則是,對於一個新系統,總是為預期使用者數量的兩倍設計系統。因此,如果您最終擁有更多使用者,您的系統不會無法擴充套件。而且使用者通常會隨著時間的推移而增長,因此您不必不斷地更改系統。但這並不意味著您將兩倍的基礎設施放在實際需要的地方。我通常會在我的系統中啟用自動縮放,因此每當出現流量高峰時,系統都會自動縮放。

 

在進行規模估算後,我們迅速確定了哪些是大規模 API,以及處理能力較高的地方,以便我們可以有選擇地為這些 API 進行可擴充套件的設計。我們不關心或花太多時間的其餘小規模 API。

所以這些是我們確定的大規模 API

  1. 獲取使用者評分和排名
  2. 獲取排行榜
  3. 計算排行榜(主要是資料處理而不是API)
  4. 釋出使用者答案

  

設計注意事項

看到要求後,我立即明白了兩件事。

  1. 出於最明顯的原因,我們將需要非同步和分散式處理來計算分數和排行榜。如果您計劃在單個節點上為 100 萬或更多使用者同步執行分數計算,祝您好運。想法很簡單,我們將一個大任務拆分為多個小任務,並在不同的節點上執行它們,讓它們按照自己的節奏進行。而且我們希望這個過程是容錯的。
  2. Cache將成為我們閱讀使用者和排行榜排名的朋友。主要有兩個原因,速度和易於擴充套件。對於簡單的讀取操作,快取比 DB 快得多,並且通常比Postgres更容易擴充套件Redis/Memcached/Hazelcast叢集。

 

在做出設計決策時,我選擇使用我熟悉的技術,不想冒險尋找適合此用例的最佳技術。例如,我熟悉Redis,因此對於快取,它是顯而易見的選擇。此外,每當我聽到排行榜時,它會自動轉換為我腦海中的Redis 排序集。對於非同步處理,Kafka仍然是我的第一選擇。如果有適當的時間我可能會做更多的探索,但這次我不會徘徊在尋找最好技術的荒野中,因為我沒有時間!!!!

 

釋出使用者回答 API

這個 API 會讓我們的使用者提交測驗的答案。這裡的主要挑戰是這會產生大量併發寫入操作,這將大大增加我們資料庫的負載。所以我們必須做兩件事

  1. 減少資料庫負載
  2. 產生某種背壓或讓 DB 操作員按自己的節奏工作,以免 DB 不堪重負

減少寫入負載的解決方案是進行批處理。因此,如果我必須執行 10 次寫入操作,我會將它們批處理以獲取單個查詢,然後執行 ​​1 次 DB 寫入操作。

背壓和訊息佇列是配合的,架構圖如下:

如何在一週內使用Kafka+Redis構建分散式排行榜系統?  - Aritra

  • 響應接收器接收使用者響應並返回 202 HTTP 狀態程式碼。這就像說我收到了你的請求,我將處理它,但你繼續做你正在做的事情。這是非同步處理的第一步,我們不會阻止呼叫者。
  • 響應接收器將使用者響應放入訊息佇列中,該佇列再次分割槽以用於可擴充套件性/可用性/冗餘目的。如果您已經瞭解 Kafka,您可以很容易地理解分割槽。如果您不這樣做,只需將其視為一種將佇列中的訊息分發到多個較小的隔離佇列的方法,這些佇列在技術上可以駐留在不同的節點上。如果您知道資料庫分片,那麼它是相同的,但主要用於訊息佇列。在這裡閱讀更多關於 Kafka 的資訊
  • 現在,這些原始響應由批處理器接收。然後它建立一批 10 條訊息並將它們推送到下一個處理階段。
  • 資料庫編寫器提取批次並向資料庫中插入查詢。背壓主要由資料庫編寫器引入,由於訊息消費者是基於拉取的,因此它以自己的速度獲取訊息。因此我們可以防止資料庫過載。由於它是批量處理而不是執行 100 個資料庫查詢,因此我們只執行了 10 個資料庫查詢。

由於我們使用訊息佇列在這裡,隨著非同步無使用者響應丟失,我們有內建的重試能力,它也是分配的負載在多個消費者。我們可以在需要時擴充套件我們的訊息佇列和我們的服務。

 

分數和排行榜計算

排行榜計算是主要問題,如果你看到這個系統,它不像我們事先知道正確答案的傳統測驗系統。因此,我們無法在有人回答後立即計算分數和排行榜。一旦主持人提交了正確的答案,我們必須計算所有使用者的分數和排名。因此,一次性完成大量工作。很明顯,我們的節點和資料庫伺服器都無法同步正確處理時尚。那麼我們該怎麼辦?我們再次回到我們的朋友 Messages Queues 進行非同步處理。很酷,所以我們可以非同步計算分數,但是排行榜呢?這需要一直可用,對嗎?那麼排名呢?除非計算出所有使用者的分數,否則您無法真正進行排名,對嗎?並且專門計算排名可能是一個難題。

還記得我在談論快取時簡要提到了 Redis 嗎?他們有一個漂亮的東西叫做排序集 Sorted set,在有序集合中,您可以新增帶有分數的鍵,Redis 會在O(log(N)) 中對其進行相應排序。這將解決我們的排名問題。它還允許我們執行範圍查詢,例如給我前 5 名,或者讓我獲得特定鍵的排名,所有這些都發生在 O(log(N)) 中。這正是我們在這裡所需要的。

這些元素被新增到一個將 Redis 物件對映到分數的雜湊表中。同時元素被新增到一個跳躍列表,將分數對映到 Redis 物件(所以物件在這個“檢視”中按分數排序)——排序集內部

  • 架構圖

如何在一週內使用Kafka+Redis構建分散式排行榜系統?  - Aritra

看起來有點嚇人不是嗎?請允許我解釋一下

  1. 一旦主持人提交正確答案,就會推送分數計算觸發訊息,這將啟動整個處理流程。
  2. 批處理器接收觸發訊息並根據正確回答的人數生成一對 DB 偏移和限制物件。例如,如果 10 個人回答正確並且批次大小為 5,那麼它將生成兩個物件(批次)。批次 1 {offset: 0, limit: 5},批次 2 {offset: 5, limit 5}。我們為什麼要做這個?這樣我們就可以執行批處理或執行分頁的資料庫查詢,並且我們最終不會無限制地呼叫資料庫。因此,如果我必須從資料庫中獲取 100 萬條記錄,並且我一次性完成,這將在許多地方引入很多問題。所以我們把它分解成更小的部分並執行更小但多個查詢,這將返回更少的行數。
  3. 使用者批處理器現在將接收這些批處理訊息並相應地執行資料庫查詢。接收到 {offset: 0, limit: 5} 訊息的處理器將從資料庫中獲取前 5 個使用者 ID(也會執行另一個批處理操作,但這裡很難解釋,所以跳過)。在此之後,我們將告別批處理並切換到流處理。因為批處理器現在會將 5 個使用者 ID 放入佇列中,由下一個處理器處理。
  4. 現在使用者分數計算器接收個人使用者 ID,執行評分邏輯來計算個人使用者分數。然後進行 1 DB update 修改使用者評分。然後它更新排序集中該特定使用者的分數,Redis 在內部分配或更新排名。一旦這個階段完成處理,我們將獲得我們資料庫中所有使用者的分數和我們 Redis 中所有使用者的排名 + 分數。並且由於我們有排序集中每個人的排名,我們可以對其執行範圍查詢,以極快的速度獲得排行榜。

如果我們的 Redis 叢集在任何時候出現故障,因為我們也在資料庫中維護了分數,我們可以隨時觸發該過程並在 Redis 上再次構建排序集。這是為了彈性。

 

我們的大部分問題現在都解決了,因為為了獲得使用者分數和排名,我們可以只進行 Redis 查詢而不訪問資料庫。這也使我們的響應時間更快。

主要設計階段到此結束。還有資料庫、API 設計和其他我在這裡跳過的東西。

 

實施

設計完成解決了我們 70% 的問題,我們知道我們可以解決這個問題,所以我們會迅速進入開發階段。

在理想的世界中,我會去建立一個新的服務,嘗試一種新的語言,比如Go,以獲得更好更快的並行處理和其他東西。但鑑於時間表,這不是正確的做法。我們堅持使用我們的核心 NodeJS 服務並將所有東西都放在那裡。微服務純粹主義者在看到此宣告後可能會失去它,但在與時間賽跑的過程中,您的負責人有時不得不退居二線。在眾多權衡中,這是我們採取的主要措施之一。

 

部署和監控

經過一些錯誤修復和 QA 驗收後,我們一切順利。工作做對了嗎?不,我的朋友,我們仍然必須為這件作品設定大量監控。因為它是在很短的時間內開發的,所以我至少有點不自信。預設情況下,我們通過LightStep在此服務上啟用了跟蹤。因此,除了跟蹤之外,我還為所有 API設定了對流量、錯誤率、延遲儀表板和警報的專門監控。上線後我和我的隊友,我們接到了一個電話,我們監視系統至少一個小時,從RAM 和 CPU 使用情況到錯誤日誌。所以總是給可觀察性同等的權重以及監控。生產系統存在一些小問題,我們只能通過監控及早發現它們。

 

相關文章