面試官:為什么需要 Hystrix?
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
成功路上并不擁擠,因為堅持的人不多。
編輯:業(yè)余草
juejin.cn/post/6955201079017472014
推薦:https://www.xttblog.com/?p=5182
為什么需要hystrix
Hystrix 官網(wǎng)地址 Github:https://github.com/Netflix/Hystrix/
Hystrix同樣是netfix公司在分布式系統(tǒng)中的貢獻(xiàn)。同樣的也進(jìn)入的不維護(hù)階段。不維護(hù)不代表被淘汰。只能說明推陳出新技術(shù)在不斷迭代。曾今的輝煌曾經(jīng)的設(shè)計還是值得我們?nèi)W(xué)習(xí)的。
在分布式環(huán)境中,服務(wù)調(diào)度是特色也是頭疼的一塊。在服務(wù)治理章節(jié)我們介紹了服務(wù)治理的功能。前一課我們也介紹了ribbon、feign進(jìn)行服務(wù)調(diào)用?,F(xiàn)在自然的到了服務(wù)監(jiān)控管理了。hystrix就是對服務(wù)進(jìn)行隔離保護(hù)。以實現(xiàn)服務(wù)不會出現(xiàn)連帶故障。導(dǎo)致整個系統(tǒng)不可用

如上圖所示,當(dāng)多個客戶端進(jìn)行服務(wù)調(diào)用Aservice時,而在分布式系統(tǒng)中Aservice存在三臺服務(wù),其中Aservice某些邏輯需要Bservice處理。Bservice在分布式系統(tǒng)中部署了兩臺服務(wù)。這個時候因為網(wǎng)絡(luò)問題導(dǎo)致Aservice中有一臺和Bservice的通信異常。如果Bservice是做日志處理的。在整個系統(tǒng)看來日志丟了和系統(tǒng)宕機比起來應(yīng)該無所謂了。但是這個時候因為網(wǎng)絡(luò)通信問題導(dǎo)致Aservice整個服務(wù)不可用了。有點得不嘗試。

在看上圖 。A-->B-->C-->D 。此時D服務(wù)宕機了。C因為D宕機出現(xiàn)處理異常。但是C的線程卻還在為B響應(yīng)。這樣隨著并發(fā)請求進(jìn)來時,C服務(wù)線程池出現(xiàn)爆滿導(dǎo)致CPU上漲。在這個時候C服務(wù)的其他業(yè)務(wù)也會受到CPU上漲的影響導(dǎo)致響應(yīng)變慢。
特色功能
Hystrix是一個低延遲和容錯的第三方組件庫。旨在隔離遠(yuǎn)程系統(tǒng)、服務(wù)和第三方庫的訪問點。官網(wǎng)上已經(jīng)停止維護(hù)并推薦使用resilience4j:https://github.com/resilience4j/resilience4j。 但是國內(nèi)的話我們有springcloud alibaba。
Hystrix 通過隔離服務(wù)之間的訪問來實現(xiàn)分布式系統(tǒng)中延遲及容錯機制來解決服務(wù)雪崩場景并且基于hystrix可以提供備選方案(fallback)。
對網(wǎng)絡(luò)延遲及故障進(jìn)行容錯
阻斷分布式系統(tǒng)雪崩
快速失敗并平緩恢復(fù)
服務(wù)降級
實時監(jiān)控、警報

上面試官網(wǎng)給出的一個統(tǒng)計。在30臺服務(wù)中每臺出現(xiàn)異常的概覽是0.01%。一億個請求就會有300000失敗。這樣換算下每個月至少有2小時停機。這對于互聯(lián)網(wǎng)系統(tǒng)來說是致命的。

上圖是官網(wǎng)給出的兩種情況。和我們上章節(jié)的類似。都是介紹服務(wù)雪崩的場景。
項目準(zhǔn)備
在openfeign專題中我們就探討了基于feign實現(xiàn)的服務(wù)熔斷當(dāng)時說了內(nèi)部就是基于hystrix。當(dāng)時我們也看了pom內(nèi)部的結(jié)構(gòu)在eureka中內(nèi)置ribbon的同時也內(nèi)置了hystrix模塊。

雖然包里面包含了hystrix 。我們還是引入對應(yīng)的start開啟相關(guān)配置吧。這里其實就是在openfeign專題中的列子。在那個專題我們提供了PaymentServiceFallbackImpl、PaymentServiceFallbackFactoryImpl兩個類作為備選方案。不過當(dāng)時我們只需指出openfeign支持設(shè)置兩種方式的備選方案。今天我們
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>演示下傳統(tǒng)企業(yè)沒有備選方案的情況會發(fā)生什么災(zāi)難。


接口測試
首先我們對payment#createByOrder接口進(jìn)行測試。查看下響應(yīng)情況

接口響應(yīng)情況 在測試payment#getTimeout/id方法。

postman接口調(diào)用 
jmeter壓測 
tomcat最大線程數(shù)測試 spring中默認(rèn)的tomcat的最大線程數(shù)是200.為了保護(hù)我們辛苦的筆記本。這里我們將線程數(shù)設(shè)置小點。這樣我們更容易復(fù)現(xiàn)線程被打滿的情況。線程滿了就會影響到payment#createByOrder接口。
現(xiàn)在我們用jmeter來壓測payment#getTimeOut/id這個接口。一位需要4S等待會照成資源消耗殆盡問題。這個時候我們的payment#createByOrder也會被阻塞。
上面我們壓測的是payment的原生接口。如果壓測的是order模塊。如果沒有在openfeign中配置fallback。那么order服務(wù)就會因為payment#getTimeOut/id接口并發(fā)導(dǎo)致線程滿了從而導(dǎo)致order模塊響應(yīng)緩慢。這就是雪崩效應(yīng)。下面我們從兩個方面來解決雪崩的發(fā)生。
業(yè)務(wù)隔離
上面的場景發(fā)生是因為payment#createByOrder 和payment#getTimeOut/id同屬于payment服務(wù)。一個payment服務(wù)實際上就是一個Tomcat服務(wù)。同一個tomcat服務(wù)是有一個線程池的。每次請求落到該tomcat 服務(wù)里就會去線程池中申請線程。獲取到線程了才能由線程來處理請求的業(yè)務(wù)。就是因為tomcat內(nèi)共享線程池。所以當(dāng)payment#getTimeOut/id并發(fā)上來后就會搶空線程池。導(dǎo)致別的借口甚至是毫不相關(guān)的接口都沒有資源可以申請。只能干巴巴的等待資源的釋放。
這就好比上班高峰期乘坐電梯因為某一個公司集中上班導(dǎo)致一段時間電梯全部被使用了。這時候國家領(lǐng)導(dǎo)過來也沒辦法上電梯。
我們也知道這種情況很好解決。每個園區(qū)都會有專用電梯供特殊使用。
我們解決上述問題也是同樣的思路。進(jìn)行隔離。不同的接口有不同的線程池。這樣就不會造成雪崩。
線程隔離

還記得我們上面為了演示并發(fā)將order模塊的最大線程數(shù)設(shè)置為10.這里我們通過測試工具調(diào)用下order/getpayment/1這個接口看看日志打印情況

接口日志 我們接口調(diào)用的地方將當(dāng)前線程打印出來。我們可以看到一只都是那10個線程在來回的使用。這也是上面為什么會造成雪崩現(xiàn)象。
@HystrixCommand(
groupKey = "order-service-getPaymentInfo",
commandKey = "getPaymentInfo",
threadPoolKey = "orderServicePaymentInfo",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1000")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize" ,value = "6"),
@HystrixProperty(name = "maxQueueSize",value = "100"),
@HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")
},
fallbackMethod = "getPaymentInfoFallback"
)
@RequestMapping(value = "/getpayment/{id}",method = RequestMethod.GET)
public ResultInfo getPaymentInfo(@PathVariable("id") Long id) {
log.info(Thread.currentThread().getName());
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id, ResultInfo.class);
}
public ResultInfo getPaymentInfoFallback(@PathVariable("id") Long id) {
log.info("已經(jīng)進(jìn)入備選方案了,下面交由自由線程執(zhí)行"+Thread.currentThread().getName());
return new ResultInfo();
}
@HystrixCommand(
groupKey = "order-service-getpaymentTimeout",
commandKey = "getpaymentTimeout",
threadPoolKey = "orderServicegetpaymentTimeout",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "10000")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize" ,value = "3"),
@HystrixProperty(name = "maxQueueSize",value = "100"),
@HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")
}
)
@RequestMapping(value = "/getpaymentTimeout/{id}",method = RequestMethod.GET)
public ResultInfo getpaymentTimeout(@PathVariable("id") Long id) {
log.info(Thread.currentThread().getName());
return orderPaymentService.getTimeOut(id);
}
這里演示效果不好展示,我就直接展示數(shù)據(jù)吧。

如果我們將hystrix加載payment原生服務(wù)就不會出現(xiàn)上面第三條情況。為什么我會放在order上就是想讓大家看看雪崩的場景。在并發(fā)50的時候因為payment設(shè)置的最大線程也是10,他本身也是有吞吐量的。在order#getpyament/id接口雖然在order模塊因為hystrix線程隔離有自己的線程運行,但是因為原生服務(wù)不給力導(dǎo)致自己調(diào)用超時從而影響運行的效果。這樣演示也是為了后續(xù)引出fallback解決雪崩的一次場景模擬吧。
我們可以在payment服務(wù)中通過hystrix設(shè)置fallback。保證payment服務(wù)低延遲從而保證order模塊不會因為payment自己緩慢導(dǎo)致order#getpayment這種正常接口異常。
還有一點雖然通過hystrix進(jìn)行線程隔離了。但是我們在運行其他接口時響應(yīng)時間也會稍長點。因為CPU在進(jìn)行線程切換的時候是有開銷的。這一點也是痛點。我們并不能隨心所欲的進(jìn)行線程隔離的。這就引出我們的信號量隔離了。
信號量隔離
關(guān)于信號量隔離這里也就不演示了。演示的意義不是很大
@HystrixCommand(
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1000"),
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,value = "SEMAPHORE"),
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,value = "6")
},
fallbackMethod = "getPaymentInfoFallback"
)
我們?nèi)缟吓渲帽硎拘盘柫孔畲鬄? 。表示并發(fā)6之后就會進(jìn)行等待。等待超時時間未1s。

除了線程隔離、信號量隔離等隔離手段我們可以通過請求合并、接口數(shù)據(jù)緩存等手段加強穩(wěn)定性。
服務(wù)降級
觸發(fā)條件
程序發(fā)生除HystrixBadRequestException異常。
服務(wù)調(diào)用超時
服務(wù)熔斷
線程池、信號量不夠

在上面我們的timeout接口。不管是線程隔離還是信號量隔離在條件滿足的時候就會直接拒絕后續(xù)請求。這樣太粗暴了。上面我們也提到了fallback。
還記的上面我們order50個并發(fā)的timeout的時候會導(dǎo)致getpayment接口異常,當(dāng)時定位了是因為原生payment服務(wù)壓力撐不住導(dǎo)致的。如果我們在payment上加入fallback就能保證在資源不足的時候也能快速響應(yīng)。這樣至少能保證order#getpayment方法的可用性。

配置fallback 但是這種配置屬于實驗性配置。在真實生產(chǎn)中我們不可能在每個方法上配置fallback的。這樣愚蠢至極。
hystrix除了在方法上特殊定制的fallback以外,還有一個全局的fallback。只需要在類上通過
@DefaultProperties(defaultFallback = "globalFallback")來實現(xiàn)全局的備選方案。一個方法滿足觸發(fā)降級的條件時如果該請求對應(yīng)的HystrixCommand注解中沒有配置fallback則使用所在類的全局fallback。如果全局也沒有則拋出異常。
不足
雖然
DefaultProperties可以避免每個接口都配置fallback。但是這種的全局好像還不是全局的fallback。我們還是需要每個類上配置fallback。筆者查閱了資料好像也沒有但是在openfeign專題里我們說了openfeign結(jié)合hystrix實現(xiàn)的服務(wù)降級功能。還記的里面提到了一個
FallbackFactory這個類嗎。這個類可以理解成spring的BeanFactory。這個類是用來產(chǎn)生我們所需要的FallBack的。我們在這個工廠里可以生成一個通用類型的fallback的代理對象。代理對象可以根據(jù)代理方法的方法簽名進(jìn)行入?yún)⒑统鰠ⅰ?/p>這樣我們可以在所有的openfeign地方配置這個工廠類。這樣的話就避免的生成很多個fallback。美中不足的還是需要每個地方都指定一下。關(guān)于
FallBackFactory感興趣的可以下載源碼查看或者進(jìn)主頁查看openfeign專題。
服務(wù)熔斷
@HystrixCommand(
commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"), //是否開啟斷路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), //請求次數(shù)
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), //時間范圍
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"), //失敗率達(dá)到多少后跳閘
},
fallbackMethod = "getInfoFallback"
)
@RequestMapping(value = "/get", method = RequestMethod.GET)
public ResultInfo get(@RequestParam Long id) {
if (id < 0) {
int i = 1 / 0;
}
log.info(Thread.currentThread().getName());
return orderPaymentService.get(id);
}
public ResultInfo getInfoFallback(@RequestParam Long id) {
return new ResultInfo();
}
首先我們通過circuitBreaker.enabled=true開啟熔斷器
circuitBreaker.requestVolumeThreshold設(shè)置統(tǒng)計請求次數(shù)circuitBreaker.sleepWindowInMilliseconds設(shè)置時間滑動單位 , 在觸發(fā)熔斷后多久進(jìn)行嘗試開放,及俗稱的半開狀態(tài)circuitBreaker.errorThresholdPercentage設(shè)置觸發(fā)熔斷開關(guān)的臨界條件上面的配置如果最近的10次請求錯誤率達(dá)到60% ,則觸發(fā)熔斷降級 , 在10S內(nèi)都處于熔斷狀態(tài)服務(wù)進(jìn)行降級。10S后半開嘗試獲取服務(wù)最新狀態(tài)
下面我們通過jmeter進(jìn)行接口
http://localhost/order/get?id=-1進(jìn)行20次測試。雖然這20次無一例額外都會報錯。但是我們會發(fā)現(xiàn)一開始報錯是因為我們代碼里的錯誤。后面的錯誤就是hystrix熔斷的錯誤了。一開始試by zero 錯誤、后面就是short-circuited and fallback failed 熔斷錯誤了

正常我們在hystrix中會配置fallback , 關(guān)于fallback兩種方式我們上面降級章節(jié)已經(jīng)實現(xiàn)了。這里是為了方便看到錯誤的不同特意放開了。

在HystrixCommand中配置的參數(shù)基本都是在HystrixPropertiesManager對象中。我們可以看到關(guān)于熔斷器的配置有6個參數(shù)?;揪褪俏覀兩厦娴乃膫€配置
服務(wù)限流
服務(wù)降級我們上面提到的兩種隔離就是實現(xiàn)限流的策略。
請求合并
除了熔斷、降級、限流意外hystrix還為我們提供了請求合并。顧名思義就是將多個請求合并成一個請求已達(dá)到降低并發(fā)的問題。
比如說我們order有個接個是查詢當(dāng)個訂單信息
order/getId?id=1突然有一萬個請求過來。為了緩解壓力我們集中一下請求每100個請求調(diào)用一次order/getIds?ids=xxxxx。這樣我們最終到payment模塊則是10000/100=100個請求。下面我們通過代碼配置實現(xiàn)下請求合并。
HystrixCollapser
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HystrixCollapser {
String collapserKey() default "";
String batchMethod();
Scope scope() default Scope.REQUEST;
HystrixProperty[] collapserProperties() default {};
}


在Hystrix中所有的properties配置都會在HystrixPropertiesManager.java中。我們在里面可以找到Collapser只有兩個相關(guān)的配置。分別表示最大請求數(shù)和統(tǒng)計時間單元。
@HystrixCollapser(
scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,
batchMethod = "getIds",
collapserProperties = {
@HystrixProperty(name = HystrixPropertiesManager.MAX_REQUESTS_IN_BATCH , value = "3"),
@HystrixProperty(name = HystrixPropertiesManager.TIMER_DELAY_IN_MILLISECONDS, value = "10")
}
)
@RequestMapping(value = "/getId", method = RequestMethod.GET)
public ResultInfo getId(@RequestParam Long id) {
if (id < 0) {
int i = 1 / 0;
}
log.info(Thread.currentThread().getName());
return null;
}
@HystrixCommand
public List<ResultInfo> getIds(List<Long> ids) {
System.out.println(ids.size()+"@@@@@@@@@");
return orderPaymentService.getIds(ids);
}
上面我們配置了getId會走getIds請求,最多是10S三個請求會合并在一起。然后getIds有payment服務(wù)在分別去查詢最終返回多個ResultInfo。

我們通過jemeter進(jìn)行g(shù)etId接口壓測,日志中ids的長度最大是3 。驗證了我們上面getId接口的配置。這樣就能保證在出現(xiàn)高并發(fā)的時候會進(jìn)行接口合并降低TPS。
上面我們是通過請求方法注解進(jìn)行接口合并處理。實際上內(nèi)部hystrix是通過HystrixCommand
工作流程

官網(wǎng)給出的流程圖示,并配備流程說明一共是9部。下面我們就翻譯下。
①、創(chuàng)建HystrixCommand或者HystrixObservableCommand對象
HystrixCommand : 用在依賴單個服務(wù)上
HystrixObservableCommand : 用在依賴多個服務(wù)上
②、命令執(zhí)行,hystrrixCommand 執(zhí)行execute、queue ;hystrixObservableCommand執(zhí)行observe、toObservable

③、查看緩存是否開啟及是否命中緩存,命中則返回緩存響應(yīng)
④、是否熔斷, 如果已經(jīng)熔斷則fallback降級;如果熔斷器是關(guān)閉的則放行
⑤、線程池、信號量是否有資源供使用。如果沒有足夠資源則fallback 。有則放行
⑥、執(zhí)行run或者construct方法。這兩個是hystrix原生的方法,java實現(xiàn)hystrix會實現(xiàn)兩個方法的邏輯,springcloud已經(jīng)幫我們封裝了。這里就不看這兩個方法了。如果執(zhí)行錯誤或者超時則fallback。在此期間會將日志采集到監(jiān)控中心。
⑦、計算熔斷器數(shù)據(jù),判斷是否需要嘗試放行;這里統(tǒng)計的數(shù)據(jù)會在hystrix.stream的dashboard中查看到。方便我們定位接口健康狀態(tài)
⑧、在流程圖中我們也能看到④、⑤、⑥都會指向fallback。也是我們俗稱的服務(wù)降級??梢姺?wù)降級是hystrix熱門業(yè)務(wù)啊。
⑨、返回響應(yīng)
HystrixDashboard
hystrix 除了服務(wù)熔斷、降級、限流以外,還有一個重要的特性是實時監(jiān)控。并形成報表統(tǒng)計接口請求信息。
關(guān)于hystrix的安裝也很簡單,只需要在項目中配置actutor和
hystrix-dashboard兩個模塊就行了
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
啟動類上添加
EnableHystrixDashboard就引入了dashboard了。我們不需要進(jìn)行任何開發(fā)。這個和eureka一樣主需要簡單的引包就可以了。

就這樣dashboard搭建完成了。dashboard主要是用來監(jiān)控hystrix的請求處理的。所以我們還需要在hystrix請求出將端點暴露出來。
在使用了hystrix命令的模塊加入如下配置即可,我就在order模塊加入
@Component
public class HystrixConfig {
@Bean
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
//注意這里配置的/hystrix.stream 最終訪問地址就是 localhost:port/hystrix.stream ; 如果在配置文件中配置在新版本中是需要
//加上actuator 即 localhost:port/actuator
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
然后我們訪問order模塊
localhost/hystrix.stream就會出現(xiàn)ping的界面。表示我們order模塊安裝監(jiān)控成功。當(dāng)然order也需要actuator模塊下面我們通過jmeter來壓測我們的熔斷、降級、限流接口在通過dashboard來看看各個狀態(tài)吧。

上面的動畫看起來我們的服務(wù)還是很忙的。想想如果是電商當(dāng)你看著每個接口的折線圖像不像就是你的心跳。太高的你就擔(dān)心的。太低了就沒有成就高。下面我們看看dashboard的指標(biāo)詳情

我們看看我們服務(wù)運行期間各個接口的現(xiàn)狀。

聚合監(jiān)控
上面我們通過新建的模塊
hystrix-dashboard來對我們的order模塊進(jìn)行監(jiān)控。但是實際應(yīng)用中我們不可能只在order中配置hystrix的。我們只是在上面為了演示所以在order配置的?,F(xiàn)在我們在payment中也對hystrix中配置。那么我們就需要在dashboard中來回切換order、payment的監(jiān)控數(shù)據(jù)了。
所以我們的聚合監(jiān)控就來了。在進(jìn)行聚合監(jiān)控之前我們先將payment也引入hystrix。注意上面我們是通過bean方式注入hystrix.stream 的 。訪問前綴不需要actuator
新建hystrix-turbine
pom
<!--新增hystrix dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
主要就是新增turbine坐標(biāo),其他的就是hystrix , dashboard等模塊,具體查看結(jié)尾處源碼
yml
spring:
application:
name: cloud-hystrix-turbine
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
instance:
prefer-ip-address: true
# 聚合監(jiān)控
turbine:
app-config: cloud-order-service,cloud-payment-service
cluster-name-expression: "'default'"
# 該處配置和url一樣。如果/actuator/hystrix.stream 的則需要配置actuator
instanceUrlSuffix: hystrix.stream
啟動類
啟動類上添加EnableTurbine注解


