1.1 Hystrix介紹
Hystrix的設計原則是什麼?
- l 資源隔離(線程池隔離和信號量隔離)機制:限制調用分佈式服務的資源使用,某一個調用的服務出現問題不會影響其它服務調用。
- l 限流機制:限流機制主要是提前對各個類型的請求設置最高的QPS閾值,若高於設置的閾值則對該請求直接返回,不再調用後續資源。
- l 熔斷機制:當失敗率達到閥值自動觸發降級(如因網絡故障、超時造成的失敗率真高),熔斷器觸發的快速失敗會進行快速恢復。
- l 降級機制:超時降級、資源不足時(線程或信號量)降級 、運行異常降級等,降級後可以配合降級接口返回託底數據。
- l 緩存支持:提供了請求緩存、請求合併實現
- l 通過近實時的統計/監控/報警功能,來提高故障發現的速度
- l 通過近實時的屬性和配置熱修改功能,來提高故障處理和恢復的速度
1.2 Hystrix整體工作流程
整個流程可以大致歸納為如下幾個步驟:
- l 創建HystrixCommand或者HystrixObservableCommand對象
- l 執行 Command
- l 檢查請求結果是否被緩存
- l 檢查是否開啟了短路器
- l 檢查 線程池/隊列/semaphore 是否已經滿
- l 執行 HystrixObservableCommand.construct() or HystrixCommand.run()
- l 計算短路健康狀況
- l 調用fallback降級機制
- l 返回依賴請求的真正結果
1.3 Hystrix特性
1.3.1 資源隔離
l 說明:在Hystrix中, 主要通過線程池來實現資源隔離. 通常在使用的時候我們會根據調用的遠程服務劃分出多個線程池. 例如調用產品服務的Command放入A線程池, 調用賬戶服務的Command放入B線程池. 這樣做的主要優點是運行環境被隔離開了. 這樣就算調用服務的代碼存在bug或者由於其他原因導致自己所在線程池被耗盡時, 不會對系統的其他服務造成影響. 但是帶來的代價就是維護多個線程池會對系統帶來額外的性能開銷. 如果是對性能有嚴格要求而且確信自己調用服務的客戶端代碼不會出問題的話, 可以使用Hystrix的信號模式(Semaphores)來隔離資源
l Command執行方式
execute():以同步堵塞方式執行 run()。調用 execute() 後,hystrix先創建一個新線程運行run(),接著調用程序要在 execute() 調用處一直堵塞著,直到 run() 運行完成。
queue():以異步非堵塞方式執行 run() 。調用 queue() 就直接返回一個 Future 對象,同時hystrix創建一個新線程運行 run(),調用程序通過 Future.get() 拿到 run() 的返回結果,而Future.get() 是堵塞執行的。
observe():立即執行,即事件subscribe()完成註冊前執行 run()/construct() 。
第一步是事件註冊前,先調用 observe() 自動觸發執行 run()/construct()(如果繼承的是HystrixCommand,hystrix將創建新線程非堵塞執行run();如果繼承的是HystrixObservableCommand,將以調用程序線程堵塞執行construct()),
第二步是從 observe() 返回後調用程序調用 subscribe() 完成事件註冊,如果 run()/construct() 執行成功則觸發 onNext() 和 onCompleted() ,如果執行異常則觸發 onError() 。
toObservable():延時執行,即事件subscribe()完成事件註冊後執行 run()/construct() 。
第一步是事件註冊前,調用 toObservable() 就直接返回一個 Observable<string> 對象,/<string>
第二步調用 subscribe() 完成事件註冊後自動觸發執行 run()/construct()(如果繼承的是HystrixCommand,hystrix將創建新線程非堵塞執行 run() ,調用程序不必等待 run() ;如果繼承的是HystrixObservableCommand,將以調用程序線程堵塞執行 construct(),調用程序等待construct()執行完才能繼續往下走),如果 run()/construct() 執行成功則觸發 onNext() 和 onCompleted() ,如果執行異常則觸發 onError() 。
備註:execute()和queue()是HystrixCommand中的方法,observe()和toObservable()是HystrixObservableCommand 中的方法。其中HystrixCommand是用來獲取一條數據的;HystrixObservableCommand是用來獲取多條數據的。從底層實現來講,HystrixCommand其實也是利用Observable實現的(如果我們看Hystrix的源碼的話,可以發現裡面大量使用了RxJava),雖然HystrixCommand只返回單個的結果,但HystrixCommand的queue方法實際上是調用了toObservable().toBlocking().toFuture(),而execute方法實際上是調用了queue().get()。
l 獲取單個產品Command
public class GetProductInfoCommand extends HystrixCommand<productinfo>{
private Long productId;
public GetProductInfoCommand(Long productId) {
super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoCommandGroup"));
this.productId=productId;
}
@Override
protected ProductInfo run() throws Exception {
String url = "http://127.0.0.1:8082/getProductInfo?productId="+productId;
String response = HttpClientUtils.sendGetRequest(url);
return JSONObject.parseObject(response,ProductInfo.class);
}
}
//使用
HystrixCommand<productinfo> command = new GetProductInfoCommand(productId);
ProductInfo productInfo=command.execute();
l 獲取產品列表Command
// 獲取產品列表Command
public class GetProductInfosCommand extends HystrixObservableCommand<productinfo> {
private String[] productIds;
public GetProductInfosCommand(String[] productIds) {
super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoGroup"));
this.productIds = productIds;
}
@Override
protected Observable<productinfo> construct() {
return Observable.create(new Observable.OnSubscribe<productinfo>() {
public void call(Subscriber super ProductInfo> observer) {
try {
for(String productId : productIds) {
String url = "http://127.0.0.1:8082/getProductInfo?productId=" + productId;
String response = HttpClientUtils.sendGetRequest(url);
ProductInfo productInfo = JSONObject.parseObject(response, ProductInfo.class);
observer.onNext(productInfo);
}
observer.onCompleted();
} catch (Exception e) {
observer.onError(e);
}
}
}).subscribeOn(Schedulers.io());
}
}
//使用
HystrixObservableCommand<productinfo> getProductInfosCommand =
new GetProductInfosCommand(productIds.split(","));
Observable<productinfo> observable = getProductInfosCommand.observe();
//observable = getProductInfosCommand.toObservable(); // 還沒有執行
observable.subscribe(new Observer<productinfo>() { // 等到調用subscribe然後才會執行
public void onCompleted() {
System.out.println("獲取完了所有的商品數據");
}
public void onError(Throwable e) {
e.printStackTrace();
}
public void onNext(ProductInfo productInfo) {
System.out.println(productInfo);
}
});/<productinfo>/<productinfo>/<productinfo>/<productinfo>/<productinfo>/<productinfo>/<productinfo>/<productinfo>
1.3.2 限流(通過配置)
限流在日常生活中很常見,比如節假日你去一個旅遊景點,為了不把景點撐爆,管理部門通常會在外面設置攔截,限制景點的進入人數(等有人出來之後,再放新的人進去)。對應到計算機中,比如要搞活動、秒殺等,通常都會限流。在Hystrix中:
l 如果是線程隔離,可以通過線程數+隊列大小限制。參數如下:
hystrix.threadpool.default.coreSize
hystrix.threadpool.default.maxQueueSize
hystrix.threadpool.default.queueSizeRejectionThreshold
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
l 如果是信號量隔離,可以設置最大併發請求數。參數如下:
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests
1.3.3 熔斷(CircuitBreaker)
熔斷器的原理很簡單,如同電力過載保護器。它可以實現快速失敗,如果它在一段時間內偵測到許多類似的錯誤,會強迫其以後的多個調用快速失敗,不再訪問遠程服務器,從而防止應用程序不斷地嘗試執行可能會失敗的操作,使得應用程序繼續執行而不用等待修正錯誤,或者浪費CPU時間去等到長時間的超時產生。熔斷器也可以使應用程序能夠診斷錯誤是否已經修正,如果已經修正,應用程序會再次嘗試調用操作。
熔斷器模式就像是那些容易導致錯誤的操作的一種代理。這種代理能夠記錄最近調用發生錯誤的次數,然後決定使用允許操作繼續,或者立即返回錯誤。
熔斷器開關相互轉換的邏輯如下圖:
熔斷器就是保護服務高可用的最後一道防線。
當Hystrix Command請求後端服務時,在一定時間內(metrics.rollingStats.timeInMilliseconds,默認10s),請求次數超過了最低要求(circuitBreaker.requestVolumeThreshold,默認20次),並且其失敗數量超過一定比例(circuitBreaker.errorThresholdPercentage,默認50%),斷路器會切換到開路狀態(Open). 這時所有請求會直接失敗而不會發送到後端服務. 斷路器保持在開路狀態一段時間後(circuitBreaker.sleepWindowInMilliseconds,默認5秒), 自動切換到半開路狀態(HALF-OPEN). 這時會判斷下一次請求的返回情況, 如果請求成功, 斷路器切回閉路狀態(CLOSED), 否則重新切換到開路狀態(OPEN). Hystrix的斷路器就像我們家庭電路中的保險絲, 一旦後端服務不可用, 斷路器會直接切斷請求鏈, 避免發送大量無效請求影響系統吞吐量, 並且斷路器有自我檢測並恢復的能力.
1.3.4 降級(Fallback)
Fallback相當於是降級操作。所謂降級,就是指在Hystrix執行非核心鏈路功能失敗的情況下,該如何處理,比如返回默認值或者從緩存中取值
觸發降級的情況
1、hystrix調用各種接口,或者訪問外部依賴(如mysql、redis等等)時,執行方法中拋出了異常。
2、對每個外部依賴,無論是服務接口,中間件,資源隔離,對外部依賴只能用一定量的資源去訪問,線程池/信號量,如果資源池已滿,則後續的請求將會被 reject,即進行限流。
3、訪問外部依賴的時候,訪問時間過長,可能就會導致超時,報一個TimeoutException異常,即Timeout機制。
上述三種情況,都是常見的異常情況,對外部依賴的東西訪問的時候出現了異常,發送異常事件到斷路器中去進行統計。
4、如果斷路器發現異常事件的佔比達到了一定的比例,直接開啟斷路器。
上述四種情況,都會去調用fallback降級機制。
如果要實現回退或者降級處理,代碼上需要實現HystrixCommand.getFallback()方法或者是HystrixObservableCommand. HystrixObservableCommand()。
1.3.5 Hystrix請求緩存(request cache)
Hystrix支持將一個請求結果緩存起來,在同一個請求上下文中,具有相同key的請求將直接從緩存中取出結果,很適合查詢類的接口,可以使用緩存進行優化,減少請求開銷,從而跳過真實服務的訪問請求。
Hystrix請求結果緩存的作用:
1、在同一個請求上下文中,可以減少使用相同參數請求原始服務的開銷。
3、請求緩存在 run() 和 construct() 執行之前生效,所以可以有效減少不必要的線程開銷。
要使用Hystrix cache功能:
1、需要構建 RequestContext ,可以在攔截器中使用 HystrixRequestContext.initializeContext() 和 HystrixRequestContext.shutdown() 來初始化 RequestContext 和 關閉RequestContext資源。
2、需要重寫 HystrixCommand 或 HystrixObservableCommand 中的 getCacheKey() 方法,指定緩存的 key,開啟緩存配置。
l 配置HystrixRequestContextServletFilter
@WebFilter(filterName = "hystrixRequestContextServletFilter",urlPatterns = "/*",asyncSupported = true)
public class HystrixRequestContextServletFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
chain.doFilter(request, response);
} finally {
context.shutdown();
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
l 開啟緩存功能:繼承HystrixCommand或HystrixObservableCommand,覆蓋getCacheKey()方法,指定緩存的key,開啟緩存配置。
private static final HystrixCommandKey COMMAND_KEY= HystrixCommandKey.Factory.asKey("GetProductInfoCommand");
@Override
protected String getCacheKey() {
return "product_info_"+productId;
}
public static void flushCache(Long productId){
HystrixRequestCache.getInstance(COMMAND_KEY, HystrixConcurrencyStrategyDefault.getInstance()).clear("product_info_"+productId);
}
1.3.6 Hystrix請求合併(request collapser)
1.4 Feign使用Hystrix
見文章:https://www.toutiao.com/i6752760297146024460/
1.5 設置TimeOut注意事項
l 如果hystrix.command.default.execution.timeout.enabled為true,則會有兩個執行方法超時的配置,一個就是ribbon的ReadTimeout,一個就是熔斷器hystrix的timeoutInMilliseconds, 此時誰的值小誰生效
l 如果hystrix.command.default.execution.timeout.enabled為false,則熔斷器不進行超時熔斷,而是根據ribbon的ReadTimeout拋出的異常而熔斷,也就是取決於ribbon
l ribbon的ConnectTimeout,配置的是請求服務的超時時間,除非服務找不到,或者網絡原因,這個時間才會生效
l ribbon還有MaxAutoRetries對當前實例的重試次數,MaxAutoRetriesNextServer對切換實例的重試次數, 如果ribbon的ReadTimeout超時,或者ConnectTimeout連接超時,會進行重試操作
l 由於ribbon的重試機制,通常熔斷的超時時間需要配置的比ReadTimeout長,ReadTimeout比ConnectTimeout長,否則還未重試,就熔斷了
l 為了確保重試機制的正常運作,理論上(以實際情況為準)建議hystrix的超時時間為:(1 + MaxAutoRetries + MaxAutoRetriesNextServer) * ReadTimeout
1.6 Hystrix微服務優化實例
瞭解了Hystrix的特性和超時效果,再看看下面這個圖,服務A調用服務B和服務C,服務C沒有太複雜的邏輯處理,300毫秒內就處理返回了,服務B邏輯複雜,Sql語句就長達上百行,經常要卡個5,6秒返回,在大量請求調用到服務B的時候,服務A調用服務B的hystrix線程池已經不堪重負,全部卡住
這裡的話,首先考慮的就是服務B的優化,優化SQL,加索引,加緩存, 優化流程,同步改異步,總之縮短響應時間
一個接口,理論的最佳響應速度應該在200ms以內,或者慢點的接口就幾百毫秒。
a. 如何設置Hystrix線程池大小,Hystrix線程池大小默認為10
hystrix:
threadpool:
default:
coreSize: 10
每秒請求數 = 1/響應時長(單位s) * 線程數 = 線程數 / 響應時長(單位s)
即:線程數 = 每秒請求數 * 響應時長(單位s) + (緩衝線程數)
比如一臺服務, 平均每秒大概收到20個請求,每個請求平均響應時長估計在500ms,
線程數 = 20 * 500 / 1000 = 10
為了應對峰值高併發,加上緩衝線程,比如這裡為了好計算設為5,就是 10 + 5 = 15個線程
b. 如何設置超時時間
還拿上面的例子,比如已經配置了總線程是15個,每秒大概20個請求,那麼極限情況,每個線程都飽和工作,也就是每個線程一秒內處理的請求為 20 / 15 = ≈ 1.3個 , 那每個請求的最大能接受的時間就是 1000 / 1.3 ≈ 769ms ,往下取小值700ms.
實際情況中,超時時間一般設為比99.5%平均時間略高即可,然後再根據這個時間推算線程池大小
1.7 資料
Hystrix屬性配置詳情:https://github.com/Netflix/Hystrix/wiki/Configuration
閱讀更多 包子餡2012 的文章