分散式應用程式的建構塊。 第二次近似

公告

同事們,仲夏時節,我計劃發布另一系列關於排隊系統設計的文章:《VTrade 實驗》——嘗試編寫一個交易系統框架。 該系列將探討建立交易所、拍賣和商店的理論和實踐。 在文章的最後,我邀請您為您最感興趣的主題投票。

分散式應用程式的建構塊。 第二次近似

這是 Erlang/Elixir 分散式反應式應用程式系列中的最後一篇文章。 在 第一篇文章 您可以找到反應式架構的理論基礎。 第二條 說明了建構此類系統的基本模式和機制。

今天我們將提出程式碼庫和專案的整體開發問題。

服務組織

在現實生活中,在開發一項服務時,您通常必須在一個控制器中組合多種互動模式。 例如,解決管理專案使用者設定檔問題的使用者服務必須回應 req-resp 請求並透過 pub-sub 報告設定檔更新。 這種情況非常簡單:訊息傳遞背後有一個控制器來實現服務邏輯並發布更新。

當我們需要實作容錯的分散式服務時,情況會變得更加複雜。 假設用戶的需求改變了:

  1. 現在服務應該處理 5 個叢集節點上的請求,
  2. 能夠執行後台處理任務,
  3. 並且還能夠動態管理個人資料更新的訂閱清單。

備註: 我們不考慮一致性儲存和資料複製的問題。 我們假設這些問題已經得到了較早的解決,並且系統已經具有可靠且可擴展的儲存層,並且處理程序具有與其互動的機制。

用戶服務的正式描述變得更加複雜。 從程式設計師的角度來看,由於訊息傳遞的使用,變化很小。 為了滿足第一個要求,我們需要在 req-resp 交換點配置平衡。

處理後台任務的需求經常出現。 對於用戶來說,這可能是檢查用戶文件、處理下載的多媒體或與社交媒體同步資料。 網路。 這些任務需要以某種方式分佈在叢集內並監視執行進度。 因此,我們有兩個解決方案選項:要么使用上一篇文章中的任務分配模板,或者如果不適合,則編寫一個自訂任務調度程序,它將按照我們需要的方式管理處理器池。

第 3 點需要 pub-sub 模板擴充。 為了實現,在創建 pub-sub 交換點後,我們需要在我們的服務中額外啟動該點的控制器。 因此,就好像我們正在將處理訂閱和取消訂閱的邏輯從訊息傳遞層轉移到用戶的實作中。

結果,問題的分解表明,為了滿足要求,我們需要在不同的節點上啟動5個服務實例,並創建一個額外的實體——一個pub-sub控制器,負責訂閱。
要執行 5 個處理程序,您不需要更改服務代碼。 唯一的額外操作是在交換點設定平衡規則,我們稍後會討論。
還有一個額外的複雜性:發布-訂閱控制器和自訂任務調度程序必須在單一副本中工作。 同樣,訊息服務作為基礎服務,必須提供一種選擇領導者的機制。

領導者的選擇

在分散式系統中,領導者選舉是指定單一程序負責調度某些負載的分散式處理的過程。

在不易中心化的系統中,會使用通用且基於共識的演算法,例如 paxos 或 raft。
由於訊息傳遞是一個代理和一個中心元素,因此它了解所有服務控制器 - 候選領導者。 訊息可以指定領導者而無需投票。

啟動並連接到交換點後,所有服務都會收到系統訊息 #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}。 如果 LeaderPid 符合 pid 當前流程,被指定為leader,列表 Servers 包括所有節點及其參數。
當新的節點出現並且工作叢集節點斷開連接時,所有服務控制器都會收到 #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} и #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} 分別

這樣,所有元件都知道所有更改,並且保證叢集在任何給定時間都有一個領導者。

中介機構

為了實現複雜的分散式處理流程,以及最佳化現有架構的問題,使用中介很方便。
為了不更改服務代碼並解決其他處理、路由或記錄訊息等問題,您可以在服務之前啟用代理處理程序,它將執行所有其他工作。

pub-sub 最佳化的經典範例是分散式應用程序,其業務核心產生更新事件(例如市場價格變化),以及存取層 - N 個伺服器為 Web 用戶端提供 websocket API。
如果你正面決定,那麼客戶服務看起來像這樣:

  • 客戶端與平台建立連線。 在終止流量的伺服器端,啟動一個程序來服務該連線。
  • 在服務進程的上下文中,發生更新的授權和訂閱。 該過程調用主題的訂閱方法。
  • 一旦在核心中產生事件,它就會被傳遞到為連線提供服務的進程。

假設我們有 50000 個「新聞」主題的訂閱者。 訂閱者均勻分佈在 5 台伺服器上。 因此,到達交換點的每個更新都將複製 50000 次:根據伺服器上的訂閱者數量,在每台伺服器上複製 10000 次。 這不是一個非常有效的計劃,對嗎?
為了改善這種情況,我們引入一個與交換點同名的代理。 全域名稱註冊器必須能夠按名稱返回最接近的進程,這一點很重要。

讓我們在訪問層伺服器上啟動這個代理,所有服務於 websocket api 的進程都將訂閱它,而不是核心中原始的 pub-sub 交換點。 代理僅在唯一訂閱的情況下訂閱核心,並將傳入訊息複製到其所有訂閱者。
結果,核心和存取伺服器之間將發送 5 個訊息,而不是 50000 條。

路由和平衡

請求-回應

在目前的訊息傳遞實作中,有7種請求分發策略:

  • default。 該請求被傳送到所有控制器。
  • round-robin。 請求被枚舉並在控制器之間循環分發。
  • consensus。 提供服務的控制器分為領導者和從者。 請求僅發送給領導者。
  • consensus & round-robin。 該組有一名領導者,但請求會分發給所有成員。
  • sticky。 計算雜湊函數並將其指派給特定的處理程序。 具有此簽章的後續請求將轉到相同的處理程序。
  • sticky-fun。 初始化交換點時,哈希計算函數為 sticky 平衡。
  • fun。 與 Sticky-fun 類似,只有您可以額外重定向、拒絕或預處理它。

分配策略是在交換點初始化時設定的。

除了平衡之外,訊息傳遞還允許您標記實體。 我們來看看系統中的標籤類型:

  • 連接標記。 讓您了解事件是透過哪個連接發生的。 當控制器程序連接到同一交換點但具有不同的路由鍵時使用。
  • 服務標籤。 允許您將處理程序組合成一項服務的群組,並擴展路由和平衡功能。 對於 req-resp 模式,路由是線性的。 我們向交換點發送請求,然後交換點將其傳遞給服務。 但是,如果我們需要將處理程序拆分為邏輯組,則可以使用標籤來完成拆分。 當指定標籤時,請求將被傳送到特定的一組控制器。
  • 請求標籤。 允許您區分答案。 由於我們的系統是非同步的,為了處理服務回應,我們需要能夠在發送請求時指定 RequestTag。 從中我們將能夠了解我們收到的請求的答案。

發布-訂閱

對於發布-訂閱,一切都稍微簡單一些。 我們有一個發布訊息的交換點。 交換點在訂閱了所需路由金鑰的訂閱者之間分發訊息(我們可以說這類似於主題)。

可擴充性和容錯性

系統整體的可擴展性取決於系統各層和組件的可擴展程度:

  • 透過向叢集添加額外的節點以及該服務的處理程序來擴展服務。 試運轉時,您可以選擇最優的平衡策略。
  • 單獨叢集內的消息傳遞服務本身通常透過將特別負載的交換點移動到單獨的叢集節點,或透過向叢集的特別負載的區域新增代理程式來擴展。
  • 整個系統的可擴展性作為一個特徵取決於架構的靈活性以及將各個集群組合成公共邏輯實​​體的能力。

專案的成功通常取決於擴充的簡單性和速度。 目前版本中的消息傳遞隨著應用程式的發展而增長。 即使我們缺少 50-60 台機器的集群,我們也可以訴諸聯邦。 不幸的是,聯合主題超出了本文的範圍。

預訂

在分析負載平衡時,我們已經討論過服務控制器的冗餘。 然而,訊息傳遞也必須保留。 如果節點或機器崩潰,訊息傳遞應該在盡可能短的時間內自動恢復。

在我的專案中,我使用額外的節點來承受跌倒時的負荷。 Erlang 為 OTP 應用程式提供了標準的分散式模式實作。 分散式模式透過在另一個先前啟動的節點上啟動失敗的應用程式來在發生故障時執行復原。 該過程是透明的;發生故障後,應用程式會自動轉移到故障轉移節點。 您可以閱讀有關此功能的更多信息 這裡.

Производительность

讓我們嘗試至少粗略地比較rabbitmq 和我們自訂訊息傳遞的效能。
我發現 官方結果 來自 openstack 團隊的rabbitmq 測試。

在第 6.14.1.2.1.2.2 段。 原始文件顯示了 RPC CAST 的結果:
分散式應用程式的建構塊。 第二次近似

我們不會提前對作業系統內核或erlang VM進行任何額外的設定。 測試條件:

  • erl 選擇:+A1 +sbtu。
  • 單一 erlang 節點內的測試是在行動版本帶有舊 i7 的筆記型電腦上執行的。
  • 集群測試在10G網路的伺服器上進行。
  • 該程式碼在 Docker 容器中運行。 網路採用NAT模式。

測試程式碼:

req_resp_bench(_) ->
  W = perftest:comprehensive(10000,
    fun() ->
      messaging:request(?EXCHANGE, default, ping, self()),
      receive
        #'$msg'{message = pong} -> ok
      after 5000 ->
        throw(timeout)
      end
    end
  ),
  true = lists:any(fun(E) -> E >= 30000 end, W),
  ok.

場景一: 測試在舊版 i7 行動版筆記型電腦上運行。 測試、訊息傳遞和服務在一個 Docker 容器的一個節點上執行:

Sequential 10000 cycles in ~0 seconds (26987 cycles/s)
Sequential 20000 cycles in ~1 seconds (26915 cycles/s)
Sequential 100000 cycles in ~4 seconds (26957 cycles/s)
Parallel 2 100000 cycles in ~2 seconds (44240 cycles/s)
Parallel 4 100000 cycles in ~2 seconds (53459 cycles/s)
Parallel 10 100000 cycles in ~2 seconds (52283 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (49317 cycles/s)

場景2:3個節點在docker(NAT)下運作在不同的機器上。

Sequential 10000 cycles in ~1 seconds (8684 cycles/s)
Sequential 20000 cycles in ~2 seconds (8424 cycles/s)
Sequential 100000 cycles in ~12 seconds (8655 cycles/s)
Parallel 2 100000 cycles in ~7 seconds (15160 cycles/s)
Parallel 4 100000 cycles in ~5 seconds (19133 cycles/s)
Parallel 10 100000 cycles in ~4 seconds (24399 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (34517 cycles/s)

在所有情況下,CPU 使用率均不超過 250%

結果

我希望這個週期看起來不像是思想轉儲,我的經驗將對分散式系統的研究人員和剛開始為其業務系統建立分散式架構並對Erlang/Elixir 感興趣的從業者帶來真正的好處,但有疑問是否值得...

照片 @chuttersnap

只有註冊用戶才能參與調查。 登入, 請。

作為 VTrade 實驗系列的一部分,我應該更詳細地介紹哪些主題?

  • 理論:市場、訂單及其時間:DAY、GTD、GTC、IOC、FOK、MOO、MOC、LOO、LOC

  • 訂單書。 實現一本書分組的理論與實踐

  • 交易視覺化:價格變動、柱線、解析度。 如何儲存和如何黏合

  • 後台。 規劃和發展。 員工監控和事件調查

  • API。 讓我們弄清楚需要哪些介面以及如何實現它們

  • 資訊儲存:交易系統中的 PostgreSQL、Timescale、Tarantool

  • 交易系統的反應性

  • 其他。 我會寫在評論裡

6 位用戶投票。 4 名用戶棄權。

來源: www.habr.com

添加評論