作者:Nicolas Frankel
譯者:羅廣明
原文:www.exoscale.com/syslog/isti…
編者按
本文作者由淺及深,從核心問題的引入到具體模式的程式碼實現,闡述了微服務兩種斷路器模式的實現原理、優缺點以及二者的比較。
前言
不可否認的是,在過去的幾年裡,Docker和Kubernetes等技術已經徹底改變了我們對軟體開發和部署的理解。
但是,儘管軟體開發行業的快速發展促使開發人員採用最新的技術,但是後退一步,更好地檢視支援這些技術的已建立的模式是很重要的。
斷路器模式是微服務體系結構中廣泛採用的模式之一。我們將比較使用兩種不同方法實現它的優缺點: Hystrix和Istio。
微服務同步通訊的核心問題
設想一個非常簡單的微服務體系結構,包括:
一個後端服務
一個前端服務
我們假設後端和前端通過同步HTTP呼叫進行通訊。客戶端C1
和 C2
呼叫前端獲取一些資訊。由於前端沒有客戶端所需的所有資料,因此它呼叫後端以獲得缺失的部分資料。但是因為網路通訊,很多事情會發生:
- 前端和後端之間的網路故障
- 後端可能會因為錯誤而當機
一個被後端依賴的服務(e.g.資料庫)可能當機
根據墨菲定律(“任何可能出錯的都會出錯”),前端和後端之間的通訊遲早會失敗。
如果我們研究從前端到後端單個呼叫的生命週期,並考慮後端由於某種原因當機,那麼在某個時候,前端將因超時取消呼叫。
將範圍縮小到應用程式級別,多個客戶機同時呼叫前端,這將轉換為對後端的多個呼叫: 前端將很快被請求淹沒,並淹沒在超時中。
在這個場景中,唯一合理的解決方案是fail-fast: 前端應該意識到後端出現了問題,並立即將故障返回給自己的客戶端。
斷路器模式
在電路領域中,斷路器是為保護電路而設計的一種自動操作的電氣開關。它的基本功能是在檢測到故障後中斷電流。然後可以重置(手動或自動),以在故障解決後恢復正常操作。
這看起來與我們的問題非常相似: 為了保護應用程式不受過多請求的影響,最好在
如果呼叫失敗,將失敗呼叫的數量增加1
如果呼叫失敗次數超過某個閾值,則開啟電路
如果電路開啟,立即返回錯誤或預設響應
如果電路是開啟的,過了一段時間,半開啟電路
如果電路是半開的,下一個呼叫失敗,再開啟它
如果電路是半開的,下一個呼叫成功,關閉它
這可以用下圖來總結:
Istio斷路器
Istio是一個服務網格(Service Mesh),微服務應用程式的可配置基礎結構層。它使服務例項之間的通訊靈活、可靠和快速,並提供服務發現、負載平衡、加密、身份驗證和授權、對斷路器模式的支援等功能。
Istio的控制平面在底層叢集管理平臺(如Kubernetes、Mesos等)上提供了一個抽象層,並要求以這種方式管理應用程式。
作為其核心,Istio由位於應用程式例項前面的Envoy代理例項組成,並且使用了sidecar容器模式和Pilot(一個管理它們的工具)。這種代理策略有很多優點:
自動為HTTP, gRPC, WebSocket和TCP流量做負載平衡。
通過豐富的路由規則、重試、失敗和錯誤注入對流量行為進行細粒度控制。
可插入的策略層和配置API,支援訪問控制、速率限制和配額。
一個叢集內所有流量的自動度量、日誌和跟蹤,包括叢集的加入和退出。
在具有強大的身份驗證和授權的叢集中進行安全的服務間通訊。
因為對
的出站呼叫通過Envoy代理,所以很容易檢測到它們何時超時。然後代理可以攔截進一步的呼叫並立即返回,從而有效地執行fail-fast。特別地,這使得斷路器模式能夠以黑箱方式執行。
配置Istio斷路器
正如我們所說,Istio構建在您選擇的叢集管理平臺上,並要求應用程式在這個平臺部署。Kubernetes通過DestinationRule
實現斷路器模式,或者更具體的路徑TrafficPolicy
(原斷路器
)->OutlierDetection
,根據以下模型:
引數如下:
域 | 描述 |
---|---|
consecutiveErrors | 斷路器開啟前的出錯次數。 |
interval | 斷路器檢查分析的時間間隔。 |
baseEjectionTime | 最小的開放時間。該電路將保持一段時間,等於最小彈射持續時間和電路已開啟的次數的乘積。 |
maxEjectionPercent | 可以彈出的上游服務的負載平衡池中主機的最大百分比。 |
與上述公稱斷路器相比,有兩個主要偏差:
沒有半開放的狀態。然而,斷路器持續開啟的時間取決於被呼叫服務之前失敗的次數。持續的故障服務將導致斷路器的開路時間越來越長。
在基本模式中,只有一個被呼叫的應用程式(
後端)。在更實際的生產環境中,負載均衡器後面可能部署同一個應用程式的多個例項。某些情況下可能會失敗,而有些人可能會工作,因為Istio也是負載平衡器的作用,能夠跟蹤失敗的,把他們從負載均衡池,在一定程度上:maxEjectionPercent
屬性的作用是保持一小部分的例項池。
Istio實現斷路器的方法是一種黑盒方法。它的視角很高,只有出了問題才能開啟電路。另一方面,它的設定非常簡單,不需要任何底層程式碼的知識,並且可以作為事後配置。
Hystrix斷路器
Hystrix是一個最初由Netflix提供的開源Java庫。它是一個延遲容忍和容錯的庫,用於隔離對遠端系統、服務和第三方庫的訪問點,停止級聯故障,並在不可避免出現故障的複雜分散式系統中啟用彈性。
Hystrix有很多特點,包括:
保護通過第三方客戶端庫訪問(通常是通過網路)的依賴項的延遲和失敗。
防止複雜分散式系統中的級聯故障。
失敗快,恢復快。
回退並儘可能優雅地降級。
啟用近實時監視、警報和操作控制。
當然,斷路器的模式體現了這些特點。因為Hystrix是一個庫,它以白盒方式實現它。
Resilience4J Netflix最近宣佈,它已經停止開發Hystrix庫,轉而開發目前知名度較低的 Resilience4J 專案。 即使客戶端程式碼可能稍有不同,Hystrix和Resilience4J的實現方法也是相似的。
一個Hystrix斷路器的例子
以電子商務web應用程式為例。該應用的架構由不同的微服務組成,每個微服務都基於一個業務特性:
身份驗證
目錄瀏覽
購物車管理
定價和引用
其它
當顯示目錄項時,將查詢定價/報價微服務的價格。如果它壞了,不管是不是斷路器,價格都不會退回來,也不可能訂購任何東西。
從企業的角度來看,任何停機時間不僅會影響品牌的認知度,還會降低銷售。大多數銷售策略都傾向於銷售,儘管價格並不完全正確。實現此銷售策略的解決方案可以是快取定價/報價服務在可用時返回的價格,並在服務關閉時返回快取的價格。
Hystrix提供了一個斷路器實現,允許在電路開啟時執行fallback機制,從而實現了這種方法。
這是Hystrix模型的一個非常簡單的類圖:
最關鍵的地方就在 HystrixCommand
方法 run()
和 getFallback()
:
run()
是要實際執行的程式碼e.g.從報價服務中獲取價格getFallabck()
獲取當斷路器開啟時的fallback結果e.g.返回快取的價格
這可以轉化為以下程式碼,使用Spring的RestTemplate
:
public class FetchQuoteCommand extends HystrixCommand<Double> { private final UUID productId; // 1 private final RestTemplate template; // 2 private final Cache<UUID, Double> cache; // 3 public FetchQuoteCommand(UUID productId, RestTemplate template, Cache<UUID, Double> cache) { super(HystrixCommandGroupKey.Factory.asKey("GetQuote")); // 4 this.template = template; this.cache = cache; this.productId = productId; } @Override protected Double run() { Double quote = template.getForObject("https://acme.com/api/quote/{id}", // 5 Double.class, productId); cache.put(productId, quote); // 6 return quote; } @Override protected Double getFallback() { return cache.get(productId); // 7 } }複製程式碼
這需要作出一些解釋:
該命令包裝產品的id,將其建模為
UUID
。Spring的
RestTemplate
用於進行REST呼叫。任何其他實現方式都可以。一個共享的JCache例項,用於在服務可用時儲存引號。
Hystrix命令需要一個組鍵,以便在需要時將它們組合在一起。這是Hystrix的另一個特性,超出了本文的範圍。有興趣的讀者可以在Hystrix wiki中閱讀有關命令組的內容。
執行對引用服務的呼叫。如果它失敗,Hystrix斷路器流程啟動。
如果呼叫成功,則將返回的引用快取到JCache共享例項中。
當斷路器開啟時呼叫getFallback()。在這種情況下,從快取中獲取引用。
Hystrix wiki提供了更高階的例子,例如fallback本身就是一個需要執行的命令。
將Hystrix與Spring Cloud整合
雖然上面的程式碼可以工作,但是每次引用時都需要建立一個Hystrix命令物件。
Spring Cloud是建立在Spring Boot(本身依賴Spring框架)之上的庫,它提供了與Spring的良好整合。它讓你在處理Hystrix命令物件的例項化時,只需註釋所需的fallback方法:
public class FetchQuoteService { private final RestTemplate template; private final Cache<UUID, Double> cache; public SpringCloudFetchQuoteCommand(RestTemplate template, Cache<UUID, Double> cache) { this.template = template; this.cache = cache; } @HystrixCommand(fallbackMethod = "getQuoteFromCache") // 1 public Double getQuoteFor(UUID productId) { // 2 Double quote = template.getForObject("https://acme.com/api/quote/{id}", // 3 Double.class, productId); cache.put(productId, quote); // 4 return quote; } public Double getQuoteFromCache(UUID productId) { // 5 return cache.get(productId); } }複製程式碼
這個方法應該用
@HystrixCommand
註釋.fallbackMethod
元素引用fallback方法. 顯然,這將通過反射來處理,並且不是型別安全的——畢竟這是一個字串。Spring Cloud Hystrix允許在方法呼叫時傳遞產品的id引數。與上面簡單的Hystrix命令相比,這允許有一個通用的服務物件。Hystrix命令的建立由Spring Cloud在執行時處理。
核心邏輯沒有改變。
同樣,快取過程保持不變。
fallback方法是一種常規方法。 它將使用與主方法完全相同的引數值來呼叫, 因此,它必須具有相同的引數型別(以相同的順序)。因為
getQuoteFor()
方法接受UUID
,所以這個方法也接受UUID
。
無論是獨立的還是由Spring Boot Cloud封裝的,Hystrix都需要在程式碼級處理斷路器。因此,需要提前計劃,更改需要部署更新後的二進位制檔案。然而,當事情出錯時,這允許有一個非常好的自定製的行為。
Istio vs Hystrix: battle of circuit breakers
如果存在失敗的可能性,給定時間,就會出現失敗,嚴重依賴網路的微服務需要針對失敗進行設計。斷路器模式是處理服務缺乏可用性的一種方法: 它不會對請求進行排隊並阻塞呼叫者,而是快速失敗(fail-fast)並立即返回。
實現斷路器的方法有兩種,一種是黑盒方式,另一種是白盒方式。Istio作為一種代理管理工具,使用了黑盒方式.它實現起來很簡單,不依賴於底層技術棧,而且可以在事後配置。
另一方面,Hystrix庫使用白盒方式。它允許所有不同型別的fallback:
單個預設值
一個快取
呼叫其他服務
它還提供了級聯回退(cascading fallbacks)。這些額外的特性是有代價的: 它需要在開發階段就做出fallback的決策。
這兩種方法之間的最佳匹配可能會依靠自己的上下文: 在某些情況下,如引用的服務,一個白盒戰略後備可能是一個更好的選擇,而對於其他情況下快速失敗可能是完全可以接受的,如一個集中的遠端登入服務。
當然,沒有什麼能阻止你同時使用它們。