為什么要做線程隔離
比如我們現在有3個業務調用分別是查詢訂單、查詢商品、查詢用戶,且這三個業務請求都是依賴第三方服務-訂單服務、商品服務、用戶服務。三個服務均是通過RPC調用。當查詢訂單服務,假如線程阻塞了,這個時候后續有大量的查詢訂單請求過來,那么容器中的線程數量則會持續增加直致CPU資源耗盡到100%,整個服務對外不可用,集群環境下就是雪崩。
Hystrix是如何通過線程池實現線程隔離的
Hystrix通過命令模式,將每個類型的業務請求封裝成對應的命令請求,比如查詢訂單->訂單Command,查詢商品->商品Command,查詢用戶->用戶Command。每個類型的Command對應一個線程池。創建好的線程池是被放入到ConcurrentHashMap中
1.依賴隔離概述
依賴隔離是Hystrix的核心目的。依賴隔離其實就是資源隔離,把對依賴使用的資源隔離起來,統一控制和調度。那為什么需要把資源隔離起來呢?主要有以下幾點:
1.合理分配資源,把給資源分配的控制權交給用戶,某一個依賴的故障不會影響到其他的依賴調用,訪問資源也不受影響。
2.可以方便的指定調用策略,比如超時異常,熔斷處理。
3.對依賴限制資源也是對下游依賴起到一個保護作用,避免大量的并發請求在依賴服務有問題的時候造成依賴服務癱瘓或者更糟的雪崩效應。
4.對依賴調用進行封裝有利于對調用的監控和分析,類似于hystrix-dashboard的使用。
Hystrix提供了兩種依賴隔離方式:線程池隔離 和 信號量隔離。如下圖,線程池隔離,Hystrix可以為每一個依賴建立一個線程池,使之和其他依賴的使用資源隔離,同時限制他們的并發訪問和阻塞擴張。每個依賴可以根據權重分配資源(這里主要是線程),每一部分的依賴出現了問題,也不會影響其他依賴的使用資源。
2.線程池隔離
如果簡單的使用異步線程來實現依賴調用會有如下問題:1、線程的創建和銷毀;2、線程上下文空間的切換,用戶態和內核態的切換帶來的性能損耗。
使用線程池的方式可以解決第一種問題,但是第二個問題計算開銷是不能避免的。Netflix在使用過程中詳細評估了使用異步線程和同步線程帶來的性能差異,結果表明在99%的情況下,異步線程帶來的幾毫秒延遲的完全可以接受的。
3.線程池隔離的優缺點
優點:
- 一個依賴可以給予一個線程池,這個依賴的異常不會影響其他的依賴。
- 使用線程可以完全隔離第三方代碼,請求線程可以快速返回。
- 當一個失敗的依賴再次變成可用時,線程池將清理,并立即恢復可用,而不是一個長時間的恢復。
- 可以完全模擬異步調用,方便異步編程。
- 使用線程池,可以有效的進行實時監控、統計和封裝。
缺點:
- 使用線程池的缺點主要是增加了計算的開銷。每一個依賴調用都會涉及到隊列,調度,上下文切換,而這些操作都有可能在不同的線程中執行。
4.Command Name&Command Group
Hystrix使用Command模式對依賴調用進行封裝。當我們寫一個調用繼承HystrixCommand的時候,可以指定一個名稱Command Name。如果不指定Hystrix將會使用getClass().getSimpleName()來默認獲取。如果要指定,可以使用如下代碼,使用HystrixCommandKey.Factory幫助類在構造函數中指定。
public HelloWorldCommand(String name)
{
//定義命令組 和 方法調用超時時間
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorldCommand")));
this.name = name;
}
而Command Group可以把一組Command歸為一組。如上例代碼,可以使用
HystrixCommandGroupKey.Factory.asKey來指定Command Group。一般情況下,邏輯上是同一類型的會放在同一個Command Group中。比如,獲取用戶相關信息的依賴可以放在一起。
雖然Hystrix可以為每個依賴建立一個線程池,但是如果依賴成千上萬,建立那么多線程池肯定是不可能的。所以默認情況下,Hystrix會為每一個Command Group建立一個線程池。
5.Command Thread Pool
Hystrix可以指定創建或關聯上一個線程池,每一個線程池都有一個Key。這個線程池就是線程隔離的關鍵,所有的監控、緩存、調用等等都來自于這個線程池。可以通過如下代碼指定線程池:
public HelloWorldCommand(String name)
{
//定義命令組 和 方法調用超時時間
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorldCommand"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
this.name = name;
}
上文說到,默認情況下,每一個Command Group會自動創建一個線程池。那什么時候我們需要單獨指定線程池呢?因為線程池主要的目的是隔離,所以當有一些依賴在一個Command Group中,但是又有隔離的必要的時候,比如一個依賴的超時會用滿所有的線程池線程,而不應該影響其他的依賴。
6.基本實現原理
Command模式:Hystrix中大量使用rxJAVA來實現Command模式。所有自定義的Command,不管繼承于HystrixObservableCommand還是HystrixCommand,最終都繼承于AbstractCommand。Thread Pool,Command Group,Command Key都在AbstractCommand這里實現。
線程池的創建和管理:Hystrix的線程池在
HystrixConcurrencyStrategy初始化,線程池是由ThreadPoolExecutor實現的。每個線程池默認初始化10個線程。Hystrix有個靜態類Factory,創建的線程池會被存儲在Factory中的ConcurrentHashMap中。ConcurrentHashMap的Key則是上文說到的CommandGroupKey或者指定的ThreadPoolKey。每次命令執行的時候,都會根據ThreadPoolKey去找到對應的線程池。線程池擁有一個繼承于rxjava中Scheduler的HystrixContextScheduler,用于在執行命令的時候,把命令在這個線程池上調度執行。
命令的執行:以execute()方法為例,Hystrix通過toObservable()來構造命令,構造過程中,定義了整個命令執行過程中的stage(未開始、執行中、完成執行、執行異常等等)的回調和處理方法。在
executeCommandWithSpecifiedIsolation()方法中,使用exjava的subscribeOn方法,傳入上文提到的HystrixContextScheduler對象,通過HystrixContextScheduler的ThreadPoolScheduler把命令submit到ThreadPoolExecutor中去執行。
7.最佳實踐
對于那些本來延遲就比較小的請求(例如訪問本地緩存成功率很高的請求)來說,線程池帶來的開銷是非常高的,這時,你可以考慮采用其他方法,例如非阻塞信號量(不支持超時),來實現依賴服務的隔離,使用信號量的開銷很小。但絕大多數情況下,Netflix 更偏向于使用線程池來隔離依賴服務,因為其帶來的額外開銷可以接受,并且能支持包括超時在內的所有功能。