Hystrix工作原理
阿新 • • 發佈:2018-12-17
概述
本來是抱著一顆認真學習的態度去學習Hystrix,結果看完書之後是這樣的:
然後像我這麼優秀的人,就回去百度啊 ,終於找到了一篇大佬的文章,原文的地址是:[防雪崩利器:熔斷器 Hystrixx]
(https://segmentfault.com/a/1190000005988895)。其實我就是把別人的文章拿過來用。天下文章一大抄,主要是自己學會
就行了
什麼是伺服器雪崩
服務雪崩效應是一種因 服務提供者 的不可用導致 服務呼叫者 的不可用,並將不可用 逐漸放大 的過程.如果所示:
上圖中, A為服務提供者, B為A的服務呼叫者, C和D是B的服務呼叫者. 當A的不可用,引起B的不可用,並將不可用逐漸放大C 和D時, 服務雪崩就形成了.
雪崩行成的原因
大致可以分成三個階段: 服務提供者不可用 原因有: 硬體故障: 硬體故障可能為硬體損壞造成的伺服器主機宕機, 網路硬體故障造成的服務提供者的不可訪問. 程式Bug: 快取擊穿快取擊穿一般發生在快取應用重啟, 所有快取被清空時,以及短時間內大量快取失效時. 大量的快取不命中, 使請求直擊後端,造成服務提供者超負荷執行,引起服務不可用. 使用者大量請求:在秒殺和大促開始前,如果準備不充分,使用者發起大量請求也會造成服務提供者的不可用. 重試加大流量 原因有: 使用者重試:在服務提供者不可用後, 使用者由於忍受不了介面上長時間的等待,而不斷重新整理頁面甚至提交表單. 程式碼邏輯重試:服務呼叫端的會存在大量服務異常後的重試邏輯. 服務呼叫者不可用 原因有: 同步等待造成的資源耗盡: 當服務呼叫者使用 同步呼叫 時, 會產生大量的等待執行緒佔用系統資源. 一旦執行緒資源被耗 盡,服務呼叫者提供的服務也將處於不可用狀態, 於是服務雪崩效應產生了.
服務雪崩的應對策略
1.流量控制 閘道器限流:因為Nginx的高效能, 目前一線網際網路公司大量採用Nginx+Lua的閘道器進行流量控制, 由此而來的OpenResty也越 來越熱門. 使用者互動限流:1. 採用載入動畫,提高使用者的忍耐等待時間. 2. 提交按鈕新增強制等待時間機制. 關閉重試 2.改進快取模式: 快取預載入 同步改為非同步重新整理 3.伺服器自動擴容 AWS的auto scaling 4.服務呼叫者降級服務 資源隔離:資源隔離主要是對呼叫服務的執行緒池進行隔離. 對依賴服務進行分類:我們根據具體業務,將依賴服務分為: 強依賴和若依賴. 強依賴服務不可用會導致當前業務中止,而弱 依賴服務的不可用不會導致當前業務的中止. 不可用服務的呼叫快速失敗:不可用服務的呼叫快速失敗一般通過 超時機制, 熔斷器 和熔斷後的 降級方法 來實現.
重量級炸彈 Hystrix
首先講一個Hystrix的設計原則
資源隔離
熔斷器
命令模式
資源熔斷
在一個高度服務化的系統中,我們實現的一個業務邏輯通常會依賴多個服務,比如: 商品詳情展示服務會依賴商品服務, 價格服
務, 商品評論服務. 如圖所示:
呼叫三個依賴服務會共享商品詳情服務的執行緒池. 如果其中的商品評論服務不可用, 就會出現執行緒池裡所有執行緒都因等待響
應而被阻塞, 從而造成服務雪崩. 如圖所示:
Hystrix通過將每個依賴服務分配獨立的執行緒池進行資源隔離, 從而避免服務雪崩.
如下圖所示, 當商品評論服務不可用時, 即使商品服務獨立分配的20個執行緒全部處於同步等待狀態,也不會影響其他依賴服務
的呼叫.
熔斷器模式
熔斷器模式定義了熔斷器開關相互轉換的邏輯:
服務的健康狀況 = 請求失敗數 / 請求總數.
熔斷器開關由關閉到開啟的狀態轉換是通過當前服務健康狀況和設定閾值比較決定的.
當熔斷器開關關閉時, 請求被允許通過熔斷器. 如果當前健康狀況高於設定閾值, 開關繼續保持關閉. 如果當前健康狀況低於
設定閾值, 開關則切換為開啟狀態.
當熔斷器開關開啟時, 請求被禁止通過.
當熔斷器開關處於開啟狀態, 經過一段時間後, 熔斷器會自動進入半開狀態, 這時熔斷器只允許一個請求通過. 當該請求呼叫
成功時, 熔斷器恢復到關閉狀態. 若該請求失敗, 熔斷器繼續保持開啟狀態, 接下來的請求被禁止通過.
熔斷器的開關能保證服務呼叫者在呼叫異常服務時, 快速返回結果, 避免大量的同步等待. 並且熔斷器能在一段時間後繼續偵測請求執行結果, 提供恢復服務呼叫的可能.
命令模式
Hystrix使用命令模式(繼承HystrixCommand類或者是HystrixObservableCommand類)來包裹具體的服務呼叫邏輯(run方法), 並在命令模式中添加了服務呼叫失敗後的降級邏輯(getFallback).
同時我們在Command的構造方法中可以定義當前服務執行緒池和熔斷器的相關引數. 如下程式碼所示:
public class Service1HystrixCommand extends HystrixCommand<Response> {
private Service1 service;
private Request request;
public Service1HystrixCommand(Service1 service, Request request){
supper(
Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ServiceGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("servcie1query"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("service1ThreadPool"))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(20))//服務執行緒池數量
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerErrorThresholdPercentage(60)//熔斷器關閉到開啟閾值
.withCircuitBreakerSleepWindowInMilliseconds(3000)//熔斷器開啟到關閉的時間窗長度
))
this.service = service;
this.request = request;
);
}
@Override
protected Response run(){
return service1.call(request);
}
@Override
protected Response getFallback(){
return Response.dummy();
}
}
在使用了Command模式構建了服務物件之後, 服務便擁有了熔斷器和執行緒池的功能.
Hystrix的內部處理邏輯
下圖為Hystrix服務呼叫的內部邏輯:
構建Hystrix的Command物件, 呼叫執行方法.
Hystrix檢查當前服務的熔斷器開關是否開啟, 若開啟, 則執行降級服務getFallback方法.
若熔斷器開關關閉, 則Hystrix檢查當前服務的執行緒池是否能接收新的請求, 若超過執行緒池已滿, 則執行降級服務getFallback方法.
若執行緒池接受請求, 則Hystrix開始執行服務呼叫具體邏輯run方法.
若服務執行失敗, 則執行降級服務getFallback方法, 並將執行結果上報Metrics更新服務健康狀況.
若服務執行超時, 則執行降級服務getFallback方法, 並將執行結果上報Metrics更新服務健康狀況.
若服務執行成功, 返回正常結果.
若服務降級方法getFallback執行成功, 則返回降級結果.
若服務降級方法getFallback執行失敗, 則丟擲異常.
Hystrix Metrics的實現
Hystrix的Metrics中儲存了當前服務的健康狀況, 包括服務呼叫總次數和服務呼叫失敗次數等. 根據Metrics的計數, 熔斷器從而
能計算出當前服務的呼叫失敗率, 用來和設定的閾值比較從而決定熔斷器的狀態切換邏輯. 因此Metrics的實現非常重要.
1.4之前的滑動視窗實現
Hystrix在這些版本中的使用自己定義的滑動視窗資料結構來記錄當前時間窗的各種事件(成功,失敗,超時,執行緒池拒絕等)的計數.
事件產生時, 資料結構根據當前時間確定使用舊桶還是建立新桶來計數, 並在桶中對計數器經行修改.
這些修改是多執行緒併發執行的, 程式碼中有不少加鎖操作,邏輯較為複雜.
1.5之後的滑動視窗實現
Hystrix在這些版本中開始使用RxJava的Observable.window()實現滑動視窗.
RxJava的window使用後臺執行緒建立新桶, 避免了併發建立桶的問題.
同時RxJava的單執行緒無鎖特性也保證了計數變更時的執行緒安全. 從而使程式碼更加簡潔.
以下為我使用RxJava的window方法實現的一個簡易滑動視窗Metrics, 短短几行程式碼便能完成統計功能,足以證明
RxJava的強大:
public void timeWindowTest() throws Exception{
Observable<Integer> source = Observable.interval(50, TimeUnit.MILLISECONDS).map(i -> RandomUtils.nextInt(2));
source.window(1, TimeUnit.SECONDS).subscribe(window -> {
int[] metrics = new int[2];
window.subscribe(i -> metrics[i]++,
InternalObservableUtils.ERROR_NOT_IMPLEMENTED,
() -> System.out.println("視窗Metrics:" + JSON.toJSONString(metrics)));
});
TimeUnit.SECONDS.sleep(3);
}