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