流程圖
下面的流程圖展示了,如果你通過 Hystrix 來向某個依賴服務發送請求的時候,會發生什麼事情:
下面的分段將向大家詳細說明每一個步驟(序號對應流程圖中的節點編號):
- Construct a
HystrixCommand
orHystrixObservableCommand
Object(構造HystrixCommand
或HystrixObservableCommand
對象) - Execute the Command(執行命令
command
) - Is the Response Cached?(判斷響應是否已緩存)
- Is the Circuit Open?(判斷斷路器是否已打開)
- Is the Thread Pool/Queue/Semaphore Full?(判斷資源 - 線程池/隊列/信號量 - 是否耗盡)
-
HystrixObservableCommand.construct()
orHystrixCommand.run()
(執行具體的命令操作) - Calculate Circuit Health(計算電路的健康值)
- Get the Fallback(獲取執行回滾的方法)
- Return the Successful Response(返回成功的響應)
1. 構造 HystrixCommand
或 HystrixObservableCommand
對象
第一步是創建 HystrixCommand
或 HystrixObservableCommand
對象,這兩種對象用來封裝向依賴服務發送請求的動作;其中 HystrixCommand
用來構造傳統的同步命令式請求,而 HystrixObservableCommand
用來構造異步的響應式請求,HystrixObservableCommand
對象是一個可觀察的對象;構造的方法很簡單,直接通過構造器進行創建:
HystrixCommand command = new HystrixCommand(arg1, arg2);
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
2. 執行命令
執行命令有四種方式:
-
execute()
—— 同步阻塞式,只能用於執行HystrixCommand
類型的命令,立即執行命令,並返回從依賴服務獲取的響應,或者拋出錯誤異常; -
queue()
—— 異步提交式,只能用於執行HystrixCommand
類型的命令,這種方式不立即執行命令,而是先將命令對象提交到隊列並獲取一個Future
對象,從Future
中獲取最終的執行結果,而命令由線程池異步執行; -
observe()
——異步響應式,返回一個Observable
來表示響應結果,通過訂閱Observable
並註冊回調函數來消費最終的響應結果;這種方法兩種命令都支持,對於HystrixCommand
類型的命令,實際上是先執行toObservable()
將自身轉換成HystrixObservableCommand
類型的命令,然後再執行異步響應式命令; -
toObservable()
——延遲異步響應式,返回一個Observable
來表示響應結果,與observe()
的區別在於,toObservable()
返回的Observable
是還未執行的,需要手動調用訂閱類方法後才執行具體的命令,而observe()
返回的Observable
是已經開始執行的,只需要訂閱最終的響應就可以
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
這種形式來發送請求時,如果請求緩存對於命令可用,並且某個請求的響應存在於緩存中,那麼緩存的響應會立刻返回。
4. 判斷斷路器是否打開
當執行 command
時,Hystrix 會檢查斷路器以查看電路是否開路。如果電路打開(或“跳閘”),那麼 Hystrix 將不會執行命令,而是轉而去執行回退動作。如果電路關閉,那麼 Hystrix 將按照流程去檢測是否有可用的資源。
5. 判斷資源 - 線程池/隊列/信號量 - 是否耗盡
如果關聯到 command
的線程池和隊列(或者信號量,如果不在一個線程內執行)等資源已經耗盡(比如隊列已滿,線程池沒有空閒的線程,或者信號量消耗完畢),Hystrix 將不會執行命令,而是立刻轉而去執行回退動作。
6. 執行具體的命令操作
如果電路是關閉的,並且有足夠的可用資源,那麼 Hystrix 就會開始執行具體的命令操作(也就是向依賴服務發送請求並等待獲取響應的代碼,這部分的邏輯是由用戶自己實現),具體執行的是如下方法:
-
HystrixCommand.run()
—— 返回用戶定義的響應對象或者拋出異常; -
HystrixObservableCommand.construct()
—— 返回一個Observable
對象,在執行完用戶請求代碼後,將用戶自定義的響應對象發送給Observable
對象中註冊的訂閱者,如果發生錯誤,會通過onError
來通知用戶;
如果 run()
或者 construct()
方法的執行時間超過了命令配置的 timeout
臨界值,執行線程會拋出 TimeoutException
,在這種情況下,Hystrix 會轉而去執行回退邏輯,如果 run()
或者 construct()
方法無法取消或者中斷,那麼最終返回的響應會被放棄。
注意:我們沒有辦法強制正在執行的線程停止工作 —— Hystrix 在 JVM 上可以做的最好的事情就是拋出一個
InterruptedException
;如果 Hystrix 封裝的任務無法響應InterruptedException
,那麼 Hystrix 線程池中的線程就無法中斷,需要繼續執行任務,直到用於發送請求的網絡客戶端代碼收到TimeoutException
。這種情況可能會導致 Hystrix 線程池飽和,因爲等待網絡客戶端直到超時期間,線程無法中斷去執行其他任務,只能等待。大多數 Java 實現的 Http Client 庫都無法響應中斷異常,因此在配置網絡客戶端的超時時間時,最好能和 Hystrix 的timeout
臨界值保持一致;如果網絡客戶端的超時臨界值大於 Hystrix 自身的超時臨界值,同時網絡客戶端又無法響應中斷,則會導致 Hystrix 線程池發生長時間阻塞等待,導致其過快飽和。
如果命令沒有發生任何異常並且成功返回了響應,Hystrix 會在執行一些日誌記錄和度量報告,將響應返回給調用方。如果是通過 run()
,直接返回響應,如果是通過 construct()
,響應會被髮布到Observable
對象,並且通過 onCompleted
來通知調用方。
7. 計算電路的健康值
Hystrix 將成功、失敗、拒絕和超時報告給斷路器,斷路器維護了一組滾動的計數器進行統計計算;Hystrix 使用這些統計值來決定何時將電路開閘,而在這個時間點之後的所有請求都將被短路直到恢復期結束,在健康檢查首次通過後,會關閉電路;
8. 執行回退動作
當 Hystrix 執行命令失敗時(當電路短路、資源不足、執行異常、發生超時等),都會嘗試執行用戶定義的回退操作(fallback
);回退操作是由用戶自行編寫,HystrixCommand.getFallback()
會返回 fallback
對象,回退操作一般執行一些服務降級或者業務回滾等操作,儘量不要在 fallback
中訪問其他依賴服務,如果要進行網絡訪問,應當通過 HystrixCommand
或者 HystrixObservableCommand
來執行。對於 HystrixCommand
,可以通過 HystrixCommand.getFallback()
來實現回退邏輯;對於 HystrixObservableCommand
可以通過 HystrixObservableCommand.resumeWithFallback()
來獲取一個可以發佈回退的 Observable
對象。如果用戶沒有實現 fallback
邏輯,Hystrix 會選擇拋出異常或者通過 onError
來將異常信息傳送給調用者。
9. 返回成功的響應
如果命令執行成功,Hystrix 將響應返回給調用者,返回響應的形式取決於你以哪種方式來執行命令。
斷路器(Circuit Breaker)
下圖展示了HystrixCommand
或 HystrixObservableCommand
是如何與 HystrixCircuitBreaker
進行交互的,以及 HystrixCircuitBreaker
的相關邏輯流程、如何進行決策以及計數器如何運作等:
電路開閉的確切方式如下:
- 當電路上的
volume
值超過閾值時(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())...
- 當電路上的錯誤率超過閾值時
(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())...
- 然後斷路器(
circuit-breaker
)會從 CLOSED 轉換爲 OPEN - 當斷路器打開時,所有的請求都會被短路,不允許被通過
- 一段時間後
(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds())
,下一個請求被允許通過(這是 HALF-OPEN 狀態); 如果請求失敗,斷路器將在休眠窗口期間返回 OPEN 狀態;如果請求成功,斷路器將轉換爲 CLOSED,並且 1. 中的邏輯再次接管。
資源隔離
Hystrix 採用了隔板模式來隔離每個依賴服務所使用的資源和限制併發訪問的數量
線程和線程池
客戶端(庫、網絡調用等)對依賴的訪問在不同的線程中執行,將對依賴的訪問同調用線程分離開來(比如 Tomcat 的線程池),因此調用者就可以離開一個耗時較長的依賴操作。
Hystrix 採用每個依賴使用隔離的獨立線程池的方式來保證一旦發生延遲,那麼就只會使該依賴的線程池內的線程飽和,而不會影響到其他的線程。
如果不使用這種隔離線程池,就需要訪問依賴服務的客戶端能夠非常快速地失敗(比如網絡連接、請求超時和重試等都能快速失敗),並且這些客戶端都能穩定的正確執行。Netflix 在設計 Hystrix 時,爲什麼要選擇隔離線程池呢?原因有很多,包括:
- 許多的應用程序會遠程調用大量不同的後臺服務(有些時候可能會超過100個),這些服務通常由許多不同的團隊開發和部署
- 每個服務可能都會提供自己的客戶端 SDK
- 這些客戶端 SDK 隨時都會更新
- 這些客戶端 SDK 可能會因爲添加新的網絡調用而發生邏輯上的更改
- 這些客戶端 SDK 會包含諸如重試、數據解析、緩存等其他行爲
- 這些客戶端 SDK 都是黑盒子 —— 它的實現細節、網絡訪問模式、默認配置等對用戶都是透明的
- 即使客戶端 SDK 本身沒有變化,但服務本身也會發生變化,這會影響性能特徵,進而導致客戶端配置無效
- 依賴鏈的某個節點因爲客戶端 SDK 的不正確配置可能會引起無法預期的錯誤,而這些錯誤會沿着調用鏈向上傳導
- 絕大多數網絡訪問都是同步的
-
失敗和延遲也可能發生在客戶端代碼中,而不僅僅是在網絡調用中
隔離線程池的優點
使用隔離線程池來訪問依賴服務比在應用線程中進行訪問有下面這些好處:
- 應用程序完全不受失控的客戶端 SDK 的影響,當分配給依賴項的線程池飽和時不會影響到應用程序的其餘部分
- 該應用程序可以接受風險較低的新客戶端 SDK;如果出現問題,因爲隔離機制,不會影響到應用程序
- 當失敗的依賴服務再次恢復健康時,分配給依賴服務的線程池將被清理,應用程序能夠立即恢復健康的性能,而不是整個應用容器不堪重負時的長時間恢復
- 如果客戶端庫 SDK 配置錯誤,或者依賴服務發生故障,線程池的健康狀況將迅速證明這一點(通過增加的錯誤、延遲、超時、拒絕等),並且用戶可以及時進行處理(通常通過動態屬性實時)而不影響應用程序功能
- 除了隔離的好處之外,專用的線程池還提供了內置的併發性,可以利用它在同步客戶端庫之上構建異步調用(類似於 Netflix API 如何在 Hystrix 命令之上構建反應式、完全異步的 Java API)
簡而言之,這種隔離的線程池,可以在依賴服務發生故障或者變動時,使用戶能夠優雅地來處理,而不會導致應用程序的中斷。
注意:儘管 Hystrix 提供了隔離的線程池,但是底層的客戶端代碼也應該具有訪問超時或響應線程中斷的功能,來避免無限期地阻塞 Hystrix 線程。
隔離線程池的缺點
使用隔離線程池的主要缺點是它們增加了計算開銷,由於每個命令都在單獨地線程上執行,因此每執行一個命令都會涉及到排隊、調度和上下文切換。但在設計這個系統時,Netflix 決定接受這種開銷的成本以換取它提供的好處,並認爲它足夠小,不會對成本或性能產生重大影響。
信號量(Semaphores)
可以使用信號量(或計數器)來限制對某個依賴服務的併發調用數量,而不是使用線程池/隊列;這允許 Hystrix 在不使用線程池的情況下降低負載,但它不允許超時和阻塞等待;如果您信任客戶端並且只想要降低負載,則可以使用該方法。
HystrixCommand
和 HystrixObservableCommand
在兩個地方支持使用信號量:
- Fallback:當 Hystrix 要執行回退(
fallback
)動作時,通常都是使用應用程序線程來執行 - Execution:如果將 Hystrix 屬性
execution.isolation.strategy
的值設置爲SEMAPHORE
時,Hystrix 將使用信號量代替線程池來限制應用程序線程併發執行命令的數量
用戶可以通過配置同時在這兩個地方使用信號量,具體的併發量(信號量的數量)可以通過屬性進行動態配置,信號量數量的計算方法和線程池大小的計算方法以及隊列大小的計算方法類似。
注意:如果使用信號量來限制對依賴服務的併發訪問數量,一旦底層的網絡調用阻塞,那麼應用程序線程也會同時阻塞,直到底層網絡調用超時返回
如果信號量達到了最大限制,會拒絕無法獲取信號量的請求,但是填充信號量的線程不能停止(信號量應該是採用了令牌桶算法的限流)
請求摺疊
你可以使用請求摺疊器 HystrixCollapser
(抽象父類)在 HystrixCommand
前面,通過它可以將多個請求摺疊到單個後端服務調用中;請求摺疊技術可以將多個重複的網絡請求合併成爲一個,從而降低負載。
下面的圖表展示了兩種場景下使用的線程和網絡連接的數量:第一個圖表沒有使用請求摺疊,而第二個使用了請求摺疊(假設所有連接在很短的時間窗口內“併發”,這種情況下爲 10ms);
爲什麼要使用請求摺疊
使用請求摺疊可以降低併發執行 HystrixCommand
所使用的線程和網絡連接數。請求摺疊以自動方式執行此操作,不會強制開發人員編寫代碼來手動協調批處理請求。
全局上下文 —— 基於所有應用容器(比如Tomcat/Jetty/Undertow)線程
理想的請求摺疊類型是全局應用程序級的,來自任何應用容器線程上的任何用戶的請求都可以摺疊在一起。例如:如果將 HystrixCommand
配置爲支持對任何用戶檢索電影評級的請求進行批處理,那麼當同一個 JVM 中的任何用戶線程發出此類請求時,Hystrix 都會將這個請求與其他相同請求一起摺疊到同一個網絡調用中。
注意:摺疊器會將單個
HystrixRequestContext
對象傳遞給摺疊的網絡調用,因此下游系統必須需要處理這種情況才能使其成爲有效選項。
用戶請求上下文 —— 基於單個應用容器線程
如果將 HystrixCommand
配置爲僅處理單個用戶的批處理請求,則 Hystrix 只會在單個應用容器線程(請求)內摺疊請求。例如:如果用戶想要爲 300 個視頻對象加載書籤,而不是執行 300 個網絡調用,Hystrix 可以將它們全部合併爲一個。
對象建模和代碼複雜性
有時,你需要創建一個對用戶具有邏輯含義的對象模型,但卻無法高效利用對象生產者的有效資源。例如:給定一個包含 300 個視頻對象的列表,遍歷它們並在每個對象上調用 getSomeAttribute()
,這種簡單的實現可能會導致 300 個網絡調用在毫秒間隔內進行(並且很可能會飽和資源),這樣就浪費了大量的計算資源。有一些手動方法可以處理這個問題,例如在用戶調用 getSomeAttribute()
之前,要求他們聲明他們想要獲取哪些視頻對象的屬性,以便可以進行批量預取;或者可以劃分對象模型,以便用戶必須從一個地方獲取視頻列表,然後從其他地方請求該視頻列表的屬性。這些方法可能導致笨重的 API 和對象模型,並且和心智模型以及使用模式不匹配;而且當多個開發人員在一個代碼庫上工作時,它們還可能導致簡單的錯誤和低效率,因爲爲一個用例完成的優化可能會被另一個用例的實現和代碼中的新路徑破壞。而通過將摺疊邏輯下推到 Hystrix 層,您如何創建對象模型、以什麼順序進行調用,或者不同的開發人員是否知道正在完成甚至需要完成的優化都無關緊要。getSomeAttribute()
方法可以放在最適合的地方,並以適合使用模式的任何方式調用,摺疊器將自動批量調用時間窗口。
請求摺疊花費的代價
啓用請求摺疊的代價是在執行實際命令之前增加了延遲;最大成本是批處理窗口的大小。如果您有一箇中位數執行時間爲 5 毫秒的命令和一個 10 毫秒的批處理窗口,那麼在最壞的情況下,執行時間可能會變成 15 毫秒。一般情況下請求不會恰好在窗口打開時提交到窗口,因此中值懲罰是窗口時間的一半,在這種情況下爲 5 毫秒。確定此成本是否值得取決於正在執行的命令。高延遲命令不會受到少量額外平均延遲的影響。此外,給定命令的併發量是關鍵:如果要一起批處理的請求很少超過 1 或 2 個,那麼付出代價是沒有意義的。事實上,在單線程順序迭代中,崩潰將是一個主要的性能瓶頸,因爲每次迭代將等待 10 毫秒的批處理窗口時間。然而,如果一個特定的命令被大量併發使用,並且可以將數十個甚至數百個調用一起批處理,那麼隨着 Hystrix 減少它所需的線程數量和網絡連接數量而實現的吞吐量增加通常遠遠超過成本。
請求緩存
HystrixCommand
和 HystrixObservableCommand
實現了緩存鍵,使用緩存鍵可以通過併發感知的方式在請求上下文中刪除重複的調用。下圖是一個示例流程,涉及 HTTP 請求的生命週期以及該請求中執行工作的兩個線程:
請求緩存的優點:
- 不同的代碼路徑都可以執行 Hystrix 命令而無需擔心重複工作;這在許多開發人員正在實現不同功能的大型代碼庫中尤其有用。例如:通過代碼的多個路徑都需要獲取用戶的 Account 對象,每個路徑都可以像這樣請求它:
Account account = new UserGetAccount(accountId).execute();
//or
Observable<Account> accountObservable = new UserGetAccount(accountId).observe();
Hystrix 的 RequestCache
將執行底層的 run()
方法僅一次,並且執行 HystrixCommand 的兩個線程將收到相同的數據,雖然創建了不同的實例。
- 數據檢索在整個請求中是一致的;不是每次執行命令都訪問依賴服務並返回不同的值(或回退),而是緩存第一個響應併爲同一請求中的所有後續調用返回。
- 消除重複的線程執行;由於請求緩存位於
construct()
或run()
方法調用之前,Hystrix 可以在線程執行該調用之前進行重複數據刪除。如果 Hystrix 沒有實現請求緩存功能,那麼每個命令都需要在構造或運行方法中自己實現它,這將把它放在線程排隊並執行之後。