標籤:切換 mon mha 容錯 排隊 oob 緩衝 離線 super
認識Hystrix
Hystrix是Netflix開源的一款容錯架構,包含常用的容錯方法:線程隔離、訊號量隔離、降級策略、熔斷技術。
在高並發訪問下,系統所依賴的服務的穩定性對系統的影響非常大,依賴有很多不可控的因素,比如網路連接變慢,資源突然繁忙,暫時不可用,服務離線等。我們要構建穩定、可靠的分布式系統,就必須要有這樣一套容錯方法。
本文主要討論區對話隔離技術。
為什麼要做線程隔離
比如我們現在有3個業務調用分別是查詢訂單、查詢商品、查詢使用者,且這三個業務請求都是依賴第三方服務-訂單服務、商品服務、使用者服務。三個服務均是通過RPC調用。當查詢訂單服務,假如線程阻塞了,這個時候後續有大量的查詢訂單請求過來,那麼容器中的線程數量則會持續增加直致CPU資源耗盡到100%,整個服務對外不可用,叢集環境下就是雪崩。如
訂單服務不可用.png
:
整個tomcat容器不可用.pngHystrix是如何通過線程池實現線程隔離的
Hystrix通過命令模式,將每個類型的業務請求封裝成對應的命令請求,比如查詢訂單->訂單Command,查詢商品->商品Command,查詢使用者->使用者Command。每個類型的Command對應一個線程池。建立好的線程池是被放入到ConcurrentHashMap中,比如查詢訂單:
final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();threadPools.put(“hystrix-order”, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
當第二次查詢訂單請求過來的時候,則可以直接從Map中擷取該線程池。具體流程如:
hystrix線程執行過程和非同步化.png
建立線程池中的線程的方法,查看原始碼如下:
public ThreadPoolExecutor getThreadPool(final HystrixThreadPoolKey threadPoolKey, HystrixProperty<Integer> corePoolSize, HystrixProperty<Integer> maximumPoolSize, HystrixProperty<Integer> keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { ThreadFactory threadFactory = null; if (!PlatformSpecific.isAppEngineStandardEnvironment()) { threadFactory = new ThreadFactory() { protected final AtomicInteger threadNumber = new AtomicInteger(0); @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, "hystrix-" + threadPoolKey.name() + "-" + threadNumber.incrementAndGet()); thread.setDaemon(true); return thread; } }; } else { threadFactory = PlatformSpecific.getAppEngineThreadFactory(); } final int dynamicCoreSize = corePoolSize.get(); final int dynamicMaximumSize = maximumPoolSize.get(); if (dynamicCoreSize > dynamicMaximumSize) { logger.error("Hystrix ThreadPool configuration at startup for : " + threadPoolKey.name() + " is trying to set coreSize = " + dynamicCoreSize + " and maximumSize = " + dynamicMaximumSize + ". Maximum size will be set to " + dynamicCoreSize + ", the coreSize value, since it must be equal to or greater than the coreSize value"); return new ThreadPoolExecutor(dynamicCoreSize, dynamicCoreSize, keepAliveTime.get(), unit, workQueue, threadFactory); } else { return new ThreadPoolExecutor(dynamicCoreSize, dynamicMaximumSize, keepAliveTime.get(), unit, workQueue, threadFactory); }}
執行Command的方式一共四種,直接看官方文檔(https://github.com/Netflix/Hystrix/wiki/How-it-Works),具體區別如下:
-
execute():以同步堵塞方式執行run()。調用execute()後,hystrix先建立一個新線程運行run(),接著調用程式要在execute()調用處一直堵塞著,直到run()運行完成。
-
queue():以非同步非堵塞方式執行run()。調用queue()就直接返回一個Future對象,同時hystrix建立一個新線程運行run(),調用程式通過Future.get()拿到run()的返回結果,而Future.get()是堵塞執行的。
-
observe():事件註冊前執行run()/construct()。第一步是事件註冊前,先調用observe()自動觸發執行run()/construct()(如果繼承的是HystrixCommand,hystrix將建立新線程非堵塞執行run();如果繼承的是HystrixObservableCommand,將以調用程式線程堵塞執行construct()),第二步是從observe()返回後調用程式調用subscribe()完成事件註冊,如果run()/construct()執行成功則觸發onNext()和onCompleted(),如果執行異常則觸發onError()。
-
toObservable():事件註冊後執行run()/construct()。第一步是事件註冊前,調用toObservable()就直接返回一個Observable<String>對象,第二步調用subscribe()完成事件註冊後自動觸發執行run()/construct()(如果繼承的是HystrixCommand,hystrix將建立新線程非堵塞執行run(),調用程式不必等待run();如果繼承的是HystrixObservableCommand,將以調用程式線程堵塞執行construct(),調用程式等待construct()執行完才能繼續往下走),如果run()/construct()執行成功則觸發onNext()和onCompleted(),如果執行異常則觸發onError()
註:
execute()和queue()是在HystrixCommand中,observe()和toObservable()是在HystrixObservableCommand 中。從底層實現來講,HystrixCommand其實也是利用Observable實現的(看Hystrix源碼,可以發現裡面大量使用了RxJava),儘管它只返回單個結果。HystrixCommand的queue方法實際上是調用了toObservable().toBlocking().toFuture(),而execute方法實際上是調用了queue().get()。
如何應用到實際代碼中
package myHystrix.threadpool;import com.netflix.hystrix.*;import org.junit.Test;import java.util.List;import java.util.concurrent.Future;/** * Created by wangxindong on 2017/8/4. */public class GetOrderCommand extends HystrixCommand<List> { OrderService orderService; public GetOrderCommand(String name){ super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ThreadPoolTestGroup")) .andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey")) .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(name)) .andCommandPropertiesDefaults( HystrixCommandProperties.Setter() .withExecutionTimeoutInMilliseconds(5000) ) .andThreadPoolPropertiesDefaults( HystrixThreadPoolProperties.Setter() .withMaxQueueSize(10) //配置隊列大小 .withCoreSize(2) // 配置線程池裡的線程數 ) ); } @Override protected List run() throws Exception { return orderService.getOrderList(); } public static class UnitTest { @Test public void testGetOrder(){// new GetOrderCommand("hystrix-order").execute(); Future<List> future =new GetOrderCommand("hystrix-order").queue(); } }}
總結
執行依賴代碼的線程與請求線程(比如Tomcat線程)分離,請求線程可以自由控制離開的時間,這也是我們通常說的非同步編程,Hystrix是結合RxJava來實現的非同步編程。通過設定線程池大小來控制並發訪問量,當線程飽和的時候可以拒絕服務,防止依賴問題擴散。
線程隔離.png
線程隔離的優點:
[1]:應用程式會被完全保護起來,即使依賴的一個服務的線程池滿了,也不會影響到應用程式的其他部分。
[2]:我們給應用程式引入一個新的風險較低的用戶端lib的時候,如果發生問題,也是在本lib中,並不會影響到其他內容,因此我們可以大膽的引入新lib庫。
[3]:當依賴的一個失敗的服務恢複正常時,應用程式會立即恢複正常的效能。
[4]:如果我們的應用程式一些參數配置錯誤了,線程池的健全狀態將會很快顯示出來,比如延遲、逾時、拒絕等。同時可以通過動態屬性即時執行來處理糾正錯誤的參數配置。
[5]:如果服務的效能有變化,從而需要調整,比如增加或者減少逾時時間,更改重試次數,就可以通過線程池指標動態屬性修改,而且不會影響到其他調用請求。
[6]:除了隔離優勢外,hystrix擁有專門的線程池可提供內建的並發功能,使得可以在同步調用之上構建非同步面板模式,這樣就可以很方便的做非同步編程(Hystrix引入了Rxjava非同步架構)。
儘管線程池提供了線程隔離,我們的用戶端底層代碼也必須要有逾時設定,不能無限制的阻塞以致線程池一直飽和。
線程隔離的缺點:
[1]:線程池的主要缺點就是它增加了計算的開銷,每個業務請求(被封裝成命令)在執行的時候,會涉及到請求排隊,調度和環境切換。不過Netflix公司內部認為線程隔離開銷足夠小,不會產生重大的成本或效能的影響。
The Netflix API processes 10+ billion Hystrix Command executions per day using thread isolation. Each API instance has 40+ thread-pools with 5–20 threads in each (most are set to 10).
Netflix API每天使用線程隔離處理10億次Hystrix Command執行。 每個API執行個體都有40多個線程池,每個線程池中有5-20個線程(大多數設定為10個)。
對於不依賴網路訪問的服務,比如只依賴記憶體緩衝這種情況下,就不適合用線程池隔離技術,而是採用訊號量隔離,後面文章會介紹。
因此我們可以放心使用Hystrix的線程隔離技術,來防止雪崩這種可怕的致命性線上故障。
轉載請註明出處,並附上連結 http://www.jianshu.com/p/df1525d58c20
參考資料:
https://github.com/Netflix/Hystrix/wiki
新棟BOOK
連結:http://www.jianshu.com/p/df1525d58c20
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
hystrix的線程隔離技術除了線程池,還有另外一種方式:訊號量。
線程池和訊號量的區別
在《Hystrix線程隔離技術解析-線程池》一文最後,我們談到了線程池的缺點,當我們依賴的服務是極低延遲的,比如訪問記憶體緩衝,就沒有必要使用線程池的方式,那樣的話開銷得不償失,而是推薦使用訊號量這種方式。下面這張圖說明了線程池隔離和訊號量隔離的主要區別:線程池方式下業務請求線程和執行依賴的服務的線程不是同一個線程;訊號量方式下業務請求線程和執行依賴服務的線程是同一個線程
訊號量和線程池的區別.png如何使用訊號量來隔離線程
將屬性execution.isolation.strategy設定為SEMAPHORE ,象這樣 ExecutionIsolationStrategy.SEMAPHORE,則Hystrix使用訊號量而不是預設的線程池來做隔離。
public class CommandUsingSemaphoreIsolation extends HystrixCommand<String> { private final int id; public CommandUsingSemaphoreIsolation(int id) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")) // since we‘re doing work in the run() method that doesn‘t involve network traffic // and executes very fast with low risk we choose SEMAPHORE isolation .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() .withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE))); this.id = id; } @Override protected String run() { // a real implementation would retrieve data from in memory data structure // or some other similar non-network involved work return "ValueFromHashMap_" + id; }}
總結
訊號量隔離的方式是限制了總的並發數,每一次請求過來,請求線程和調用依賴服務的線程是同一個線程,那麼如果不涉及遠程RPC調用(沒有網路開銷)則使用訊號量來隔離,更為輕量,開銷更小。
轉載請註明出處,並附上連結http://www.jianshu.com/p/af8dc67e5238
參考資料:https://github.com/Netflix/Hystrix/wiki
新棟BOOK
連結:http://www.jianshu.com/p/af8dc67e5238
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
Hystrix線程隔離技術解析-線程池(轉)