流程圖
??下面的流程圖展示了,如果你通過 Hystrix 來向某個依賴服務發(fā)送請求的時候,會發(fā)生什么事情:

??下面的分段將向大家詳細說明每一個步驟(序號對應流程圖中的節(jié)點編號):
- Construct a
HystrixCommandorHystrixObservableCommandObject(構造HystrixCommand或HystrixObservableCommand對象) - Execute the Command(執(zhí)行命令
command) - Is the Response Cached?(判斷響應是否已緩存)
- Is the Circuit Open?(判斷斷路器是否已打開)
- Is the Thread Pool/Queue/Semaphore Full?(判斷資源 - 線程池/隊列/信號量 - 是否耗盡)
-
HystrixObservableCommand.construct()orHystrixCommand.run()(執(zhí)行具體的命令操作) - Calculate Circuit Health(計算電路的健康值)
- Get the Fallback(獲取執(zhí)行回滾的方法)
- Return the Successful Response(返回成功的響應)
1. 構造 HystrixCommand 或 HystrixObservableCommand 對象
??第一步是創(chuàng)建 HystrixCommand 或 HystrixObservableCommand 對象,這兩種對象用來封裝向依賴服務發(fā)送請求的動作;其中 HystrixCommand 用來構造傳統(tǒng)的同步命令式請求,而 HystrixObservableCommand 用來構造異步的響應式請求,HystrixObservableCommand 對象是一個可觀察的對象;構造的方法很簡單,直接通過構造器進行創(chuàng)建:
HystrixCommand command = new HystrixCommand(arg1, arg2);
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
2. 執(zhí)行命令
??執(zhí)行命令有四種方式:
-
execute()—— 同步阻塞式,只能用于執(zhí)行HystrixCommand類型的命令,立即執(zhí)行命令,并返回從依賴服務獲取的響應,或者拋出錯誤異常; -
queue()—— 異步提交式,只能用于執(zhí)行HystrixCommand類型的命令,這種方式不立即執(zhí)行命令,而是先將命令對象提交到隊列并獲取一個Future對象,從Future中獲取最終的執(zhí)行結果,而命令由線程池異步執(zhí)行; -
observe()——異步響應式,返回一個Observable來表示響應結果,通過訂閱Observable并注冊回調函數(shù)來消費最終的響應結果;這種方法兩種命令都支持,對于HystrixCommand類型的命令,實際上是先執(zhí)行toObservable()將自身轉換成HystrixObservableCommand類型的命令,然后再執(zhí)行異步響應式命令; -
toObservable()——延遲異步響應式,返回一個Observable來表示響應結果,與observe()的區(qū)別在于,toObservable()返回的Observable是還未執(zhí)行的,需要手動調用訂閱類方法后才執(zhí)行具體的命令,而observe()返回的Observable是已經開始執(zhí)行的,只需要訂閱最終的響應就可以
K value = command.execute();
Future<K> fValue = command.queue();
Observable<K> ohValue = command.observe(); //hot observable
Observable<K> ocValue = command.toObservable(); //cold observable
3. 判斷響應是否已緩存
??當以 Observable 這種形式來發(fā)送請求時,如果請求緩存對于命令可用,并且某個請求的響應存在于緩存中,那么緩存的響應會立刻返回。
4. 判斷斷路器是否打開
??當執(zhí)行 command 時,Hystrix 會檢查斷路器以查看電路是否開路。如果電路打開(或“跳閘”),那么 Hystrix 將不會執(zhí)行命令,而是轉而去執(zhí)行回退動作。如果電路關閉,那么 Hystrix 將按照流程去檢測是否有可用的資源。
5. 判斷資源 - 線程池/隊列/信號量 - 是否耗盡
??如果關聯(lián)到 command 的線程池和隊列(或者信號量,如果不在一個線程內執(zhí)行)等資源已經耗盡(比如隊列已滿,線程池沒有空閑的線程,或者信號量消耗完畢),Hystrix 將不會執(zhí)行命令,而是立刻轉而去執(zhí)行回退動作。
6. 執(zhí)行具體的命令操作
??如果電路是關閉的,并且有足夠的可用資源,那么 Hystrix 就會開始執(zhí)行具體的命令操作(也就是向依賴服務發(fā)送請求并等待獲取響應的代碼,這部分的邏輯是由用戶自己實現(xiàn)),具體執(zhí)行的是如下方法:
-
HystrixCommand.run()—— 返回用戶定義的響應對象或者拋出異常; -
HystrixObservableCommand.construct()—— 返回一個Observable對象,在執(zhí)行完用戶請求代碼后,將用戶自定義的響應對象發(fā)送給Observable對象中注冊的訂閱者,如果發(fā)生錯誤,會通過onError來通知用戶;
如果 run() 或者 construct() 方法的執(zhí)行時間超過了命令配置的 timeout 臨界值,執(zhí)行線程會拋出 TimeoutException,在這種情況下,Hystrix 會轉而去執(zhí)行回退邏輯,如果 run() 或者 construct() 方法無法取消或者中斷,那么最終返回的響應會被放棄。
注意:我們沒有辦法強制正在執(zhí)行的線程停止工作 —— Hystrix 在 JVM 上可以做的最好的事情就是拋出一個
InterruptedException;如果 Hystrix 封裝的任務無法響應InterruptedException,那么 Hystrix 線程池中的線程就無法中斷,需要繼續(xù)執(zhí)行任務,直到用于發(fā)送請求的網絡客戶端代碼收到TimeoutException。這種情況可能會導致 Hystrix 線程池飽和,因為等待網絡客戶端直到超時期間,線程無法中斷去執(zhí)行其他任務,只能等待。大多數(shù) Java 實現(xiàn)的 Http Client 庫都無法響應中斷異常,因此在配置網絡客戶端的超時時間時,最好能和 Hystrix 的timeout臨界值保持一致;如果網絡客戶端的超時臨界值大于 Hystrix 自身的超時臨界值,同時網絡客戶端又無法響應中斷,則會導致 Hystrix 線程池發(fā)生長時間阻塞等待,導致其過快飽和。
?? 如果命令沒有發(fā)生任何異常并且成功返回了響應,Hystrix 會在執(zhí)行一些日志記錄和度量報告,將響應返回給調用方。如果是通過 run(),直接返回響應,如果是通過 construct(),響應會被發(fā)布到Observable 對象,并且通過 onCompleted 來通知調用方。
7. 計算電路的健康值
??Hystrix 將成功、失敗、拒絕和超時報告給斷路器,斷路器維護了一組滾動的計數(shù)器進行統(tǒng)計計算;Hystrix 使用這些統(tǒng)計值來決定何時將電路開閘,而在這個時間點之后的所有請求都將被短路直到恢復期結束,在健康檢查首次通過后,會關閉電路;
8. 執(zhí)行回退動作
??當 Hystrix 執(zhí)行命令失敗時(當電路短路、資源不足、執(zhí)行異常、發(fā)生超時等),都會嘗試執(zhí)行用戶定義的回退操作(fallback);回退操作是由用戶自行編寫,HystrixCommand.getFallback() 會返回 fallback 對象,回退操作一般執(zhí)行一些服務降級或者業(yè)務回滾等操作,盡量不要在 fallback 中訪問其他依賴服務,如果要進行網絡訪問,應當通過 HystrixCommand 或者 HystrixObservableCommand 來執(zhí)行。對于 HystrixCommand,可以通過 HystrixCommand.getFallback() 來實現(xiàn)回退邏輯;對于 HystrixObservableCommand 可以通過 HystrixObservableCommand.resumeWithFallback() 來獲取一個可以發(fā)布回退的 Observable 對象。如果用戶沒有實現(xiàn) fallback 邏輯,Hystrix 會選擇拋出異?;蛘咄ㄟ^ onError 來將異常信息傳送給調用者。
9. 返回成功的響應
??如果命令執(zhí)行成功,Hystrix 將響應返回給調用者,返回響應的形式取決于你以哪種方式來執(zhí)行命令。
斷路器(Circuit Breaker)
??下圖展示了HystrixCommand 或 HystrixObservableCommand 是如何與 HystrixCircuitBreaker 進行交互的,以及 HystrixCircuitBreaker 的相關邏輯流程、如何進行決策以及計數(shù)器如何運作等:

電路開閉的確切方式如下:
- 當電路上的
volume值超過閾值時(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())... - 當電路上的錯誤率超過閾值時
(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())... - 然后斷路器(
circuit-breaker)會從 CLOSED 轉換為 OPEN - 當斷路器打開時,所有的請求都會被短路,不允許被通過
- 一段時間后
(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()),下一個請求被允許通過(這是 HALF-OPEN 狀態(tài)); 如果請求失敗,斷路器將在休眠窗口期間返回 OPEN 狀態(tài);如果請求成功,斷路器將轉換為 CLOSED,并且 1. 中的邏輯再次接管。
資源隔離
??Hystrix 采用了隔板模式來隔離每個依賴服務所使用的資源和限制并發(fā)訪問的數(shù)量

線程和線程池
??客戶端(庫、網絡調用等)對依賴的訪問在不同的線程中執(zhí)行,將對依賴的訪問同調用線程分離開來(比如 Tomcat 的線程池),因此調用者就可以離開一個耗時較長的依賴操作。
??Hystrix 采用每個依賴使用隔離的獨立線程池的方式來保證一旦發(fā)生延遲,那么就只會使該依賴的線程池內的線程飽和,而不會影響到其他的線程。

??如果不使用這種隔離線程池,就需要訪問依賴服務的客戶端能夠非??焖俚厥。ū热缇W絡連接、請求超時和重試等都能快速失?。?,并且這些客戶端都能穩(wěn)定的正確執(zhí)行。Netflix 在設計 Hystrix 時,為什么要選擇隔離線程池呢?原因有很多,包括:
- 許多的應用程序會遠程調用大量不同的后臺服務(有些時候可能會超過100個),這些服務通常由許多不同的團隊開發(fā)和部署
- 每個服務可能都會提供自己的客戶端 SDK
- 這些客戶端 SDK 隨時都會更新
- 這些客戶端 SDK 可能會因為添加新的網絡調用而發(fā)生邏輯上的更改
- 這些客戶端 SDK 會包含諸如重試、數(shù)據(jù)解析、緩存等其他行為
- 這些客戶端 SDK 都是黑盒子 —— 它的實現(xiàn)細節(jié)、網絡訪問模式、默認配置等對用戶都是透明的
- 即使客戶端 SDK 本身沒有變化,但服務本身也會發(fā)生變化,這會影響性能特征,進而導致客戶端配置無效
- 依賴鏈的某個節(jié)點因為客戶端 SDK 的不正確配置可能會引起無法預期的錯誤,而這些錯誤會沿著調用鏈向上傳導
- 絕大多數(shù)網絡訪問都是同步的
-
失敗和延遲也可能發(fā)生在客戶端代碼中,而不僅僅是在網絡調用中
圖4. 資源隔離
隔離線程池的優(yōu)點
??使用隔離線程池來訪問依賴服務比在應用線程中進行訪問有下面這些好處:
- 應用程序完全不受失控的客戶端 SDK 的影響,當分配給依賴項的線程池飽和時不會影響到應用程序的其余部分
- 該應用程序可以接受風險較低的新客戶端 SDK;如果出現(xiàn)問題,因為隔離機制,不會影響到應用程序
- 當失敗的依賴服務再次恢復健康時,分配給依賴服務的線程池將被清理,應用程序能夠立即恢復健康的性能,而不是整個應用容器不堪重負時的長時間恢復
- 如果客戶端庫 SDK 配置錯誤,或者依賴服務發(fā)生故障,線程池的健康狀況將迅速證明這一點(通過增加的錯誤、延遲、超時、拒絕等),并且用戶可以及時進行處理(通常通過動態(tài)屬性實時)而不影響應用程序功能
- 除了隔離的好處之外,專用的線程池還提供了內置的并發(fā)性,可以利用它在同步客戶端庫之上構建異步調用(類似于 Netflix API 如何在 Hystrix 命令之上構建反應式、完全異步的 Java API)
??簡而言之,這種隔離的線程池,可以在依賴服務發(fā)生故障或者變動時,使用戶能夠優(yōu)雅地來處理,而不會導致應用程序的中斷。
注意:盡管 Hystrix 提供了隔離的線程池,但是底層的客戶端代碼也應該具有訪問超時或響應線程中斷的功能,來避免無限期地阻塞 Hystrix 線程。
隔離線程池的缺點
??使用隔離線程池的主要缺點是它們增加了計算開銷,由于每個命令都在單獨地線程上執(zhí)行,因此每執(zhí)行一個命令都會涉及到排隊、調度和上下文切換。但在設計這個系統(tǒng)時,Netflix 決定接受這種開銷的成本以換取它提供的好處,并認為它足夠小,不會對成本或性能產生重大影響。
信號量(Semaphores)
??可以使用信號量(或計數(shù)器)來限制對某個依賴服務的并發(fā)調用數(shù)量,而不是使用線程池/隊列;這允許 Hystrix 在不使用線程池的情況下降低負載,但它不允許超時和阻塞等待;如果您信任客戶端并且只想要降低負載,則可以使用該方法。
??HystrixCommand 和 HystrixObservableCommand 在兩個地方支持使用信號量:
- Fallback:當 Hystrix 要執(zhí)行回退(
fallback)動作時,通常都是使用應用程序線程來執(zhí)行 - Execution:如果將 Hystrix 屬性
execution.isolation.strategy的值設置為SEMAPHORE時,Hystrix 將使用信號量代替線程池來限制應用程序線程并發(fā)執(zhí)行命令的數(shù)量
用戶可以通過配置同時在這兩個地方使用信號量,具體的并發(fā)量(信號量的數(shù)量)可以通過屬性進行動態(tài)配置,信號量數(shù)量的計算方法和線程池大小的計算方法以及隊列大小的計算方法類似。
注意:如果使用信號量來限制對依賴服務的并發(fā)訪問數(shù)量,一旦底層的網絡調用阻塞,那么應用程序線程也會同時阻塞,直到底層網絡調用超時返回
如果信號量達到了最大限制,會拒絕無法獲取信號量的請求,但是填充信號量的線程不能停止(信號量應該是采用了令牌桶算法的限流)
請求折疊
??你可以使用請求折疊器 HystrixCollapser(抽象父類)在 HystrixCommand 前面,通過它可以將多個請求折疊到單個后端服務調用中;請求折疊技術可以將多個重復的網絡請求合并成為一個,從而降低負載。
??下面的圖表展示了兩種場景下使用的線程和網絡連接的數(shù)量:第一個圖表沒有使用請求折疊,而第二個使用了請求折疊(假設所有連接在很短的時間窗口內“并發(fā)”,這種情況下為 10ms);

為什么要使用請求折疊
??使用請求折疊可以降低并發(fā)執(zhí)行 HystrixCommand 所使用的線程和網絡連接數(shù)。請求折疊以自動方式執(zhí)行此操作,不會強制開發(fā)人員編寫代碼來手動協(xié)調批處理請求。
全局上下文 —— 基于所有應用容器(比如Tomcat/Jetty/Undertow)線程
??理想的請求折疊類型是全局應用程序級的,來自任何應用容器線程上的任何用戶的請求都可以折疊在一起。例如:如果將 HystrixCommand 配置為支持對任何用戶檢索電影評級的請求進行批處理,那么當同一個 JVM 中的任何用戶線程發(fā)出此類請求時,Hystrix 都會將這個請求與其他相同請求一起折疊到同一個網絡調用中。
注意:折疊器會將單個
HystrixRequestContext對象傳遞給折疊的網絡調用,因此下游系統(tǒng)必須需要處理這種情況才能使其成為有效選項。
用戶請求上下文 —— 基于單個應用容器線程
??如果將 HystrixCommand 配置為僅處理單個用戶的批處理請求,則 Hystrix 只會在單個應用容器線程(請求)內折疊請求。例如:如果用戶想要為 300 個視頻對象加載書簽,而不是執(zhí)行 300 個網絡調用,Hystrix 可以將它們全部合并為一個。
對象建模和代碼復雜性
??有時,你需要創(chuàng)建一個對用戶具有邏輯含義的對象模型,但卻無法高效利用對象生產者的有效資源。例如:給定一個包含 300 個視頻對象的列表,遍歷它們并在每個對象上調用 getSomeAttribute(),這種簡單的實現(xiàn)可能會導致 300 個網絡調用在毫秒間隔內進行(并且很可能會飽和資源),這樣就浪費了大量的計算資源。有一些手動方法可以處理這個問題,例如在用戶調用 getSomeAttribute() 之前,要求他們聲明他們想要獲取哪些視頻對象的屬性,以便可以進行批量預取;或者可以劃分對象模型,以便用戶必須從一個地方獲取視頻列表,然后從其他地方請求該視頻列表的屬性。這些方法可能導致笨重的 API 和對象模型,并且和心智模型以及使用模式不匹配;而且當多個開發(fā)人員在一個代碼庫上工作時,它們還可能導致簡單的錯誤和低效率,因為為一個用例完成的優(yōu)化可能會被另一個用例的實現(xiàn)和代碼中的新路徑破壞。而通過將折疊邏輯下推到 Hystrix 層,您如何創(chuàng)建對象模型、以什么順序進行調用,或者不同的開發(fā)人員是否知道正在完成甚至需要完成的優(yōu)化都無關緊要。getSomeAttribute() 方法可以放在最適合的地方,并以適合使用模式的任何方式調用,折疊器將自動批量調用時間窗口。
請求折疊花費的代價
??啟用請求折疊的代價是在執(zhí)行實際命令之前增加了延遲;最大成本是批處理窗口的大小。如果您有一個中位數(shù)執(zhí)行時間為 5 毫秒的命令和一個 10 毫秒的批處理窗口,那么在最壞的情況下,執(zhí)行時間可能會變成 15 毫秒。一般情況下請求不會恰好在窗口打開時提交到窗口,因此中值懲罰是窗口時間的一半,在這種情況下為 5 毫秒。確定此成本是否值得取決于正在執(zhí)行的命令。高延遲命令不會受到少量額外平均延遲的影響。此外,給定命令的并發(fā)量是關鍵:如果要一起批處理的請求很少超過 1 或 2 個,那么付出代價是沒有意義的。事實上,在單線程順序迭代中,崩潰將是一個主要的性能瓶頸,因為每次迭代將等待 10 毫秒的批處理窗口時間。然而,如果一個特定的命令被大量并發(fā)使用,并且可以將數(shù)十個甚至數(shù)百個調用一起批處理,那么隨著 Hystrix 減少它所需的線程數(shù)量和網絡連接數(shù)量而實現(xiàn)的吞吐量增加通常遠遠超過成本。

請求緩存
??HystrixCommand 和 HystrixObservableCommand 實現(xiàn)了緩存鍵,使用緩存鍵可以通過并發(fā)感知的方式在請求上下文中刪除重復的調用。下圖是一個示例流程,涉及 HTTP 請求的生命周期以及該請求中執(zhí)行工作的兩個線程:

請求緩存的優(yōu)點:
- 不同的代碼路徑都可以執(zhí)行 Hystrix 命令而無需擔心重復工作;這在許多開發(fā)人員正在實現(xiàn)不同功能的大型代碼庫中尤其有用。例如:通過代碼的多個路徑都需要獲取用戶的 Account 對象,每個路徑都可以像這樣請求它:
Account account = new UserGetAccount(accountId).execute();
//or
Observable<Account> accountObservable = new UserGetAccount(accountId).observe();
Hystrix 的 RequestCache 將執(zhí)行底層的 run() 方法僅一次,并且執(zhí)行 HystrixCommand 的兩個線程將收到相同的數(shù)據(jù),雖然創(chuàng)建了不同的實例。
- 數(shù)據(jù)檢索在整個請求中是一致的;不是每次執(zhí)行命令都訪問依賴服務并返回不同的值(或回退),而是緩存第一個響應并為同一請求中的所有后續(xù)調用返回。
- 消除重復的線程執(zhí)行;由于請求緩存位于
construct()或run()方法調用之前,Hystrix 可以在線程執(zhí)行該調用之前進行重復數(shù)據(jù)刪除。如果 Hystrix 沒有實現(xiàn)請求緩存功能,那么每個命令都需要在構造或運行方法中自己實現(xiàn)它,這將把它放在線程排隊并執(zhí)行之后。
