方便的架構模式

嘿哈布爾!

鑑於目前由冠狀病毒引起的事件,許多網路服務的負載已開始增加。 例如, 英國一家零售連鎖店乾脆停止了其線上訂購網站。,因為容量不夠。 並且並不總是可以透過簡單地添加更強大的設備來加速伺服器,但必須處理客戶端請求(否則它們將流向競爭對手)。

在本文中,我將簡要討論一些流行的實踐,這些實踐將允許您創建快速且容錯的服務。 不過,從可能的開發方案中,我只選擇了目前正在開發的方案。 方便使用。 對於每個項目,您要么有現成的庫,要么有機會使用雲端平台解決問題。

水平縮放

最簡單也是最廣為人知的一點。 傳統上,最常見的兩種負載分配方案是水平縮放和垂直縮放。 在第一種情況下 您允許服務並行運行,從而在它們之間分配負載。 在第二個 您訂購更強大的伺服器或最佳化程式碼。

例如,我將採用抽象的雲端文件存儲,即 OwnCloud、OneDrive 等的一些類似物。

下面是此類電路的標準圖片,但它僅展示了系統的複雜性。 畢竟,我們需要以某種方式同步服務。 如果用戶從平板電腦儲存文件然後想從手機查看該文件,會發生什麼情況?

方便的架構模式
方法之間的差異:在垂直擴展中,我們準備增加節點的能力,而在水平擴展中,我們準備新增節點來分配負載。

連續QRS

命令查詢職責分離 這是一個相當重要的模式,因為它不僅允許不同的客戶端連接到不同的服務,而且還允許接收相同的事件流。 對於簡單的應用程式來說,它的好處並不那麼明顯,但對於繁忙的服務來說,它極其重要(而且簡單)。 其本質是:傳入和傳出的資料流不應相交。 也就是說,您不能發送請求並期望得到回應;相反,您會向服務 A 發送請求,但收到來自服務 B 的回應。

這種方法的第一個好處是能夠在執行長請求時中斷連線(廣義的連線)。 例如,讓我們採取一個或多或少標準的序列:

  1. 客戶端向伺服器發送請求。
  2. 伺服器啟動處理時間較長。
  3. 伺服器將結果回應給客戶端。

讓我們想像一下,在第 2 點,連接被中斷(或網路重新連接,或使用者轉到另一個頁面,從而中斷了連接)。 在這種情況下,伺服器將很難向使用者發送包含有關具體處理內容的資訊的回應。 使用 CQRS,順序會略有不同:

  1. 客戶端已訂閱更新。
  2. 客戶端向伺服器發送請求。
  3. 伺服器回應「請求已接受」。
  4. 伺服器從點“1”通過通道回應結果。

方便的架構模式

正如您所看到的,該方案稍微複雜一些。 此外,這裡缺少直覺的請求-回應方法。 但是,正如您所看到的,處理請求時連線中斷不會導致錯誤。 此外,如果實際上使用者從多個裝置(例如,從行動電話和平板電腦)連接到服務,您可以確保回應到達兩個裝置。

有趣的是,對於受客戶端本身影響的事件和其他事件(包括來自其他客戶端的事件),處理傳入訊息的程式碼變得相同(不是 100%)。

然而,實際上我們得到了額外的好處,因為單向流可以以函數式的方式處理(使用 RX 和類似的)。 這已經是一個重要的優點,因為本質上應用程式可以完全響應式,並且還可以使用函數式方法。 對於胖程式來說,這可以顯著節省開發和支援資源。

如果我們將這種方法與水平擴展結合起來,那麼作為獎勵,我們就能夠向一台伺服器發送請求並從另一台伺服器接收回應。 這樣,客戶端就可以選擇對他來說方便的服務,而內部的系統仍然能夠正確地處理事件。

事件溯源

如您所知,分散式系統的主要特徵之一是沒有公共時間、公共臨界區。 對於一個進程,您可以進行同步(在相同的互斥體上),在同步過程中您可以確保沒有其他人正在執行此程式碼。 然而,這對於分散式系統來說是危險的,因為它需要開銷,並且還會破壞擴展的所有優點 - 所有元件仍將等待一個。

從這裡我們得到一個重要的事實──快速的分散式系統無法同步,因為那樣我們就會降低效能。 另一方面,我們常常需要元件之間有一定的一致性。 為此,您可以使用該方法 最終一致性,其中保證如果在上次更新後的一段時間內(“最終”)沒有資料更改,則所有查詢都將返回上次更新的值。

重要的是要理解,對於經典資料庫來說,它經常被使用 強一致性,其中每個節點都具有相同的資訊(這通常是在只有第二個伺服器回應後才認為事務已建立的情況下實現的)。 由於隔離等級的原因,這裡有一些放鬆,但總體思路保持不變 - 您可以生活在一個完全和諧的世界中。

不過,讓我們回到最初的任務。 如果系統的一部分可以用 最終一致性,那我們可以構造下圖。

方便的架構模式

這種方法的重要特點:

  • 每個傳入請求都放置在一個佇列中。
  • 在處理請求時,服務也可以將任務放入其他佇列中。
  • 每個傳入事件都有一個識別符(這是重複資料刪除所必需的)。
  • 隊列在思想上按照「僅附加」方案工作。 您無法從中刪除元素或重新排列它們。
  • 隊列按照 FIFO 方案工作(抱歉是同義反覆)。 如果您需要並行執行,那麼在某一階段您應該將物件移動到不同的佇列。

讓我提醒您,我們正在考慮線上文件儲存的情況。 在這種情況下,系統將如下所示:

方便的架構模式

重要的是,圖中的服務不一定意味著單獨的伺服器。 甚至過程也可能是一樣的。 另一件重要的事情是:從意識形態上來說,這些東西是分開的,以便可以輕鬆應用水平擴展。

對於兩個用戶,該圖將如下所示(針對不同用戶的服務以不同的顏色表示):

方便的架構模式

這種組合的獎金:

  • 資訊處理服務是分開的。 隊列也是分開的。 如果我們需要提高系統吞吐量,那麼我們只需要在更多伺服器上啟動更多服務。
  • 當我們接收來自用戶的資訊時,我們不必等到資料完全保存。 相反,我們只需要回答“好”,然後逐步開始工作。 同時,佇列可以消除峰值,因為新增物件的速度很快,而且使用者不必等待整個週期的完整通過。
  • 例如,我新增了一個嘗試合併相同檔案的重複資料刪除服務。 如果它在 1% 的情況下長時間工作,客戶幾乎不會注意到它(見上文),這是一個很大的優勢,因為我們不再要求 XNUMX% 的速度和可靠性。

然而,缺點也是顯而易見的:

  • 我們的系統已經失去了嚴格的一致性。 這意味著,例如,如果您訂閱了不同的服務,那麼理論上您可以獲得不同的狀態(因為其中一項服務可能沒有時間從內部隊列接收通知)。 另一個結果是,系統現在沒有公共時間。 也就是說,例如,不可能簡單地按到達時間對所有事件進行排序,因為伺服器之間的時鐘可能不同步(此外,兩個伺服器上的相同時間是烏托邦)。
  • 現在不能簡單地回滾任何事件(就像使用資料庫可以完成的那樣)。 相反,您需要添加一個新事件 - 補償事件,這會將最後一個狀態變更為所需的狀態。 舉一個類似領域的例子:如果不重寫歷史記錄(這在某些情況下很糟糕),你就不能回滾 git 中的提交,但你可以做一個特殊的 回滾提交,它本質上只是返回舊狀態。 然而,錯誤的提交和回滾都將留在歷史中。
  • 資料模式可能會因版本而異,但舊事件將不再能夠更新到新標準(因為原則上事件無法更改)。

如您所見,事件溯源與 CQRS 配合良好。 此外,實現一個具有高效且方便的隊列但不分離資料流的系統本身就已經很困難,因為您必須添加同步點,這將抵消隊列的整體積極作用。 同時應用這兩種方法,需要稍微調整程式碼。 在我們的例子中,當向伺服器發送檔案時,回應僅是“ok”,這僅表示“新增檔案的操作已儲存”。 從形式上來說,這並不意味著資料在其他裝置上已經可用(例如,重複資料刪除服務可以重建索引)。 然而,一段時間後,客戶端將收到「檔案 X 已儲存」樣式的通知。

因此:

  • 文件發送狀態的數量正在增加:不再是經典的“文件已發送”,而是兩個:“文件已添加到伺服器上的隊列”和“文件已保存在存儲中”。 後者意味著其他設備已經可以開始接收檔案(根據隊列以不同速度運行的事實進行調整)。
  • 由於現在提交資訊來自不同的管道,我們需要想出解決方案來接收文件的處理狀態。 結果是:與經典的請求-回應不同,客戶端可以在處理文件時重新啟動,但此處理本身的狀態將是正確的。 此外,該產品本質上是開箱即用的。 結果是:我們現在對失敗更寬容。

拆分

如上所述,事件溯源系統缺乏嚴格的一致性。 這意味著我們可以使用多個存儲,而無需它們之間進行任何同步。 解決我們的問題,我們可以:

  • 按類型分隔文件。 例如,可以對圖片/影片進行解碼並可以選擇更有效的格式。
  • 按國家/地區分開帳戶。 由於許多法律,這可能是必要的,但是這種架構方案自動提供了這樣的機會

方便的架構模式

如果您想將資料從一個存儲傳輸到另一個存儲,那麼標準方法已經不夠了。 不幸的是,在這種情況下,您需要停止佇列,進行遷移,然後啟動它。 在一般情況下,資料不能「即時」傳輸,但是,如果事件佇列已完全存儲,並且您有先前儲存狀態的快照,那麼我們可以按如下方式重播事件:

  • 在事件來源中,每個事件都有自己的標識符(理想情況下是非遞減的)。 這意味著我們可以向存儲添加一個字段 - 最後處理的元素的 id。
  • 我們複製佇列,以便可以為多個獨立存儲處理所有事件(第一個是已儲存資料的存儲,第二個是新的,但仍然是空的)。 當然,第二個隊列還沒有被處理。
  • 我們啟動第二個佇列(即,我們開始重播事件)。
  • 當新佇列相對空時(即新增元素和檢索元素之間的平均時間差可以接受),您可以開始將讀取器切換到新儲存。

正如您所看到的,我們的系統過去沒有、現在仍然沒有嚴格的一致性。 只有最終的一致性,即保證事件以相同的順序處理(但可能有不同的延遲)。 而且,使用它,我們可以相對輕鬆地將資料傳輸到地球的另一端,而無需停止系統。

因此,繼續我們關於文件在線存儲的示例,這樣的架構已經為我們帶來了許多好處:

  • 我們可以以動態的方式將物件移近使用者。 這樣您就可以提高服務品質。
  • 我們可能會在公司內部儲存一些資料。 例如,企業用戶通常要求將其資料儲存在受控資料中心(以避免資料外洩)。 透過分片,我們可以輕鬆支持這一點。 如果客戶擁有相容的雲端(例如, Azure 自我託管).
  • 最重要的是我們不必這麼做。 畢竟,首先,我們會很高興為所有帳戶提供一個儲存(以便快速開始工作)。 該系統的主要特點是雖然具有可擴展性,但在初始階段卻相當簡單。 您只是不必立即編寫與一百萬個單獨的獨立隊列等一起使用的程式碼。 如果有必要,將來可以這樣做。

靜態內容託管

這一點看起來似乎很明顯,但對於或多或少標準加載的應用程式來說,它仍然是必要的。 其本質很簡單:所有靜態內容不是從應用程式所在的同一台伺服器分發的,而是從專門致力於此任務的特殊伺服器分發的。 因此,這些操作執行得更快(條件 nginx 比 Java 伺服器更快提供檔案且更便宜)。 加上CDN架構(內容交付網絡)使我們能夠將文件定位到更靠近最終用戶的位置,這對使用該服務的便利性產生積極影響。

靜態內容最簡單、最標準的範例是網站的一組腳本和圖像。 對於它們來說,一切都很簡單 - 它們是預先知道的,然後將存檔上傳到 CDN 伺服器,從那裡將它們分發給最終用戶。

然而,實際上,對於靜態內容,您可以使用類似於 lambda 架構的方法。 讓我們回到我們的任務(線上文件儲存),其中我們需要將文件分發給用戶。 最簡單的解決方案是建立一個服務,針對每個使用者請求,執行所有必要的檢查(授權等),然後直接從我們的儲存下載檔案。 這種方法的主要缺點是靜態內容(經過一定修訂的文件實際上是靜態內容)由包含業務邏輯的相同伺服器分發。 相反,您可以製作下圖:

  • 伺服器提供下載 URL。 它可以採用 file_id + key 的形式,其中 key 是一個迷你數位簽名,授予在接下來的 XNUMX 小時內存取資源的權利。
  • 該檔案由簡單的 nginx 分發,具有以下選項:
    • 內容快取。 由於此服務可以位於單獨的伺服器上,因此我們為將來保留了將所有最新下載的檔案儲存在磁碟上的能力。
    • 建立連線時檢查密鑰
  • 可選:流內容處理。 例如,如果我們壓縮服務中的所有文件,那麼我們可以直接在該模組中進行解壓縮。 結果是:IO 操作在它們所屬的地方完成。 Java 中的歸檔器很容易分配大量額外內存,但將具有業務邏輯的服務重寫為 Rust/C++ 條件也可能無效。 在我們的例子中,使用了不同的流程(甚至服務),因此我們可以非常有效地分離業務邏輯和 IO 操作。

方便的架構模式

這種方案與分發靜態內容不太相似(因為我們不會將整個靜態包上傳到某個地方),但實際上,這種方法恰好涉及分發不可變資料。 此外,該方案可以推廣到其他情況,其中內容不僅僅是靜態的,而是可以表示為一組不可變和不可刪除的區塊(儘管可以添加它們)。

另一個例子(為了強化):如果您使用過 Jenkins/TeamCity,那麼您知道這兩個解決方案都是用 Java 編寫的。 它們都是一個 Java 進程,可以處理建置編排和內容管理。 特別是,它們都有諸如“從伺服器傳輸文件/資料夾”之類的任務。 舉個例子:發布工件、傳輸原始碼(當代理程式不直接從儲存庫下載程式碼,而是伺服器為他下載時)、存取日誌。 所有這些任務的 IO 負載都不同。 也就是說,事實證明,負責複雜業務邏輯的伺服器必須同時能夠透過自身有效地推送大量資料。 最有趣的是,這樣的操作可以根據完全相同的方案委託給同一個 nginx(除了資料金鑰應該要加入請求中)。

然而,如果我們回到我們的系統,我們會得到一個類似的圖表:

方便的架構模式

正如您所看到的,系統變得更加複雜。 現在它不僅僅是一個在本地儲存檔案的迷你進程。 現在需要的不是最簡單的支援、API版本控制等。 因此,在繪製完所有圖表之後,最好詳細評估可擴展性是否值得為此付出代價。 但是,如果您希望能夠擴展系統(包括與更多數量的用戶一起工作),那麼您將不得不採用類似的解決方案。 但是,因此,系統在架構上已準備好增加負載(幾乎每個組件都可以克隆以進行水平擴展)。 系統可以在不停止的情況下更新(只是某些操作會稍微變慢)。

正如我一開始所說,現在一些網路服務的負載已經開始增加。 其中一些根本就開始停止正常運作。 事實上,正是在企業該賺錢的時候,系統卻出現了故障。 也就是說,系統不會推遲交貨,也不會建議客戶“計劃未來幾個月的交貨”,而是簡單地說“去找你的競爭對手”。 事實上,這就是低生產力的代價:利潤最高的時候卻恰恰發生了損失。

結論

所有這些方法以前都是已知的。 同一個VK長期以來一直在使用靜態內容託管的想法來顯示圖像。 許多線上遊戲都使用分片方案將玩家劃分為區域或分隔遊戲位置(如果世界本身就是一個)。 事件溯源方法在電子郵件中被積極使用。 大多數不斷接收資料的交易應用程式實際上都是基於 CQRS 方法建構的,以便能夠過濾接收到的資料。 嗯,水平擴展已經在許多服務中使用了相當長的時間。

然而,最重要的是,所有這些模式都變得非常容易在現代應用程式中應用(當然,如果它們合適的話)。 雲端立即提供分片和水平擴展,這比您自己在不同資料中心訂購不同的專用伺服器要容易得多。 如果僅僅因為 RX 等庫的開發,CQRS 就變得更加容易。 大約十年前,很少有網站可以支持這一點。 由於採用 Apache Kafka 的現成容器,事件溯源也非常容易設定。 10年前,這可能是一項創新,但現在這已經司空見慣了。 靜態內容託管也是如此:由於更方便的技術(包括有詳細的文檔和龐大的答案資料庫),這種方法變得更加簡單。

因此,許多相當複雜的架構模式的實作現在變得更加簡單,這意味著最好事先仔細研究一下。 如果在一個已有十年歷史的應用程式中,上述解決方案之一由於實施和運營成本高昂而被放棄,那麼現在,在一個新的應用程式中,或者在重構之後,您可以創建一個在架構上已經可擴展的服務(效能方面)並可滿足客戶的新要求(例如,本地化個人資料)。

最重要的是:如果您有一個簡單的應用程序,請不要使用這些方法。 是的,它們很漂亮而且很有趣,但是對於一個峰值訪問量為 100 人的網站,您通常可以使用經典的整體架構(至少在外部,內部的所有內容都可以分為模組等)。

來源: www.habr.com

添加評論