[翻譯]Hystrix wiki–How it Works
註:本文並非是精確的文檔翻譯,而是根據自己理解的整理,有些內容可能由於理解偏差翻譯有誤,有些內容由於是顯而易見的,並沒有翻譯,而是略去了。本文更多是學習過程的產出,請盡量參考原官方文檔。
流程圖
下圖描述了當通過Hystrix請求依賴服務時的流程:
1. 創建HystrixCommand 或者 HystrixObserverbleCommand對象
通過將調用服務所需的參數傳入commad的構造函數,創建commad對象。HystrixObserverbleCommand表示,調用的服務將返回一個Observable對象並且發射(emit)響應。
HystrixCommand command = newHystrixCommand(arg1, arg2); HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
2. 執行command
上述兩種command有四種執行方式(前兩種只適用於HystrixCommand ):
- execute() -- 阻塞調用,返回所調用服務的正常、錯誤或者異常返回信息。
- queue() -- 返回Future對象,可從其中獲取所調用服務的返回
- observe() -- 訂閱代表服務返回的Observable對象,並且返回該對象的一個副本
- toObservable() -- 返回Observable對象,當向其訂閱時,將會執行commad並且發射一個response
K value = command.execute(); Future<K> fValue = command.queue(); Observable<K> ohValue = command.observe(); //hot observable Observable<K> ocValue = command.toObservable(); //cold observable
同步調用execute()會調用queue().get()方法,queue()會調用toObservable().toBlocking().toFuture。所以,最終每個HystrixCommand都是Observable實現。
3. 響應是否被緩存?
如對應依賴的響應緩存是打開的,並且緩存中存在對應響應,則會以Observable的形式從緩存中直接返回。
4. 斷路器是否打開?
在執行commad時,將會檢查斷路器是否打開。
當斷路器處於打開的狀態時,將不會執行Commad,跳轉到第8步執行降級策略。
如果斷路器關閉,將會繼續執行第5步,並且檢查是否有足夠的容量來執行命令。
5. 線程池/隊列/信號量是否滿?
當上述資源池已滿,將不會執行Command,跳轉到第8步執行降級策略。
6. 執行HystrixObservableCommand.construct() 或 HystrixCommand.run()
通過執行上述方法中的一種,Hystrix實現對依賴的調用,其中:
- HystrixCommand.run() -- 正常返回或者拋出異常
- HystrixObservableCommand.construct() -- 返回Observable對象,發射一個返回或者onError通知
如果run或者construct方法超時,對應線程將會拋出超時異常。此時進入降級流程,並且如果正常的服務調用沒有取消而且最終得到響應,則此響應將被丟棄。
這裏需要註意的是,由於沒有辦法確保一個線程強制結束,Hystrix最多能夠向服務調用線程拋出一個InterruptedException。如果實際的工作線程沒有對該異常進行正常響應,而是忽略;則Hystrix線程池將繼續執行,此時客戶端實際上已經得到了TimeoutException。盡管此時客戶端的負載已被有效隔離,這種情形仍可能導致Hystrix線程池耗盡。大多數的Java Http Client組件庫在得到InterruptedException時不會正常終止線程執行,因此,請確保服務調用的讀寫超時配置正確。
7. 計算回路健康指標
斷路器通過維護一組計數器來統計回路的健康指標,Hystrix會將服務調用的成功、失敗、拒絕、超時等次數發給斷路器。
斷路器通過統計數據來決定是否觸發斷路邏輯。斷路器打開後,將經過一定的預設時間,在此期間所有的請求均不會實際執行。預設時間達到後,如果此時服務調用健康檢測通過,則會關閉斷路器。
8. 進入降級策略
當command執行失敗(4、5、6步中提及的情形),Hystrix將觸發降級。
使用Hystrix時,需要根據具體的場景寫出降級邏輯,可以是從緩存中獲取響應或者進入靜態的處理邏輯。如果,此時仍舊必須通過網絡調用來執行降級邏輯,則需要在另外一個Command中執行。
當使用HystrixCommand時,需要實現HystrixCommand.getFallBack()方法來實現降級策略。
當使用HystrixObservableCommand時,需要實現HystrixObservableCommand.resumeWithFallback(),其將會返回一個Observable對象。
當降級方法返回時,Hystrix向調用者相應返回。使用HystrixCommand.getFallback()時,返回Observable對象發射從降級方法中得到的響應。當使用HystrixObservableCommand.resumeWithFallback()時,返回降級方法實際返回的Observable對象。
如果在使用時未實現降級方法,或者降級方法本身會拋出異常,Hystrix仍然會返回Observable對象,但不會發射任何對象,而是會在onError通知後立即結束。通過onError通知,導致Command終止的異常被傳遞給調用方。(降級方法本身不應該失敗,在實際中應該盡量避免這種情況)
觸發Hystrix command的方式不同,失敗的降級方法的結果不同:
- execute() – 拋出異常
- queue() -- 返回Future,當其get()方法被調用時,拋出異常
- observe() -- 返回Observable,當對其subscribe時,通過調用subscriber的onError方法將立即終止
- toObservable() -- 返回Observable,當對其subscribe時, 通過調用subscriber的onError方法將終止
9. 成功返回
當Hystrix command成功返回時,其通過Observable向調用者返回。實際返回的形式取決於第2步中觸發command的方式:
- execute() – 通過Future對象的get()方法得到返回值。
- queue() -- 通過將Observable對象轉換為BlockingObservable對象,以便轉換為Future對象並返回。
- observe() -- 首先向Observable註冊,並開始執行command;最終返回Observable對象,當向其訂閱時,將會執行發射或者通知
- toObservable() -- 直接返回Observable對象;必須顯示訂閱來觸發調用
斷路器
下面的時序圖,表示了HystrixCommand和斷路器間的交互邏輯以及各統計判斷過程。
斷路器的開關狀態切換如下:
- 如果通過回路的流量達到一定值
- 如果出錯率超過了預設的出錯率
- 斷路器打開
- 當斷路器打開時,所有請求將不會被通過
- 達到預設的時間後,下一個請求將會被通過(half-open)。如果請求失敗,斷路器將繼續保持打開狀態並休眠固定的時間;如果請求成功,則斷路器關閉。
隔離
Hystrix采用壁倉模式(bulkhead pattern)隔離依賴。
線程及線程池
Hystrix 使用每個服務獨立的線程池,這樣一來,每個服務的延遲最壞只會造成該服務對應的線程池耗盡。
也可以不使用針對每個服務獨立的線程池,在這種情況下,要保證服務不受外部依賴影響,則需要客戶端能夠快速失敗並且能夠正確返回失敗信息。
Hystrix采用線程池作為服務隔離的手段主要有以下原因:
- 許多應用依賴的後臺服務成十上百,這些服務可能有許多不同的團隊開發。
- 每個服務都提供了自己的客戶端庫
- 客戶端庫總是在不停的改變中
- 客戶端庫可能隨著需要調用的服務增多而改變內部邏輯
- 客戶端庫可能包含了諸如重試、緩存、數據解析等等內部邏輯
- 對於使用方來說客戶端庫更像是一個黑盒,實現細節、網絡調用方式、默認配置等對使用方來說不是非常顯而易見的
- 在線上,很多問題的最終定位原因都會歸結為:“客戶端有些東西改變,需要對應調整應用的配置”或者“客戶端的邏輯改變了”
- 如果使用方未做任何變更,服務本身可能升級,這種情況可能也會引起使用方配置的不可用從而引起問題
- Transitive dependencies can pull in other client libraries that are not expected and perhaps not correctly configured.(沒明白)
- 網絡調用通常是同步的調用
- 不僅僅是網絡調用,客戶端代碼也可能引起超時或者不可用
使用線程池的好處
使用服務級獨立的線程池,主要有以下好處:
- 應用受保護不被不可用的客戶端庫影響。每個服務只可能耗盡對應的線程池,不會影響整個應用
- 新加入的客戶端庫帶來的影響可控
- 當客戶端庫恢復可用後,對應的線程池能夠快速恢復可用。相反,不使用線程池時,應用級的恢復可能耗費更多的時間。
- 如果某個依賴客戶端的配置有誤,可以迅速定位問題,並且在不影響整個應用的情況下快速修復問題。
- 同樣地,當某個客戶端的特性改變引起原油的配置不適用時,也可以迅速定位問題,同樣可以迅速修復問題。
- 除了隔離帶來的益處,適用線程池在同步調用的基礎上增加了一層內建的異步機制
總之,線程池隔離能夠使得應用對於客戶端庫以及遠程服務的改變迅速響應,優雅應對,避免資源耗盡。
線程池的缺陷
采用線程池帶來的主要負面影響是額外的計算開銷。每個單獨的command執行都包含排隊、調度、上下文切換等等由於線程執行帶來的開銷。
線程的開銷
Hystrix對父線程端到端執行的總時間消耗以及執行command的子線程的時間消耗都做了計量,以次來觀測Hystrix帶來的整體開銷。
下圖描述了一個每秒通過HystrixCommand處理每秒約60次請求(向後每秒調用約350次後臺服務)的請求開銷:
在中位數處,采用額外的線程沒有帶來明顯的開銷
在90%處,額外的線程帶來的開銷約為3ms
在99%處,額外的線程帶來的開銷約為9ms。相比於中位數處,實際服務調用時間從2增加到28,而額外的線程開銷從0增加到9。
這種額外的開銷相比起帶來的益處在大多數情況下我們認為是可以接受的。
信號量
作為線程池的替代,Hystrix同樣支持使用信號量(或計數器)限制某個服務的並發調用量。與線程池不同的是,信號量不支持超時計數(timing out)和walking away(這個咋翻。。。)。在信任客戶端並且僅僅想達到負載限制的目的的情況下,可以采用這種方法。
HystrixCommand和HystrixObservableCommand在如下兩個方面支持信號量:
降級:當Hystrix獲取降級返回時,總是在調用容器線程上進行
執行:如果將execution.isolaiton.strategy設置為SEMOPHORE,則將采用信號量作為限制並發數的方法。
上述兩個方面均可以通過動態參數的方式實現配置,來實現對並發線程數的限制。限制數的大小和線程池大小的估計方法類似。
註意:如果一個外部依賴時通過信號量的方式隔離的,並且發生了延遲,則父線程將同樣保持阻塞,直至調用超時或者返回。
信號量並發數限制將會在達到後立即生效,但是已經阻塞的線程將繼續保持阻塞,不能提前返回。
請求合並
可以在HystrixCommand前置一個collapser,來實現對多個請求對應的後臺服務調用的合並。
下圖是采用和不采用請求合並的服務調用的示意圖。
為什麽采用請求合並?
在並發請求下,使用請求合並主要用來降低並發的線程數及網絡連接數。Hystrix采用自動的方式來完成請求合並,不需要開發者顯示執行請求合並操作。
全局上下文
理想的合並應該是應用全局級別的,這樣容器內所有用戶的請求均合一被合並。
例如,如果對請求電影排行的一個服務做了請求合並配置,那麽同一個JVM內的所有請求,都將別Hystrix合並到一個網絡調用中。
需要註意的是,collapser會通過網絡調用向下遊系統發送一個HystrixRequestContext,下遊系統必須能夠正確處理這個context以便正確響應。
單用戶請求上下文
Hystrix還支持基於單個用戶態的線程對請求做合並,比如某用戶線程請求300部電影的標簽信息,Hystrix會將300各請求合並成一個。
關於對象模型及代碼復雜度
有些時候當在客戶端設計了一個符合業務邏輯的對象模型後,通常會發現,這個模型在有效利用服務端資源方面可能並不好。
例如,對於一個獲取300各電影的各自標簽的場景,逐個叠代調用標簽獲取服務是最簡單的邏輯,但是這種設計會在短時間內發起300個後臺的服務調用請求,容易造成資源的耗盡。
對於這種場景,有常見的解決方式,。例如,在用戶視角做限制,在獲取電影標簽前,必須讓用戶限定獲取標簽的電影範圍,而不是全部電影。或者,可以對對象模型做分解,可以首先調用一個服務獲取電影信息列表,再針對這個列表請求屬性信息。
顯而易見,這些設計可能導致用戶不友好的API設計或者不符合現實邏輯的模型設計。同時,在許多開發者協同開發的應用系統中,這些特殊的設計很有可能被其他開發者修改而失效。
Hystrix通過將請求合並下沈到實際應用邏輯以外,使得對象模型的特殊設計與否、服務調用的順序特殊考慮與否都不會對應用造成顯著影響,甚至開發者也不需要做太多額外的業務邏輯外的優化。
請求合並的代價是什麽?
很明顯,請求合並帶來的額外消耗主要是執行合並帶來的時間延遲。其最大值便是合並的時間窗口的最大值。
例如,有一個外部調用花費5ms,並且通過Hystrix對其做了時間窗口為10ms的合並,那麽,最壞情況下,這個外部調用將可能花費15ms。但是,一般來講,並不是所有請求都在窗口剛剛打開的那一刻進來,因此,實際上做請求合並帶來的平均額外時間消耗是5ms。
是否做請求合並的關鍵是外部請求本身的特性。對於一個本身耗時很高的調用,對其做請求合並帶來的額外時間影響可能微乎其微。同時,對外部服務的並發調用數也是關鍵。顯然沒有必要對一個並發是1或者2的外部調用做請求合並。
然而,對於並發很高的外部調用,例如在合並窗口內並發可以達到幾十或者上百,則做請求合並帶來的額外延時相比於過多的資源消耗調用是可以忽略的。
請求流程圖
請求緩存
HystrixCommand和HystrixObservableCommand的實現可以針對並發場景下的請求定義緩存key,來減少後續的實際網絡請求。
下圖展示了一個HTTP請求的生命周期及兩個執行該請求的線程:
請求緩存的主要優勢在於:
- 不同的代碼路徑都可以執行Hystrix command,而不用擔心重復工作。
這在較大的團隊中帶來的好處通常更加明顯。例如,對於獲取用戶賬戶的服務調用,多處代碼都可能這樣寫:
Account account = new UserGetAccount(accountId).execute(); //or Observable<Account> accountObservable = new UserGetAccount(accountId).observe();
Hystrix的RequestCache將僅執行一次run()方法,所有執行HystrixCommand的線程都將得到相同的返回結果,盡管他們完全不同的線程實例。
- 整個請求中得到的數據都是一致的
當第一個返回被緩存後,在同一個請求中,後續的command執行的返回都將是一致的。
- 消除了重復的線程執行
Request cache位於執行run()或者executed()方法之前,因此可以消除實際執行調用帶來的重復線程執行消耗。
【Reference】
https://github.com/Netflix/Hystrix/wiki/How-it-Works
[翻譯]Hystrix wiki–How it Works