使用Hystrix實現自動降級與依賴隔離[微服務]
轉載:https://www.jianshu.com/p/138f92aa83dc
這篇文章是記錄了自己的一次整合Hystrix的經驗,原本寫在公司內部wiki裡,所以裡面有一些內容為了避免重複,直接引用了其他同事的wiki,而釋出到外網,這部分就不能直接引用了,因此可能不會太完整,後續會補充進去。
1.背景
目前對於一些非核心操作,如增減庫存後儲存操作日誌 傳送非同步訊息時(具體業務流程),一旦出現MQ服務異常時,會導致介面響應超時,因此可以考慮對非核心操作引入服務降級、服務隔離。
2.Hystrix說明
官方文件 [https://github.com/Netflix/Hystrix/wiki]
hystrix是netflix開源的一個容災框架,解決當外部依賴故障時拖垮業務系統、甚至引起雪崩的問題。
2.1為什麼需要Hystrix?
在大中型分散式系統中,通常系統很多依賴(HTTP,hession,Netty,Dubbo等),在高併發訪問下,這些依賴的穩定性與否對系統的影響非常大,但是依賴有很多不可控問題:如網路連線緩慢,資源繁忙,暫時不可用,服務離線等。
當依賴阻塞時,大多數伺服器的執行緒池就出現阻塞(BLOCK),影響整個線上服務的穩定性,在複雜的分散式架構的應用程式有很多的依賴,都會不可避免地在某些時候失敗。高併發的依賴失敗時如果沒有隔離措施,當前應用服務就有被拖垮的風險。
例如:一個依賴30個SOA服務的系統,每個服務99.99%可用。
99.99%的30次方 ≈ 99.7%
0.3% 意味著一億次請求 會有 3,000,00次失敗
換算成時間大約每月有2個小時服務不穩定.
隨著服務依賴數量的變多,服務不穩定的概率會成指數性提高.
解決問題方案:對依賴做隔離。
2.2Hystrix設計理念
想要知道如何使用,必須先明白其核心設計理念,Hystrix基於命令模式,通過UML圖先直觀的認識一下這一設計模式
image.png
可見,Command是在Receiver和Invoker之間新增的中間層,Command實現了對Receiver的封裝。那麼Hystrix的應用場景如何與上圖對應呢?
API既可以是Invoker又可以是reciever,通過繼承Hystrix核心類HystrixCommand來封裝這些API(例如,遠端介面呼叫,資料庫查詢之類可能會產生延時的操作)。就可以為API提供彈性保護了。
2.3Hystrix如何解決依賴隔離
- 1:Hystrix使用命令模式HystrixCommand(Command)包裝依賴呼叫邏輯,每個命令在單獨執行緒中/訊號授權下執行。
- 2:可配置依賴呼叫超時時間,超時時間一般設為比99.5%平均時間略高即可.當呼叫超時時,直接返回或執行fallback邏輯。
- 3:為每個依賴提供一個小的執行緒池(或訊號),如果執行緒池已滿呼叫將被立即拒絕,預設不採用排隊.加速失敗判定時間。
- 4:依賴呼叫結果分:成功,失敗(丟擲異常),超時,執行緒拒絕,短路。 請求失敗(異常,拒絕,超時,短路)時執行fallback(降級)邏輯。
- 5:提供熔斷器元件,可以自動執行或手動呼叫,停止當前依賴一段時間(10秒),熔斷器預設錯誤率閾值為50%,超過將自動執行。
- 6:提供近實時依賴的統計和監控
2.4Hystrix流程結構解析
image.png
流程說明:
1:每次呼叫建立一個新的HystrixCommand,把依賴呼叫封裝在run()方法中.
2:執行execute()/queue做同步或非同步呼叫.
3:判斷熔斷器(circuit-breaker)是否開啟,如果開啟跳到步驟8,進行降級策略,如果關閉進入步驟.
4:判斷執行緒池/佇列/訊號量是否跑滿,如果跑滿進入降級步驟8,否則繼續後續步驟.
5:呼叫HystrixCommand的run方法.執行依賴邏輯
5a:依賴邏輯呼叫超時,進入步驟8.
6:判斷邏輯是否呼叫成功
6a:返回成功呼叫結果
6b:調用出錯,進入步驟8.
7:計算熔斷器狀態,所有的執行狀態(成功, 失敗, 拒絕,超時)上報給熔斷器,用於統計從而判斷熔斷器狀態.
8:getFallback()降級邏輯.
以下四種情況將觸發getFallback呼叫:
(1):run()方法丟擲非HystrixBadRequestException異常。
(2):run()方法呼叫超時
(3):熔斷器開啟攔截呼叫
(4):執行緒池/佇列/訊號量是否跑滿
8a:沒有實現getFallback的Command將直接丟擲異常
8b:fallback降級邏輯呼叫成功直接返回
8c:降級邏輯呼叫失敗丟擲異常
9:返回執行成功結果
2.5熔斷器:Circuit Breaker
每個熔斷器預設維護10個bucket,每秒一個bucket,每個bucket記錄成功,失敗,超時,拒絕的狀態,
預設錯誤超過50%且10秒內超過20個請求進行中斷攔截.
image.png
2.6Hystrix隔離分析
Hystrix隔離方式採用執行緒/訊號的方式,通過隔離限制依賴的併發量和阻塞擴散.
- (1)執行緒隔離
把執行依賴程式碼的執行緒與請求執行緒(如:jetty執行緒)分離,請求執行緒可以自由控制離開的時間(非同步過程)。
通過執行緒池大小可以控制併發量,當執行緒池飽和時可以提前拒絕服務,防止依賴問題擴散。
線上建議執行緒池不要設定過大,否則大量堵塞執行緒有可能會拖慢伺服器。 - (2)執行緒隔離的優缺點
- 執行緒隔離的優點:
- 執行緒隔離的缺點:
- NOTE: Netflix公司內部認為執行緒隔離開銷足夠小,不會造成重大的成本或效能的影響。
- Netflix 內部API 每天100億的HystrixCommand依賴請求使用執行緒隔,每個應用大約40多個執行緒池,每個執行緒池大約5-20個執行緒。
- 執行緒隔離的優點:
- (3)訊號隔離
- 訊號隔離也可以用於限制併發訪問,防止阻塞擴散, 與執行緒隔離最大不同在於執行依賴程式碼的執行緒依然是請求執行緒(該執行緒需要通過訊號申請),
- 如果客戶端是可信的且可以快速返回,可以使用訊號隔離替換執行緒隔離,降低開銷.
- 訊號量的大小可以動態調整, 執行緒池大小不可以.
執行緒隔離與訊號隔離區別如下圖:
image.png
3.接入方式
本文會重點介紹基於服務化專案(thrift服務化專案)的接入方式。
3.1新增hystrix依賴
關於版本問題:由於不同版本Compile Dependencies不同,在使用過程中可以針對具體情況修改版本,具體依賴關係http://mvnrepository.com/artifact/com.netflix.hystrix/hystrix-javanica
<hystrix-version>1.4.22</hystrix-version>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>${hystrix-version}</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-metrics-event-stream</artifactId>
<version>${hystrix-version}</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>${hystrix-version}</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-servo-metrics-publisher</artifactId>
<version>${hystrix-version}</version>
</dependency>
<dependency>
<groupId>com.meituan.service.us</groupId>
<artifactId>hystrix-collector</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
3.2引入Hystrix Aspect
application-context.xml檔案中
<aop:aspectj-autoproxy/>
<bean id="hystrixAspect" class="com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect"></bean>
<context:component-scan base-package="com.***.***"/>
<context:annotation-config/>
注意:
- 1)hystrixAspect的這兩行配置一定要和下面的context:component-scan放在同一個檔案
- 2)Hystrix依賴的一些jar需要解決衝突問題,例如guava為15.0版本
3.3統計資料
需要註冊plugin,直接從plugin中獲取統計資料
新增初始化Bean
import com.meituan.service.us.collector.notifier.CustomEventNotifier;
import com.netflix.hystrix.contrib.servopublisher.HystrixServoMetricsPublisher;
import com.netflix.hystrix.strategy.HystrixPlugins;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
/**
* Created by gaoguangchao on 16/7/1.
*/
public class HystrixMetricsInitializingBean {
private static final Logger LOGGER = LoggerFactory.getLogger(HystrixMetricsInitializingBean.class);
public void init() throws Exception {
LOGGER.info("HystrixMetrics starting...");
HystrixPlugins.getInstance().registerEventNotifier(CustomEventNotifier.getInstance());
HystrixPlugins.getInstance().registerMetricsPublisher(HystrixServoMetricsPublisher.getInstance());
}
}
application-context.xml檔案中
<bean id="hystrixMetricsInitializingBean" class="com.***.HystrixMetricsInitializingBean" init-method="init"/>
3.4添加註解
本文使用同步執行方式,因此註解及方法實現都為同步方式,如果有非同步執行、反應執行的需求,可以參考:官方註解說明[https://github.com/Netflix/Hystrix/tree/master/hystrix-contrib/hystrix-javanica]
@HystrixCommand(groupKey = "productStockOpLog", commandKey = "addProductStockOpLog", fallbackMethod = "addProductStockOpLogFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "400"),//指定多久超時,單位毫秒。超時進fallback
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//判斷熔斷的最少請求數,預設是10;只有在一個統計視窗內處理的請求數量達到這個閾值,才會進行熔斷與否的判斷
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10"),//判斷熔斷的閾值,預設值50,表示在一個統計視窗內有50%的請求處理失敗,會觸發熔斷
}
)
public void addProductStockOpLog(Long sku_id, Object old_value, Object new_value) throws Exception {
if (new_value != null && !new_value.equals(old_value)) {
doAddOpLog(null, null, sku_id, null, ProductOpType.PRODUCT_STOCK, old_value != null ? String.valueOf(old_value) : null, String.valueOf(new_value), 0, "C端", null);
}
}
public void addProductStockOpLogFallback(Long sku_id, Object old_value, Object new_value) throws Exception {
LOGGER.warn("傳送商品庫存變更訊息失敗,進入Fallback,skuId:{},oldValue:{},newValue:{}", sku_id, old_value, new_value);
}
示例:
@HystrixCommand(groupKey="UserGroup", commandKey = "GetUserByIdCommand",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100"),//指定多久超時,單位毫秒。超時進fallback
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//判斷熔斷的最少請求數,預設是10;只有在一個統計視窗內處理的請求數量達到這個閾值,才會進行熔斷與否的判斷
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10"),//判斷熔斷的閾值,預設值50,表示在一個統計視窗內有50%的請求處理失敗,會觸發熔斷
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30"),
@HystrixProperty(name = "maxQueueSize", value = "101"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
})
說明:
hystrix函式必須為public,fallback函式可以為private。兩者需要返回值和引數相同 詳情。
hystrix函式需要放在一個service中,並且,在類本身的其他函式中呼叫hystrix函式,是無法達到監控的目的的。
3.5引數配置
引數說明 | 值 | 備註 |
---|---|---|
groupKey | productStockOpLog | group標識,一個group使用一個執行緒池 |
commandKey | addProductStockOpLog | command標識 |
fallbackMethod | addProductStockOpLogFallback | fallback方法,兩者需要返回值和引數相同 |
超時時間設定 | 400ms | 執行策略,在THREAD模式下,達到超時時間,可以中斷 For most circuits, you should try to set their timeout values close to the 99.5th percentile of a normal healthy system so they will cut off bad requests and not let them take up system resources or affect user behavior. |
統計視窗(10s)內最少請求數 | 10 | 熔斷策略 |
熔斷多少秒後去嘗試請求 | 5s | 熔斷策略,預設值 |
熔斷閥值 | 10% | 熔斷策略:一個統計視窗內有10%的請求處理失敗,會觸發熔斷 |
執行緒池coreSize | 10 | 預設值(推薦值).在當前專案中,需要做依賴隔離的方法為傳送一條MQ訊息,傳送MQ訊息方法的TP99耗時在1ms以下,近2周單機QPS最高值在18左右,經過灰度驗證了午高峰後(當日QPS>上週末QPS),ActiveThreads<=2,rejected=0,經過壓測後得出結論:執行緒池大小為10足以應對2000QPS,前提傳送MQ訊息時耗時正常(該部分為實際專案中的情況,在此不做詳述) |
執行緒池maxQueueSize | -1 | 即執行緒池佇列為SynchronousQueue |
4.引數說明
其他引數可參見 https://github.com/Netflix/Hystrix/wiki/Con
分類 | 引數 | 作用 | 預設值 | 備註 |
---|---|---|---|---|
基本引數 | groupKey | 表示所屬的group,一個group共用執行緒池 | getClass().getSimpleName(); | |
基本引數 | commandKey | 當前執行方法名 | ||
Execution ( 控制HystrixCommand.run()的執行策略) | execution.isolation.strategy | 隔離策略,有THREAD和SEMAPHORE THREAD | 以下幾種可以使用SEMAPHORE模式: 只想控制併發度 外部的方法已經做了執行緒隔離 呼叫的是本地方法或者可靠度非常高、耗時特別小的方法(如medis) | |
Execution | execution.isolation.thread.timeoutInMilliseconds | 超時時間 | 1000ms | 預設值:1000 在THREAD模式下,達到超時時間,可以中斷 在SEMAPHORE模式下,會等待執行完成後,再去判斷是否超時 設定標準: 有retry,99meantime+avg meantime 沒有retry,99.5meantime |
Execution | execution.timeout.enabled | 是否開啟超時 | true | |
Execution | execution.isolation.thread.interruptOnTimeout | 是否開啟超時執行緒中斷 | true | THREAD模式有效 |
Execution | execution.isolation.semaphore.maxConcurrentRequests | 訊號量最大併發度 | 10 | SEMAPHORE模式有效 |
Fallback ( 設定當fallback降級發生時的策略) | fallback.isolation.semaphore.maxConcurrentRequests | fallback最大併發度 | 10 | |
Fallback | fallback.enabled | fallback是否可用 | true | |
Circuit Breaker (配置熔斷的策略) | circuitBreaker.enabled | 是否開啟熔斷 | true | |
Circuit Breaker | circuitBreaker.requestVolumeThreshold | 一個統計視窗內熔斷觸發的最小個數/10s | 20 | |
Circuit Breaker | circuitBreaker.sleepWindowInMilliseconds | 熔斷多少秒後去嘗試請求 | 5000ms | |
Circuit Breaker | circuitBreaker.errorThresholdPercentage | 失敗率達到多少百分比後熔斷 | 50 | 主要根據依賴重要性進行調整 |
Circuit Breaker | circuitBreaker.forceOpen | 是否強制開啟熔斷 | ||
Circuit Breaker | circuitBreaker.forceClosed | 是否強制關閉熔斷 | 如果是強依賴,應該設定為true | |
Metrics (設定關於HystrixCommand執行需要的統計資訊) | metrics.rollingStats.timeInMilliseconds | 設定統計滾動視窗的長度,以毫秒為單位。用於監控和熔斷器。 | 10000 | 滾動視窗被分隔成桶(bucket),並且進行滾動。 例如這個屬性設定10s(10000),一個桶是1s。 |
Metrics | metrics.rollingStats.numBuckets 設定統計視窗的桶數量 | 10 | metrics.rollingStats.timeInMilliseconds必須能被這個值整除 | |
Metrics | metrics.rollingPercentile.enabled | 設定執行時間是否被跟蹤,並且計算各個百分比,50%,90%等的時間 | true | |
Metrics | metrics.rollingPercentile.timeInMilliseconds | 設定執行時間在滾動視窗中保留時間,用來計算百分比 | 60000ms | |
Metrics | metrics.rollingPercentile.numBuckets | 設定rollingPercentile視窗的桶數量 | 6 | metrics.rollingPercentile.timeInMilliseconds必須能被這個值整除 |
Metrics | metrics.rollingPercentile.bucketSize | 此屬性設定每個桶儲存的執行時間的最大值。 | 100 | 如果設定為100,但是有500次求情,則只會計算最近的100次 |
Metrics | metrics.healthSnapshot.intervalInMilliseconds | 取樣時間間隔 | 500 | |
Request Context ( 設定HystrixCommand使用的HystrixRequestContext相關的屬性) | requestCache.enabled | 設定是否快取請求,request-scope內快取 | true | |
Request Context | requestLog.enabled | 設定HystrixCommand執行和事件是否列印到HystrixRequestLog中 | ||
ThreadPool Properties(配置HystrixCommand使用的執行緒池的屬性) | coreSize | 設定執行緒池的core size,這是最大的併發執行數量。 | 10 | 設定標準:coreSize = requests per second at peak when healthy × 99th percentile latency in seconds + some breathing room 大多數情況下預設的10個執行緒都是值得建議的 |
ThreadPool Properties | maxQueueSize | 最大佇列長度。設定BlockingQueue的最大長度 | -1 | 預設值:-1 如果使用正數,佇列將從SynchronousQueue改為LinkedBlockingQueue |
ThreadPool Properties | queueSizeRejectionThreshold | 設定拒絕請求的臨界值 | 5 | 此屬性不適用於maxQueueSize = - 1時 設定設個值的原因是maxQueueSize值執行時不能改變,我們可以通過修改這個變數動態修改允許排隊的長度 |
ThreadPool Properties | keepAliveTimeMinutes | 設定keep-live時間 | 1分鐘 | 這個一般用不到因為預設corePoolSize和maxPoolSize是一樣的。 |
5.效能測試
5.1測試情況
image.png
去除Cold狀態的第一個異常點後,1-10測試場景的Hystrix平均耗時如上圖所示, 可以得出結論:
- 單個HystrixCommand的額外耗時基本穩定處於0.3ms左右,和執行緒池大小無關,和client數量無關
- hystrix的額外耗時和執行的HystrixCommand數量有關係,隨著command數量增多,耗時增加,但是增量較小,沒有比例關係
- App剛啟動時,第一個請求耗時300+ms,隨後請求的耗時降低至1ms以下;剛啟動的一小段時間內耗時略大於Hot狀態時耗時,總體不超過1ms