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