二、Hystrix如何工作的

流程圖

下圖顯示了當您通過Hystrix向服務依賴項發出請求時會發生什麼情況:

下面詳細地解釋這個流程:

1、構造一個HystrixCommand或HystrixObservableCommand對象

第一步是構造一個HystrixCommand或HystrixObservableCommand對象,以表示您對依賴項的請求。向構造函數傳遞發出請求時所需的任何參數。

如果依賴希望返回單個響應,則構造一個HystrixCommand對象。例如:

HystrixCommand command = new HystrixCommand(arg1, arg2);

如果依賴希望返回發出響應的可觀察對象,則構造一個HystrixObservableCommand對象。例如:

HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);

2、Execute the Command

您可以使用以下四種方法之一來執行該命令(前兩種方法僅適用於簡單的HystrixCommand對象,不適用於HystrixObservableCommand):

execute() —塊,然後返回從依賴項接收到的單個響應(或在出現錯誤時拋出異常)

queue() —返回可以從依賴項獲取單個響應的Future

observe() —訂閱表示來自依賴項的響應的可觀察對象(Observable ),並返回複製源可觀察對象(source Observable)的可觀察對象

toObservable() — 返回一個可觀察的,當您訂閱它時,它將執行Hystrix命令併發出響應

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()依次調用observable (). toblock (). tofuture()。也就是說,最終每個HystrixCommand都由一個可觀察的實現支持,即使是那些打算返回單個簡單值的命令。

3.是否緩存了響應?

如果爲該命令啓用了請求緩存,並且該請求的響應在緩存中可用,則該緩存的響應將立即以可觀察的形式返回。

4. 電路打開了嗎?

當您執行該命令時,Hystrix將與斷路器一起檢查電路是否打開。

如果電路是打開的(或“跳閘”),Hystrix將不執行該命令,而是將流路由到(8)獲得回退。

如果電路是關閉的,則流繼續(5)檢查是否有能力運行該命令。

5. 線程池/隊列/信號量是否已滿?

如果與該命令相關的線程池和隊列(或信號量,如果不在線程中運行)已滿,那麼Hystrix將不執行該命令,而是立即將流路由到(8)以獲取回退

6. HystrixObservableCommand.construct() or HystrixCommand.run()

在這裏,Hystrix通過爲此目的編寫的方法調用對依賴項的請求,方法如下:

HystrixCommand.run() —返回單個響應或拋出異常

HystrixObservableCommand.construct() — 返回發出響應或發送onError通知的可觀察對象

    如果run()或construct()方法超過了命令的超時值,線程將拋出TimeoutException(如果命令本身不在自己的線程中運行,則另一個計時器線程將拋出TimeoutException)。在這種情況下,Hystrix將響應路由到8。獲取回退,如果該方法沒有取消/中斷,則它將丟棄最終返回值run()或construct()方法。
請注意,沒有辦法強制潛在線程停止工作——Hystrix在JVM上能做的最好的事情就是拋出一個InterruptedException異常。如果         Hystrix包裝的工作不尊重InterruptedExceptions,那麼Hystrix線程池中的線程將繼續它的工作,儘管客戶機已經收到一個TimeoutException異常。這種行爲會使Hystrix線程池飽和,儘管負載被“正確地釋放”。大多數Java HTTP客戶端庫不解釋InterruptedExceptions。因此,請確保正確配置HTTP客戶機上的連接和讀寫超時。

如果該命令沒有拋出任何異常並返回響應,那麼Hystrix在執行一些日誌記錄和指標報告之後將返回此響應。在run()的情況下,Hystrix返回一個可觀察的對象,該對象發出單個響應,然後發出onCompleted通知;對於construct(), Hystrix返回與construct()返回的相同的Observable。

7. Calculate Circuit Health

Hystrix向斷路器報告成功、失敗、拒絕和超時,斷路器維護一組計算統計信息的滾動計數器。
它使用這些統計數據來確定電路應該在什麼時候“跳閘”,在這一點上,它將短路任何後續的請求,直到恢復週期結束,在此期間,它在第一次檢查某些健康檢查之後再次關閉電路。

8. Get the Fallback

Hystrix試圖恢復你的回滾命令執行失敗時:當一個異常的構造()或()運行(6),當命令電路短路,因爲打開(4),當命令的線程池和隊列或信號能力(5),或者當命令已超過其超時長度。

編寫回退,以便在不依賴任何網絡的情況下,從內存緩存或通過其他靜態邏輯提供通用響應。如果必須在回退中使用網絡調用,則應該通過另一個HystrixCommand或HystrixObservableCommand來實現。

對於HystrixCommand,要提供回退邏輯,需要實現HystrixCommand. getfallback(),它返回一個回退值。

在HystrixObservableCommand的情況下,要提供回退邏輯,需要實現HystrixObservableCommand. resumewithfallback(),它返回一個或多個可回退值。

如果回退方法返回一個響應,那麼Hystrix將把這個響應返回給調用者。對於HystrixCommand.getFallback(),它將返回一個發出方法返回值的Observable。對於HystrixObservableCommand.resumeWithFallback(),它將返回與方法返回的Observable相同的值。

如果您沒有爲您的Hystrix命令實現一個回退方法,或者回退本身拋出一個異常,那麼Hystrix仍然返回一個可觀察的異常,但是這個異常不會發出任何內容,並立即以onError通知結束。正是通過這個onError通知,導致命令失敗的異常才被傳輸回調用者。(實現可能失敗的回退實現是一種糟糕的實踐。您應該實現回退,這樣它就不會執行任何可能失敗的邏輯。

失敗或不存在回退的結果將根據您調用Hystrix命令的方式而有所不同:

  1. execute()——拋出異常
  2. queue() -成功返回一個Future,但是如果調用該Future的get()方法,該Future將拋出一個異常
  3. observe() -返回一個Observable,當您訂閱它時,它將通過調用訂閱者的onError方法立即終止
  4. toObservable()——返回一個Observable,當您訂閱它時,它將通過調用訂閱者的onError方法來終止

9. Return the Successful Response

如果Hystrix命令成功,它將以可觀察的形式向調用者返回一個或多個響應。根據您在上面步驟2中調用命令的方式,這個可觀察的對象可能在返回給您之前被轉換:

  1. execute()—以與.queue()相同的方式獲取Future,然後對該Future調用get()以獲取可觀察對象發出的單個值
  2. queue() -將觀察到的對象轉換爲BlockingObservable,以便將其轉換爲Future,然後返回該Future
  3. observer()—立即訂閱被觀察對象,並開始執行該命令的流;返回一個可觀察到的,當您訂閱它時,它將回放排放和通知
  4. toObservable() -返回未改變的Observable;您必須訂閱它,以便實際開始執行該命令的流

斷路器

下圖顯示了HystrixCommand或HystrixObservableCommand如何與hystrixcircuit斷路器交互,以及它的邏輯和決策流程,包括計數器在斷路器中的行爲。

電路開閉的精確方式如下:

  1. 假設通過一個電路的音量達到某個閾值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())…
  2. 假設錯誤百分比超過閾值錯誤百分比(hystrixcommandproperties .circuit breakererrorthreshold oldpercentage())…
  3. 然後斷路器從閉合過渡到打開。
  4. 當它是開着的,它短路了所有對斷路器的要求。
  5. 經過一段時間後(hystrixcommandproperties .circuit breakersleepwindowin毫秒()),下一個請求被允許通過(這是半打開狀態)。如果請求失敗,斷路器在休眠窗口期間返回到打開狀態。如果請求成功,斷路器轉換爲閉合,邏輯在1。一遍又一遍。

隔離

Hystrix使用bulkhead模式將依賴項彼此隔離,並限制對其中任何一個的併發訪問。

線程和線程池

客戶機(庫、網絡調用等)在不同的線程上執行。這將它們與調用線程(Tomcat線程池)隔離開來,以便調用者可以“離開”耗時過長的依賴項調用。

Hystrix使用單獨的、每個依賴項的線程池作爲約束任何給定依賴項的一種方式,因此底層執行的延遲將只會使該池中的可用線程飽和。

您可以在不使用線程池的情況下防止失敗,但是這要求受信任的客戶機非常快地失敗(網絡連接/讀取超時和重試配置),並且始終表現良好。

Netflix在設計Hystrix時,選擇使用線程和線程池來實現隔離,原因有很多,包括:

  1. 許多應用程序對許多不同團隊開發的許多不同服務執行數十個(有時甚至超過100個)不同的後端服務調用。
  2. 每個服務都提供自己的客戶端庫。
  3. 客戶端庫一直在變化。
  4. 客戶端庫邏輯可以更改以添加新的網絡調用。
  5. 客戶端庫可以包含重試、數據解析、緩存(內存中或跨網絡)等邏輯,以及其他此類行爲。
  6. 客戶端庫往往是“黑盒”——對用戶不透明的實現細節、網絡訪問模式、配置默認值等。
  7. 在幾次實際生產中斷中,確定是“噢,發生了一些更改,應該調整屬性”或“客戶端庫更改了其行爲”。
  8. 即使客戶機本身沒有更改,服務本身也會更改,這會影響性能特徵,從而導致客戶機配置無效。
  9. 傳遞依賴項可以引入其他不期望的、可能沒有正確配置的客戶端庫。
  10. 大多數網絡訪問是同步執行的。
  11. 失敗和延遲也可能發生在客戶端代碼中,而不僅僅是在網絡調用中。

線程池的好處

通過線程在它們自己的線程池中進行隔離的好處是:

  1. 應用程序完全受失控客戶端庫的保護。給定依賴項庫的池可以填滿,而不會影響應用程序的其餘部分。
  2. 應用程序可以以低得多的風險接受新的客戶端庫。如果出現問題,則將其隔離到庫中,不會影響其他所有內容。
  3. 當失敗的客戶機再次恢復健康時,線程池將清空,應用程序將立即恢復健康的性能,而不是在整個Tomcat容器不堪重負時進行長時間的恢復。
  4. 如果客戶端庫配置錯誤,線程池的健康狀況將很快證明這一點(通過增加錯誤、延遲、超時、拒絕等等),您可以在不影響應用程序功能的情況下處理它(通常是通過動態屬性實時處理)。
  5. 如果客戶服務更改性能特徵(經常發生足以成爲問題)進而導致需要調整屬性(增加/減少超時,改變重試,等等)又可以通過線程池指標(錯誤、延遲、超時、拒絕),可以處理而不影響其他客戶,請求,或用戶。
  6. 除了隔離的好處之外,擁有專用的線程池還提供了內置的併發性,可以利用併發性在同步客戶端庫之上構建異步facade(類似於Netflix API如何在Hystrix命令之上構建響應性的、完全異步的Java API)。

簡而言之,線程池提供的隔離允許客戶端庫和子系統性能特徵的不斷變化和動態組合被優雅地處理,而不會導致停機。

注意:儘管單獨的線程提供了隔離,但是您的底層客戶端代碼也應該有超時和/或響應線程中斷,這樣它就不會無限期地阻塞和飽和Hystrix線程池。

線程池的缺點

線程池的主要缺點是增加了計算開銷。每個命令的執行都涉及到在單獨的線程上運行命令所涉及的隊列、調度和上下文切換。

Netflix在設計這個系統時,決定接受這種開銷的成本,以換取它所提供的好處,並認爲它足夠小,不會對成本或性能產生重大影響。

線程的成本

Hystrix測量子線程上執行構造()或run()方法時的延遲,以及父線程上的端到端總時間。通過這種方式,您可以看到Hystrix開銷的成本(線程、指標、日誌記錄、斷路器等)。

Netflix API每天使用線程隔離處理100多億次Hystrix命令執行。每個API實例有40多個線程池,每個線程池中有5-20個線程(大多數被設置爲10個)。

下圖表示在單個API實例上以每秒60個請求的速度執行一個HystrixCommand(每個服務器每秒執行大約350個線程):

在中值(或更低)處,擁有單獨的線程沒有成本。
在第90個百分位上,使用一個單獨的線程需要花費3ms。
在第99百分位上,使用一個單獨的線程需要花費9ms。但是請注意,成本的增加遠遠小於單獨線程(網絡請求)執行時間的增加,後者從2跳到28,而成本從0跳到9。
對於像這樣的電路,這種90%以上的開銷被認爲是可以接受的,因爲Netflix的大多數用例都實現了彈性。
電路,包裝非常低延遲請求(比如那些主要是內存中的緩存)的開銷可以過高,在這些情況下你可以使用另一種方法,比如tryable信號量,雖然他們不允許超時,提供大部分的彈性福利沒有開銷。然而,總的來說,開銷非常小,以至於Netflix在實踐中通常更喜歡單獨線程的隔離優勢,而不是這種技術。

信號量

您可以使用信號量(或計數器)來限制對任何給定依賴項的併發調用的數量,而不是使用線程池/隊列大小。這允許Hystrix在不使用線程池的情況下釋放負載,但不允許超時和退出。如果您信任客戶機,並且只希望減少負載,那麼可以使用這種方法。

HystrixCommand和HystrixObservableCommand在兩個地方支持信號量:

  1. 回退:當Hystrix檢索回退時,它總是在調用Tomcat線程上這樣做。
  2. 執行:如果您設置屬性execute .isolation。然後Hystrix將使用信號量而不是線程來限制調用該命令的併發父線程的數量。

您可以通過定義可以執行多少併發線程的動態屬性來配置這兩種信號量的使用。您應該使用與調整線程池大小時類似的計算來確定它們的大小(以亞毫秒爲單位返回的內存調用在信號量僅爲1或2的情況下可以執行5000rps以上的性能,但默認值是10)。
注意:如果依賴項與信號量隔離,然後成爲潛伏的,那麼父線程將一直被阻塞,直到底層網絡調用超時。
一旦達到限制,信號量拒絕將開始,但是填充信號量的線程不能離開。

請求崩潰

您可以使用一個請求摺疊器(hystrix摺疊器是抽象父命令)來前置一個HystrixCommand,使用該命令可以將多個請求摺疊成一個後端依賴項調用。
下圖顯示了兩種情況下的線程和網絡連接數量:首先沒有請求崩潰,然後請求崩潰(假設所有連接在短時間窗口內都是“併發”的,在本例中是10ms)。

 

爲什麼使用請求崩潰?

使用請求崩潰來減少執行HystrixCommand併發執行所需的線程和網絡連接數量。請求崩潰以一種自動化的方式來實現這一點,它不會強迫代碼庫的所有開發人員協調請求的手動批處理。

全局上下文(跨所有Tomcat線程)

理想的崩潰類型是在全局應用程序級別上完成的,這樣來自任何Tomcat線程上的任何用戶的請求都可以一起崩潰。
例如,如果您將HystrixCommand配置爲支持將請求批處理給檢索電影評級的依賴項的任何用戶,那麼當同一JVM中的任何用戶線程發出這樣的請求時,Hystrix將把它的請求和其他請求一起添加到同一個崩潰的網絡調用中。
請注意,摺疊器將向摺疊的網絡調用傳遞單個HystrixRequestContext對象,因此下游系統必須處理這種情況,以使其成爲有效的選項。

用戶請求上下文(單個Tomcat線程)

如果您將HystrixCommand配置爲只處理單個用戶的批處理請求,那麼Hystrix可以從單個Tomcat線程(請求)中摺疊請求。
例如,如果用戶希望爲300個視頻對象加載書籤,而不是執行300個網絡調用,Hystrix可以將它們合併爲一個。

對象建模和代碼複雜性

有時,當您創建一個對象模型,該模型對對象的使用者具有邏輯意義時,這與對象的生產者對資源的有效利用並不匹配。
例如,給定一個包含300個視頻對象的列表,遍歷它們並在每個對象上調用getSomeAttribute()顯然是一個對象模型,但是如果天真地實現,可能會導致300個網絡調用在毫秒內完成(並且很可能會使資源飽和)。
有一些手動的方法可以處理這個問題,比如在允許用戶調用getSomeAttribute()之前,要求他們聲明他們想要爲哪些視頻對象獲取屬性,以便它們都可以被預先獲取。

939/5000  
或者,您可以分割對象模型,以便用戶必須從一個位置獲得視頻列表,然後從其他位置請求該視頻列表的屬性。
這些方法會導致笨拙的api和對象模型與心智模型和使用模式不匹配。當多個開發人員處理一個代碼庫時,它們還可能導致簡單的錯誤和效率低下,因爲對一個用例進行的優化可能會被另一個用例的實現和通過代碼的新路徑所破壞。
通過將摺疊邏輯下推到Hystrix層,無論您如何創建對象模型,調用的順序如何,或者不同的開發人員是否知道正在進行或甚至需要進行優化,都不重要。
getSomeAttribute()方法可以放在最適合它的任何位置,並以適合使用模式的任何方式進行調用,而摺疊器將自動將調用批處理到時間窗口中。

請求崩潰的代價是什麼?

啓用請求崩潰的代價是在實際命令執行之前延遲增加。最大成本是批處理窗口的大小。
如果您的命令執行時間中值爲5ms,並且批處理窗口爲10ms,那麼在最壞的情況下,執行時間可能變爲15ms。通常情況下,請求不會在打開時被提交到窗口,因此中間的懲罰是窗口時間的一半,在本例中是5ms。
判斷這個代價是否值得,取決於執行的命令。高延遲命令不會因爲額外的少量平均延遲而受到很大影響。此外,給定命令的併發量是關鍵:如果需要批處理的請求很少超過1或2個,那麼就沒有必要爲此付出代價。事實上,在單線程順序迭代中,崩潰將是一個主要的性能瓶頸,因爲每個迭代將等待10ms批處理窗口時間。
但是,如果一個特定的命令被大量併發地使用,並且可以批量處理數十個甚至數百個調用,那麼當Hystrix減少所需的線程數量和到依賴項的網絡連接數量時,增加的吞吐量通常會大大降低成本。

崩潰流

請求緩存

HystrixCommand和HystrixObservableCommand實現可以定義一個緩存鍵,然後使用該鍵以併發感知的方式在請求上下文中去欺騙調用。
下面是一個涉及HTTP請求生命週期和在該請求中執行工作的兩個線程的示例流:

 

請求緩存的好處是:

不同的代碼路徑可以執行Hystrix命令,而不必擔心重複的工作。
這在許多開發人員實現不同功能的大型代碼庫中特別有用。
例如,代碼中的多個路徑都需要獲取用戶的Account對象,每個路徑都可以像這樣請求它

Account account = new UserGetAccount(accountId).execute();

//or

Observable<Account> accountObservable = new UserGetAccount(accountId).observe();

Hystrix RequestCache將只執行一次底層run()方法,而且執行HystrixCommand的兩個線程將接收相同的數據,儘管它們實例化了不同的實例。

數據檢索在整個請求中是一致的。

與每次執行命令時可能返回不同的值(或回退)不同,將緩存第一個響應,併爲同一請求中的所有後續調用返回第一個響應。

消除重複的線程執行。

由於請求緩存位於構造()或run()方法調用之前,Hystrix可以在導致線程執行之前對調用進行反欺騙

如果Hystrix沒有實現請求緩存功能,那麼每個命令都需要在構造或run方法中自己實現它,該方法將在線程排隊並執行之後放置它。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章