認(rèn)識(shí)Hystrix
Hystrix是Netflix開(kāi)源的一款容錯(cuò)框架,包含常用的容錯(cuò)方法:線程隔離妙同、信號(hào)量隔離砚蓬、降級(jí)策略矢门、熔斷技術(shù)。
在高并發(fā)訪問(wèn)下,系統(tǒng)所依賴的服務(wù)的穩(wěn)定性對(duì)系統(tǒng)的影響非常大祟剔,依賴有很多不可控的因素涩拙,比如網(wǎng)絡(luò)連接變慢眷唉,資源突然繁忙,暫時(shí)不可用,服務(wù)脫機(jī)等怔接。我們要構(gòu)建穩(wěn)定棋电、可靠的分布式系統(tǒng)橙困,就必須要有這樣一套容錯(cuò)方法依啰。
本文主要討論線程隔離技術(shù)。
為什么要做線程隔離
比如我們現(xiàn)在有3個(gè)業(yè)務(wù)調(diào)用分別是查詢訂單案训、查詢商品买置、查詢用戶,且這三個(gè)業(yè)務(wù)請(qǐng)求都是依賴第三方服務(wù)-訂單服務(wù)强霎、商品服務(wù)忿项、用戶服務(wù)。三個(gè)服務(wù)均是通過(guò)RPC調(diào)用城舞。當(dāng)查詢訂單服務(wù)轩触,假如線程阻塞了,這個(gè)時(shí)候后續(xù)有大量的查詢訂單請(qǐng)求過(guò)來(lái)家夺,那么容器中的線程數(shù)量則會(huì)持續(xù)增加直致CPU資源耗盡到100%脱柱,整個(gè)服務(wù)對(duì)外不可用,集群環(huán)境下就是雪崩拉馋。如下圖
:
Hystrix是如何通過(guò)線程池實(shí)現(xiàn)線程隔離的
Hystrix通過(guò)命令模式榨为,將每個(gè)類型的業(yè)務(wù)請(qǐng)求封裝成對(duì)應(yīng)的命令請(qǐng)求,比如查詢訂單->訂單Command煌茴,查詢商品->商品Command随闺,查詢用戶->用戶Command。每個(gè)類型的Command對(duì)應(yīng)一個(gè)線程池蔓腐。創(chuàng)建好的線程池是被放入到ConcurrentHashMap中矩乐,比如查詢訂單:
final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
threadPools.put(“hystrix-order”, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
當(dāng)?shù)诙尾樵冇唵握?qǐng)求過(guò)來(lái)的時(shí)候,則可以直接從Map中獲取該線程池回论。具體流程如下圖:
創(chuàng)建線程池中的線程的方法散罕,查看源代碼如下:
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);
}
}
執(zhí)行Command的方式一共四種,直接看官方文檔(https://github.com/Netflix/Hystrix/wiki/How-it-Works)傀蓉,具體區(qū)別如下:
execute():以同步堵塞方式執(zhí)行run()笨使。調(diào)用execute()后,hystrix先創(chuàng)建一個(gè)新線程運(yùn)行run()僚害,接著調(diào)用程序要在execute()調(diào)用處一直堵塞著,直到run()運(yùn)行完成。
queue():以異步非堵塞方式執(zhí)行run()萨蚕。調(diào)用queue()就直接返回一個(gè)Future對(duì)象靶草,同時(shí)hystrix創(chuàng)建一個(gè)新線程運(yùn)行run(),調(diào)用程序通過(guò)Future.get()拿到run()的返回結(jié)果岳遥,而Future.get()是堵塞執(zhí)行的奕翔。
observe():事件注冊(cè)前執(zhí)行run()/construct()。第一步是事件注冊(cè)前浩蓉,先調(diào)用observe()自動(dòng)觸發(fā)執(zhí)行run()/construct()(如果繼承的是HystrixCommand派继,hystrix將創(chuàng)建新線程非堵塞執(zhí)行run();如果繼承的是HystrixObservableCommand捻艳,將以調(diào)用程序線程堵塞執(zhí)行construct())驾窟,第二步是從observe()返回后調(diào)用程序調(diào)用subscribe()完成事件注冊(cè),如果run()/construct()執(zhí)行成功則觸發(fā)onNext()和onCompleted()认轨,如果執(zhí)行異常則觸發(fā)onError()绅络。
toObservable():事件注冊(cè)后執(zhí)行run()/construct()。第一步是事件注冊(cè)前嘁字,調(diào)用toObservable()就直接返回一個(gè)Observable<String>對(duì)象恩急,第二步調(diào)用subscribe()完成事件注冊(cè)后自動(dòng)觸發(fā)執(zhí)行run()/construct()(如果繼承的是HystrixCommand,hystrix將創(chuàng)建新線程非堵塞執(zhí)行run()纪蜒,調(diào)用程序不必等待run()衷恭;如果繼承的是HystrixObservableCommand,將以調(diào)用程序線程堵塞執(zhí)行construct()纯续,調(diào)用程序等待construct()執(zhí)行完才能繼續(xù)往下走)随珠,如果run()/construct()執(zhí)行成功則觸發(fā)onNext()和onCompleted(),如果執(zhí)行異常則觸發(fā)onError()
注:
execute()和queue()是在HystrixCommand中杆烁,observe()和toObservable()是在HystrixObservableCommand 中牙丽。從底層實(shí)現(xiàn)來(lái)講,HystrixCommand其實(shí)也是利用Observable實(shí)現(xiàn)的(看Hystrix源碼兔魂,可以發(fā)現(xiàn)里面大量使用了RxJava)烤芦,盡管它只返回單個(gè)結(jié)果。HystrixCommand的queue方法實(shí)際上是調(diào)用了toObservable().toBlocking().toFuture()析校,而execute方法實(shí)際上是調(diào)用了queue().get()构罗。
如何應(yīng)用到實(shí)際代碼中
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) //配置隊(duì)列大小
.withCoreSize(2) // 配置線程池里的線程數(shù)
)
);
}
@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();
}
}
}
總結(jié)
執(zhí)行依賴代碼的線程與請(qǐng)求線程(比如Tomcat線程)分離,請(qǐng)求線程可以自由控制離開(kāi)的時(shí)間智玻,這也是我們通常說(shuō)的異步編程遂唧,Hystrix是結(jié)合RxJava來(lái)實(shí)現(xiàn)的異步編程。通過(guò)設(shè)置線程池大小來(lái)控制并發(fā)訪問(wèn)量吊奢,當(dāng)線程飽和的時(shí)候可以拒絕服務(wù)盖彭,防止依賴問(wèn)題擴(kuò)散。
線程隔離的優(yōu)點(diǎn):
[1]:應(yīng)用程序會(huì)被完全保護(hù)起來(lái),即使依賴的一個(gè)服務(wù)的線程池滿了召边,也不會(huì)影響到應(yīng)用程序的其他部分铺呵。
[2]:我們給應(yīng)用程序引入一個(gè)新的風(fēng)險(xiǎn)較低的客戶端lib的時(shí)候,如果發(fā)生問(wèn)題隧熙,也是在本lib中片挂,并不會(huì)影響到其他內(nèi)容,因此我們可以大膽的引入新lib庫(kù)贞盯。
[3]:當(dāng)依賴的一個(gè)失敗的服務(wù)恢復(fù)正常時(shí)音念,應(yīng)用程序會(huì)立即恢復(fù)正常的性能。
[4]:如果我們的應(yīng)用程序一些參數(shù)配置錯(cuò)誤了躏敢,線程池的運(yùn)行狀況將會(huì)很快顯示出來(lái)闷愤,比如延遲、超時(shí)父丰、拒絕等肝谭。同時(shí)可以通過(guò)動(dòng)態(tài)屬性實(shí)時(shí)執(zhí)行來(lái)處理糾正錯(cuò)誤的參數(shù)配置。
[5]:如果服務(wù)的性能有變化蛾扇,從而需要調(diào)整攘烛,比如增加或者減少超時(shí)時(shí)間,更改重試次數(shù)镀首,就可以通過(guò)線程池指標(biāo)動(dòng)態(tài)屬性修改坟漱,而且不會(huì)影響到其他調(diào)用請(qǐng)求。
[6]:除了隔離優(yōu)勢(shì)外更哄,hystrix擁有專門的線程池可提供內(nèi)置的并發(fā)功能芋齿,使得可以在同步調(diào)用之上構(gòu)建異步的外觀模式,這樣就可以很方便的做異步編程(Hystrix引入了Rxjava異步框架)成翩。
盡管線程池提供了線程隔離觅捆,我們的客戶端底層代碼也必須要有超時(shí)設(shè)置,不能無(wú)限制的阻塞以致線程池一直飽和麻敌。
線程隔離的缺點(diǎn):
[1]:線程池的主要缺點(diǎn)就是它增加了計(jì)算的開(kāi)銷栅炒,每個(gè)業(yè)務(wù)請(qǐng)求(被包裝成命令)在執(zhí)行的時(shí)候,會(huì)涉及到請(qǐng)求排隊(duì)术羔,調(diào)度和上下文切換赢赊。不過(guò)Netflix公司內(nèi)部認(rèn)為線程隔離開(kāi)銷足夠小,不會(huì)產(chǎn)生重大的成本或性能的影響级历。
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執(zhí)行释移。 每個(gè)API實(shí)例都有40多個(gè)線程池,每個(gè)線程池中有5-20個(gè)線程(大多數(shù)設(shè)置為10個(gè))寥殖。
對(duì)于不依賴網(wǎng)絡(luò)訪問(wèn)的服務(wù)玩讳,比如只依賴內(nèi)存緩存這種情況下涩蜘,就不適合用線程池隔離技術(shù),而是采用信號(hào)量隔離熏纯,后面文章會(huì)介紹皱坛。
因此我們可以放心使用Hystrix的線程隔離技術(shù),來(lái)防止雪崩這種可怕的致命性線上故障豆巨。