分布式系統(tǒng)的代碼檢視清單
微服務(wù)架構(gòu)是目前在軟件工程界廣泛采用的一種做法。 采用這種體系結(jié)構(gòu)樣式的組織發(fā)現(xiàn)自己正在處理分布式故障的增加的復(fù)雜性(除了實(shí)現(xiàn)業(yè)務(wù)邏輯的復(fù)雜性之外)。
分布式計(jì)算的謬論有據(jù)可查,但難以發(fā)現(xiàn)。 結(jié)果,構(gòu)建大規(guī)模,可靠的分布式系統(tǒng)架構(gòu)是一個(gè)難題。 作為必然的結(jié)果,當(dāng)我們向網(wǎng)絡(luò)中引入網(wǎng)絡(luò)交互的復(fù)雜性時(shí),在非分布式系統(tǒng)中看起來(lái)不錯(cuò)的代碼可能會(huì)成為一個(gè)巨大的問(wèn)題。
在生產(chǎn)代碼中遇到故障模式數(shù)年并根源導(dǎo)致它們進(jìn)入各種代碼位后,我(和許多其他人一樣)來(lái)確定一些更常見(jiàn)的故障模式。 這些在公司和語(yǔ)言堆棧之間略有不同(取決于內(nèi)部基礎(chǔ)結(jié)構(gòu)和工具的成熟度),但是其中一個(gè)或多個(gè)通常是導(dǎo)致生產(chǎn)問(wèn)題的原因。
這是一些代碼檢查指南,它們是我檢查與分布式環(huán)境中的系統(tǒng)間通信有關(guān)的代碼的基本清單。 并非所有這些方法始終都適用,但是它們都是非?;镜膯?wèn)題,因此我覺(jué)得機(jī)械地將此列表下標(biāo),將缺失的項(xiàng)目標(biāo)記為進(jìn)一步的討論很有用且令人放心。 從這個(gè)意義上講,這是一個(gè)愚蠢的清單,您可能始終希望遵循該清單。
調(diào)用遠(yuǎn)程系統(tǒng)時(shí),遠(yuǎn)程系統(tǒng)出現(xiàn)故障時(shí)會(huì)發(fā)生什么?
無(wú)論系統(tǒng)設(shè)計(jì)了多大的維護(hù),它都會(huì)在某些時(shí)候失效-這是在生產(chǎn)環(huán)境中運(yùn)行軟件的事實(shí)。 它可能由于錯(cuò)誤,某些基礎(chǔ)結(jié)構(gòu)問(wèn)題,流量突然激增或忽略的緩慢衰減而失敗,但失敗了。 呼叫者如何處理此故障將確定整個(gè)體系結(jié)構(gòu)的彈性和健壯性。
- 定義錯(cuò)誤處理路徑:在代碼中必須有明確定義的錯(cuò)誤處理路徑,而不僅僅是讓您的系統(tǒng)在最終用戶面前爆炸。 無(wú)論是設(shè)計(jì)合理的錯(cuò)誤頁(yè)面,具有錯(cuò)誤度量標(biāo)準(zhǔn)的異常日志,還是具有后備機(jī)制的斷路器,都必須明確地處理錯(cuò)誤。
- 制定恢復(fù)計(jì)劃:考慮代碼中的每個(gè)遠(yuǎn)程交互,并弄清楚我們需要做什么來(lái)恢復(fù)被中斷的工作。 我們的工作流程是否需要保持有狀態(tài),以便從故障點(diǎn)被觸發(fā)? 我們是否將所有失敗的有效負(fù)載發(fā)布到重試隊(duì)列/數(shù)據(jù)庫(kù)表,并在遠(yuǎn)程系統(tǒng)恢復(fù)運(yùn)行時(shí)重試它們? 我們是否有腳本來(lái)比較兩個(gè)系統(tǒng)的數(shù)據(jù)庫(kù)并以某種方式使其同步? 在部署實(shí)際代碼之前,應(yīng)實(shí)施并部署明確的,最好是系統(tǒng)的恢復(fù)計(jì)劃。
遠(yuǎn)程系統(tǒng)變慢時(shí)會(huì)發(fā)生什么?
這比完全失敗更隱患,因?yàn)槲覀儾恢肋h(yuǎn)程系統(tǒng)是否正常工作。 為了處理這種情況,應(yīng)始終檢查以下內(nèi)容。
始終在遠(yuǎn)程系統(tǒng)調(diào)用上設(shè)置超時(shí):這包括遠(yuǎn)程API調(diào)用,事件發(fā)布和數(shù)據(jù)庫(kù)調(diào)用上的超時(shí)。 我在太多的代碼中發(fā)現(xiàn)了這個(gè)簡(jiǎn)單的缺陷,以至于它同時(shí)令人震驚,但并非無(wú)法預(yù)料。 檢查是否為調(diào)用中的所有遠(yuǎn)程系統(tǒng)設(shè)置了有限且合理的超時(shí),以避免由于某種原因遠(yuǎn)程系統(tǒng)無(wú)響應(yīng)時(shí)在等待中浪費(fèi)資源。
- 超時(shí)重試:網(wǎng)絡(luò)和系統(tǒng)不可靠,重試是系統(tǒng)彈性的絕對(duì)必要條件。 重試通常會(huì)消除系統(tǒng)間交互中的許多"漏洞"。 如果可能,請(qǐng)?jiān)谥卦囍惺褂媚撤N退避(固定的,指數(shù)的)。 在重試機(jī)制上增加一點(diǎn)抖動(dòng)可以使呼吸變得有些呼吸。 如果負(fù)載很大,則將被調(diào)用的系統(tǒng)放置在房間中,可能會(huì)提高成功率。 重試的另一面是冪等,我們將在本文后面介紹。
- 使用斷路器:并沒(méi)有預(yù)包裝此功能的許多實(shí)現(xiàn),但是我看到公司在內(nèi)部編寫(xiě)自己的包裝器。 如果您有這種選擇,請(qǐng)一定要練習(xí)。 如果您不這樣做,請(qǐng)考慮投資建設(shè)它。 有一個(gè)定義良好的框架來(lái)定義發(fā)生錯(cuò)誤時(shí)的后備情況,這是一個(gè)很好的先例
- 不要像失敗一樣處理超時(shí)-超時(shí)不是失敗,而是不確定的情況,應(yīng)該以支持解決不確定性的方式進(jìn)行處理。 我們應(yīng)該建立明確的解決機(jī)制,使系統(tǒng)可以在發(fā)生超時(shí)的情況下保持同步。 范圍從簡(jiǎn)單的對(duì)帳腳本到有狀態(tài)的工作流再到死信隊(duì)列等等。
- 以可控制的方式使用批處理:如果要處理大量數(shù)據(jù),請(qǐng)進(jìn)行批處理遠(yuǎn)程調(diào)用(API調(diào)用,數(shù)據(jù)庫(kù)讀取),而不是一對(duì)一地進(jìn)行,以消除網(wǎng)絡(luò)開(kāi)銷(xiāo)。 但是請(qǐng)記住,批處理大小越大,總的延遲就越大,可能失敗的工作單元也就越大。 因此,優(yōu)化批處理以提高性能和容錯(cuò)能力。
在構(gòu)建系統(tǒng)時(shí),其他人將調(diào)用
- 所有API都必須是冪等的:這是重試API超時(shí)的另一面。 僅當(dāng)您的API安全重試且不會(huì)引起意外副作用時(shí),調(diào)用方才可以重試。 API是指同步API和任何消息傳遞接口-客戶端可以發(fā)布同一條消息兩次(或者代理可以發(fā)送兩次)。
- 明確定義響應(yīng)時(shí)間和吞吐量SLA,并遵循它們進(jìn)行編碼:在分布式系統(tǒng)中,快速失敗比讓呼叫者等待要好得多。 誠(chéng)然,吞吐量SLA難以實(shí)現(xiàn)(分布式速率限制本身很難解決),但是我們應(yīng)該認(rèn)識(shí)到我們的SLA,并規(guī)定如果要解決這些問(wèn)題,可以主動(dòng)使呼叫失敗。 另一個(gè)重要的方面是知道下游系統(tǒng)的響應(yīng)時(shí)間,以便您可以確定系統(tǒng)最快的速度。
- 定義和限制批處理API:如果要公開(kāi)批處理API,則最大批處理大小應(yīng)由我們希望的SLA明確定義和限制。 這是兌現(xiàn)SLA的必然結(jié)果。
- 事先考慮可觀察性:可觀察性意味著能夠分析系統(tǒng)的行為而不必關(guān)注系統(tǒng)內(nèi)部。 事先考慮一下您應(yīng)該收集有關(guān)系統(tǒng)的哪些指標(biāo)以及應(yīng)收集哪些數(shù)據(jù),這些數(shù)據(jù)將使您能夠回答以前未提出的問(wèn)題。 然后對(duì)系統(tǒng)進(jìn)行檢測(cè)以獲取此數(shù)據(jù)。 執(zhí)行此操作的強(qiáng)大機(jī)制是識(shí)別系統(tǒng)的域模型并在域中每次發(fā)生事件時(shí)發(fā)布事件(例如,接收到請(qǐng)求ID 123,返回請(qǐng)求123的響應(yīng)-請(qǐng)注意如何使用這兩個(gè)"域"事件 得出稱為"響應(yīng)時(shí)間"的新指標(biāo)。原始數(shù)據(jù)>>預(yù)先確定的匯總)。
一般準(zhǔn)則
- 主動(dòng)緩存:網(wǎng)絡(luò)是多變的,因此請(qǐng)盡可能多地緩存數(shù)據(jù),以盡可能接近數(shù)據(jù)的使用情況。 當(dāng)然,您的緩存機(jī)制也可以是遠(yuǎn)程的(例如,在另一臺(tái)計(jì)算機(jī)上運(yùn)行的Redis服務(wù)器),但是至少您可以將數(shù)據(jù)帶入您的控制域并減少其他系統(tǒng)的負(fù)載。
- 考慮故障單位:如果一個(gè)API或一條消息代表多個(gè)工作單元(批量),那么故障的單位是什么? 整個(gè)有效負(fù)載應(yīng)全部失敗一次,還是各個(gè)單元可以獨(dú)立成功或失敗。 在部分成功的情況下,API是否以成功或失敗代碼響應(yīng)?
- 在系統(tǒng)邊緣隔離外部域?qū)ο螅簭拈L(zhǎng)遠(yuǎn)來(lái)看,這是我看到的又一個(gè)麻煩。 我們不應(yīng)該以重用的名義在整個(gè)系統(tǒng)中使用其他系統(tǒng)的域?qū)ο蟆? 這將我們的系統(tǒng)與另一個(gè)系統(tǒng)對(duì)實(shí)體的建模耦合在一起,并且每次其他系統(tǒng)發(fā)生更改時(shí),我們都會(huì)進(jìn)行大量重構(gòu)。 我們應(yīng)該始終構(gòu)建自己的實(shí)體表示,并將外部有效負(fù)載轉(zhuǎn)換為該架構(gòu),然后在系統(tǒng)內(nèi)部使用它。
我希望您發(fā)現(xiàn)這些準(zhǔn)則有助于減少分布式系統(tǒng)代碼中最常見(jiàn)的錯(cuò)誤。 我想聽(tīng)聽(tīng)您是否認(rèn)為其他一些考慮因素很容易應(yīng)用但非常有效-我們可以在此處添加它們!