譯者 | 崔皓
策劃 | 云昭
分布式系統設計是一個難題,難就難在設計過程中是不會提供直接反饋的。往往有些問題的產生是來源于設計的,例如:可擴展性問題、彈性問題、數據問題。然而,通常的解決方案是治標不治本——僅僅對系統進行修補以使其保持運行,但是潛在的設計問題仍然存在,并且可能在不同的情況下再次爆發。當系統在生產環境中出現故障,再去分析與設計相關的根本原因就需要付出更多的努力,同時會引來大量的組織爭論。
和分布式系統代碼審查一樣,本文會給出簡單的清單,列出在審查分布式系統功能(多個系統協同工作)的設計時要注意的事項。
本文會從三個方面考慮分布式設計問題:一致性與可用性、域耦合以及可觀察性。前兩者經常相互泄漏,因為分布式系統就像一個復雜的網格,每個設計選擇都會影響其他多個事物。每個方面的問題都可以作為一個大主題來討論,因此以下指南代表了對任何設計審查的底線。
根據問題的用例對應上下文,檢查完這些基礎知識后,就可以針對研究特定方向進行深入研究了。相反,如果在檢查中發現問題,那就需要格外小心了。
一致性或可用性
需要提前聲明的是本文中的“系統”是指一組獨立系統,多個這樣的“系統”以不同的方式協作為用戶提供最終服務。一致性與可用性是針對多個協作系統而言的。
CPA定理告訴我們,我們可以根據系統的一致性、可用性和分區容錯性,然后選擇其中任意兩個。如果說分區容錯生活的一部分,也就是無法避免的。那么CAP 定理的真正選擇就會落到一致性和可用性之間了—也就是選擇“AP”系統(在分區容錯下的可用性)或者個“CP”系統(在分區容錯下的一致性)。
軟件架構的一個基本規則是所有軟件都會失敗。假設需要保持三個組件之間的一致性才能使設計的功能正常工作。這種做法會使得該功能變得脆弱,因為如果任何一個組件都有可能出現故障,一旦出現故障該功能就無法正常工作。此時我們需要面對“單點故障”問題了 ——實際上我們有三個有可能出現故障的組件!!!當我們努力保持使整個系統的一致性時,就越容易讓它在最輕微的影響下發生故障。保持同步的組件越多,這種情況就越糟糕。
幸運的是,對于這個問題是有解的。
在單個系統中,CP 和 AP是二元選擇的(例如 MySQL 是一致的,Cassandra 不是),也就是非此即彼的關系。但在分布式系統中,卻并非如此它們之間并不是非黑即白,而是存在灰度地帶。每個組件可能會保持一致性(訂單、庫存和付款),但整個系統角度來看可以設計成保持最終一致性。這種方式也給我們留有余地,從而增強系統的可用性。由此我們的指導方針就是設計整體系統的可用性,即便個別子系統存在不可用的情況,隨著時間的推移也可以實現整個系統的可用性。
使用異步消息進行通信
它幫我們消除一致性壓力的有力武器,異步消息通信的引入有利于將可用性和特性作為ReactiveManifesto的主要準則。考慮使組件之間的異步通信(通過消息代理傳遞消息),而不是直接使用請求-響應式 API 調用。如果說同步通信是硅谷的可卡因的話,那么同步API調用就是分布式系統設計的可卡因。可以思考一下——如果兩個系統不必保持一致,那么我們為什么要通過同步的方式立即完成調用呢?(正如同步通信模型所要求的那樣)。請求-響應模型創建了一種時間耦合形式(“立即服務這個請求!”)在調用者和被調用者之間,如果后者出現不可用的狀況,就會導致調用者的調用失敗,從而會導致調用者的級聯故障。異步通信允許被調用系統按照自己的節奏處理請求,從而減輕可用性的壓力。
雖然異步消息傳遞是一個強大的工具,但在采用它時必須牢記以下幾件事。
定義最低可接受的用戶體驗——對于每個最終用戶體驗,定義最低的一致的體驗。例如,用戶贏得了在線游戲,是否必須以全有或全無的方式記入獎勵積分、獎勵他在排行榜上的新位置、并通知他的所有朋友以及向他發送通知呢?很明顯,如果我們越能在核心、一致的體驗之外做更多的事情,遇到系統故障的可能性就越小。在討論需求時,必須對此毫不留情,并且支持它所需的最低要求——其他一切都應該通過異步完成。
通過這個示例,我們可以根據在線游戲的各個與用戶相關的環節進行拆解,并且進行設計。獎勵得分不一定在游戲結束的時候才給予,而是可以在游戲過程中通過異步的方式更新積分。同理:排行榜、和發送通知的功能也可以異步進行。這些東西需要在需求設計階段就定義好,只要在滿足用戶最低要求的游戲體驗的情況下,將各個游戲步驟進行拆解異步就好了。
保證最終一致性:無論單個用戶操作還是客戶端請求都可以跨多個組件修改數據,因此需要通過設計應保證所有系統將在某個指定時間對用戶請求達成共識——即使是通過分布式回滾的方式進行。
系統地保證 SLA 的一致性:它是非常有價值的,可能有一個最終一致性的計劃,但是如果沒有設置和執行設定時間框架的機制,就不可能從緩慢的處理中檢測到故障。由于我們無法確定事件在處理過程中是否引發錯誤或消息是否在網絡中丟失,因此需要通過明確的時間硬綁定來維持最終的一致性保證。
域耦合
好分布式系統設計會在正確的抽象級別將不同的事物進行拆分。拆分之后的分隔線被稱為域邊界,并且使用獨特的溝通語言和域特有的功能接口來對域邊界進行標識。例如,消息代理域會封裝消息、交付保證、存儲媒體等信息。支付域會封裝交易、支付網關等信息。
雖然這這里會提到到微服務中界限上下文的概念,但這一概念是廣泛適用的。例如,支付可以是一個域,其中支付網關和交易可以支付域的子域。
根據域設計的概念來進行分布式系統架構,其主要思想是在同一級別對域進行解耦,同時在父域級別建立內聚。例如,在上面的示例中,試圖讓支付網關和事務在實現時盡可能相互分離,不過作為同一域的一部分應該要求術語和數據模型保持一致性。
創建域邊界:隨著微服務采用率的大幅上升,通過底層技術分離的方式來識別組件域的方式就顯得尤為重要來。識別哪些組件屬于哪個域以及外部系統如何與這些系統通信也是很重要的。我們可能會使用多種服務來處理貨物(跟蹤服務、掃描服務、審計服務等),但外部用戶應該使用有聚合的“物流”域實體和API來訪問域中的功能組件。構建域邊界的最好工具是API 網關,它可以透過域本身抽象出更高級別API 背后的內部細節。
使用標準領域語言在系統之間進行通信:兩個組件之間的交流應該通過:現有實體 + 實體的狀態 +實體可能執行操作 的方式進行,不要去創建一些不屬于兩個域的構造然后讓它們進行通信。可以使用通信雙方的構造來實現這一點,這取決于我們想要事件還是消息。如果您發現兩個組件之間的通信需要創建某種特殊語言,那么就意味著組件的拆分方式存在問題,或者需要通過中間組件,該組件存在于這兩個組件之間,利用中間組件的方式讓兩個組件進行通信。
將多域/多組件操作分離到工作流中——有一種域耦合發生的常見方式,即當一個組件開始對多組件工作流進行端到端控制。這意味著當前域知道各種其他域、以及它們的行為和其邊界之外的“工作流”的性質。這種意識使組件與現有工作流耦合,因此難以擴展。
如果一個功能需要調用多個組件,應該將此功能從對應的域核心服務中分離出來,并將其劃為有狀態的組件。該組件可以應用到專用的編排服務或某種BPM 系統中。有狀態的組件意味著可以從一個地方獲得重試、錯誤報告、SLA等諸多信息。
如果我們不能對所有事情都使用顯式編排,使用編排來構建工作流也是一個可行的選擇,但需要使用某種方式來跟蹤任務完成SLA,以減少長期工作流的脆弱性。
跨組件建模業務流程:上述觀點的一個推論是,我們應該對業務流程進行端到端建模,而不應該考慮技術邊界。由于我們已經通過轉移編排到工作流的方式來解耦域,因此在狹窄的團隊邊界內構建工作流是沒有意義的。應該使工作流盡可能多地包含整個業務流程——這將建立業務知識的中央存儲庫,并提供對運營狀態的可見性。
事件優先與消息:解耦域更喜歡使用 pub-sub(發布訂閱) 模型的事件功能,而不是目標消息。雖然這取決于許多其他因素,但它增強了對其他域的不可知性,因為發布者不關心pub-sub 模型中的消費者,因此消除了發布者和消費者域之間的耦合。
可觀察性
用Charity Majors的話來說,可觀察性可以用來回答有關系統的新問題,同時無需窺視系統內部的能力。我認為可觀察性分為兩種:技術和業務。我們應該能夠解釋系統的技術狀態,并且可以通過業務指標來確定它的運行狀態。
使用事件數據來構建度量:任何系統的基本工作單元是事件,系統會用自己的領域語言來解釋事件。事件可以是任何類型(例如ORDER_CREATED、REQUEST_RECEIVED、ERROR_RESPONSE_RETURNED)。一個有追求的想法是,我們會將整個系統信息建模,并通過發送事件的方式與外部系統進行通訊,同時對事件進行保存和分析用以獲取更多的信息,而不是通過常見的日志跟蹤方式來獲取信息。擁有原始事件數據的好處是可以與領域建模方法同步,并且可以隨時使用原始數據并獲取新的指標。這比瀏覽非結構化文本日志、Spans、traces和任意Prometheus/Statsd 類型指標的組合要好得多。
將原始數據存儲在一個中心位置:遵守上面給出的可觀察性定義的唯一方法是存儲來自系統的原始數據,因為當我們開始只處理預定義的指標時,就被它們束縛而失去了回答“新”問題的能力。反對使用原始數據的一個常見觀點就是需要更多的容量存儲它們,但可以采取一些方法緩解容量的問題(例如:采樣等),反過來看原始數據的存儲在面對新的故障時,為系統提供的診斷能力就顯得無價了。在分布式系統中,將可觀察性數據集中存放顯然比存放在分布式孤島中要好的多,這樣更加利于提升對整體系統的洞察力。
一般準則
使用異步框架來實現——我之前寫過關于如何使用異步編程來擴展系統。因此,在調用遠程系統時,應該確保以異步方式進行,以免阻塞應用程序線程。這需要在早期的實現/設計中考慮,如果應用程序框架不允許這樣做,或者應用程序的基本框架不是為此而設計的,那么您就不走運了。如果您可以選擇異步,請始終接受它——當意外的流量激增時,您的系統和您的團隊會感謝您。
了解你的調用者——根據定義,沒有人會為分布式系統負責。因此,我們應該對內部系統采取盡可能多的預防措施,就像對外部系統采取的措施一樣。如果可以的話,其中至少要強制執行速率限制,同時要了解您的調用者。這將有助于在出現問題時,隔離問題的根源——當系統著火時使用才使用IP 地址識別源頭就太晚了。
知道何時失敗——我已經談了很多關于如何讓系統在不同條件下保持可用性的問題了。但在某些情況下,失敗總比做錯事要好。在許多情況下,一致性是完全可以接受的選擇,我們應該小心不要過度補償它們。
譯者介紹
崔皓,51CTO社區編輯,資深架構師,擁有18年的軟件開發和架構經驗,10年分布式架構經驗。曾任惠普技術專家。樂于分享,撰寫了很多熱門技術文章,閱讀量超過60萬。《分布式架構原理與實踐》作者。