在基於微服務的應用程序中,開發人員通常需要調用多個微服務來完成特定的任務。在不使用艙壁模式的情況下,這些調用默認是使用同一批線程來執行調用的,這些線程是為了處理整個Java容器的請求而預留的。在存在大量請求的情況下,一個服務出現性能問題會導致Java容器的所有線程被刷爆並等待處理工作,同時堵塞新請求,最終導致Java容器崩潰。艙壁模式將遠程資源調用隔離在它們自己的線程池中,以便可以控制單個表現不佳的服務,而不會使該容器崩潰。
Hystrix使用線程池來委派所有對遠程服務的請求。在默認情況下,所有的Hystrix命令都將共享同一個線程池來處理請求。這個線程池將有10個線程來處理遠程服務調用,而這些遠程服務調用可以是任何東西,包括REST服務調用、數據庫調用等。圖5-7說明了這一點。

圖5-7 多種資源類型共享默認的Hystrix線程池
在應用程序中訪問少量的遠程資源時,這種模型運行良好,並且各個服務的調用量分布相對均勻。問題是,如果某些服務具有比其他服務高得多的請求量或更長的完成時間,那么最終可能會導致Hystrix線程池中的線程耗盡,因為一個服務最終會占據默認線程池中的所有線程。
幸好,Hystrix提供了一種易於使用的機制,在不同的遠程資源調用之間創建艙壁。圖5-8展示了Hystrix管理的資源被隔離到它們自己的“艙壁”時的情況。

圖5-8 Hystrix命令綁定到隔離的線程池
要實現隔離的線程池,我們需要使用@HystrixCommand注解的其他屬性。接下來的代碼將完成以下操作。
(1)為getLicensesByOrg()調用建立一個單獨的線程池。
(2)設置線程池中的線程數。
(3)設置單個線程繁忙時可排隊的請求數的隊列大小。
代碼清單5-6展示了如何圍繞服務調用建立一個艙壁,該服務調用從許可證服務查詢許可證數據。
代碼清單5-6 圍繞getLicensesByOrg()方法創建艙壁
@HystrixCommand(fallbackMethod= "buildFallbackLicenseList",
threadPoolKey = "licenseByOrgThreadPool",
⇽--- threadPoolKey屬性定義線程池的唯一名稱
threadPoolProperties = {
⇽--- threadPoolProperties屬性用於定義和定制threadPool的行為
@HystrixProperty(name = "coreSize", value="30"),
⇽--- coreSize屬性用於定義線程池中線程的最大數量
@HystrixProperty(name = "maxQueueSize", value="10")}
⇽--- maxQueueSize用於定義一個位於線程池前的隊列,它可以對傳入的請求進行排隊 )
public List<License> getLicensesByOrg(String organizationId){
return licenseRepository.findByOrganizationId(organizationId);
}
要注意的第一件事是,我們在@HystrixCommand注解中引入了一個新屬性,即threadPoolkey。這向 Hystrix 發出信號,我們想要建立一個新的線程池。如果在線程池中沒有設置任何進一步的值,Hystrix會使用threadPoolKey屬性中的名稱搭建一個線程池,並使用所有的默認值來對線程池進行配置。
要定制線程池,應該使用@HystrixCommand上的threadPoolProperties屬性。此屬性使用HystrixProperty對象的數組,這些HystrixProperty對象用於控制線程池的行為。使用coreSize屬性可以設置線程池的大小。
開發人員還可以在線程池前創建一個隊列,該隊列將控制在線程池中線程繁忙時允許堵塞的請求數。此隊列大小由maxQueueSize屬性設置。一旦請求數超過隊列大小,對線程池的任何其他請求都將失敗,直到隊列中有空間。
請注意有關maxQueueSize屬性的兩件事情。首先,如果將其值設置為−1,則將使用Java SynchronousQueue來保存所有傳入的請求。同步隊列本質上會強制要求正在處理中的請求數量永遠不能超過線程池中可用線程的數量。將maxQueueSize設置為大於1的值將導致Hystrix使用Java LinkedBlockingQueue。LinkedBlockingQueue``的使用允許開發人員即使所有線程都在忙於處理請求,也能對請求進行排隊。
要注意的第二件事是,maxQueueSize屬性只能在線程池首次初始化時設置(例如,在應用程序啟動時)。Hystrix允許通過使用queueSizeRejectionThreshold屬性來動態更改隊列的大小,但只有在maxQueueSize屬性的值大於0時,才能設置此屬性。
自定義線程池的適當大小是多少?Netflix推薦以下公式:
服務在健康狀態時每秒支撐的最大請求數×第99百分位延遲時間 (以秒為單位)+ 用於緩沖的少量額外線程
通常情況下,直到服務處於負載狀態,開發人員才能知道它的性能特征。線程池屬性需要被調整的關鍵指標就是,即使目標遠程資源是健康的,服務調用仍然超時。
5.8 基礎進階——微調Hystrix
我們目前已經研究了使用Hystrix創建斷路器模式和艙壁模式的基本概念。現在我們來看看如何真正定制Hystrix斷路器的行為。記住,Hystrix不僅能超時長時間運行的調用,它還會監控調用失敗的次數,如果調用失敗的次數足夠多,那么Hystrix會在請求發送到遠程資源之前,通過使調用失敗來自動阻止未來的調用到達服務。
這樣做有兩個原因。首先,如果遠程資源有性能問題,那么快速失敗將防止應用程序等待調用超時。這顯著降低了調用應用程序或服務所導致的資源耗盡問題和崩潰的風險。其次,快速失敗和阻止來自服務客戶端的調用有助於苦苦掙扎的服務保持其負載,而不會徹底崩潰。快速失敗給了性能下降的系統一些時間去進行恢復。
要了解如何在Hystrix中配置斷路器,需要先了解Hystrix如何確定何時跳閘斷路器的流程。圖5-9展示了Hystrix在遠程資源調用失敗時使用的決策過程。

圖5-9 Hystrix經過一系列檢查來確定是否跳閘
每當Hystrix命令遇到服務錯誤時,它將開始一個10 s的計時器,用於檢查服務調用失敗的頻率。這個10 s窗口是可配置的。Hystrix做的第一件事就是查看在10 s內發生的調用數量。如果調用次數少於在這個窗口內需要發生的最小調用次數,那么即使有幾個調用失敗,Hystrix 也不會采取行動。例如,在Hystrix考慮采取行動之前,需要在10 s之內進行調用的次數的默認值為20。如果這些調用之中有15個在10 s內發生調用失敗,只要在10 s之內調用次數達不到20次,那么即使15個調用都失敗,這些調用的數量也不足以讓斷路器發生跳閘。Hystrix將繼續讓調用通過,到達遠程服務。
在10 s窗口內達到最少的遠程資源調用次數時,Hystrix將開始查看整體故障的百分比。如果故障的總體百分比超過閾值,Hystrix將觸發斷路器,使將來幾乎所有的調用都失敗。正如稍后即將討論的那樣,Hystrix將會讓部分調用通過來進行“測試”,以查看服務是否恢復。錯誤閾值的默認值為50%。
如果超過錯誤閾值的百分比,Hystrix 將“跳閘”斷路器,防止更多的調用訪問遠程資源。如果遠程調用失敗的百分比未達到要求的閾值,並且10 s窗口已過去,Hystrix將重置斷路器的統計信息。
當Hystrix在一個遠程調用上“跳閘”斷路器時,它將嘗試啟動一個新的活動窗口。每隔5 s(這個值是可配置的),Hystrix會讓一個調用到達這個苦苦掙扎的服務。如果調用成功,Hystrix將重置斷路器並重新開始讓調用通過。如果調用失敗,Hystrix將保持斷路器斷開,並在另一個5 s里再次嘗試上述步驟。
基於此,開發人員可以使用5個屬性來定制斷路器的行為。@HystrixCommand注解通過commandPoolProperties屬性公開了這5個屬性。其中,threadPoolProperties屬性用於設置Hystrix命令中使用的底層線程池的行為,而commandPoolProperties屬性用於定制與Hystrix命令關聯的斷路器的行為。代碼清單5-7展示了這些屬性的名稱以及如何在每個屬性中設置值。
5.8 基礎進階微調Hystrix
代碼清單5-7 配置斷路器的行為
@HystrixCommand( fallbackMethod = "buildFallbackLicenseList",
threadPoolKey = "licenseByOrgThreadPool",
threadPoolProperties = { @HystrixProperty(name = "coreSize",value="30"), @HystrixProperty(name="maxQueueSize"value="10"),
},
commandPoolProperties = {
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"),
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"), @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="15000"),
@HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")
}
)
public List<License> getLicensesByOrg(String organizationId){
logger.debug("getLicensesByOrg Correlation id: {}",
UserContextHolder .getContext() .getCorrelationId());
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
第一個屬性circuitBreaker.requestVolumeThreshold用於控制Hystrix考慮將該斷路器跳閘之前,在10 s之內必須發生的連續調用數量。第二個屬性circuitBreaker.errorThresholdPercentage是在超過circuitBreaker.requestVolumeThreshold值之后在斷路器跳閘之前必須達到的調用失敗(由於超時、拋出異常或返回HTTP 500)百分比。上述代碼示例中的最后一個屬性circuitBreaker.sleepWindowInMilliseconds是在斷路器跳閘之后,Hystrix允許另一個調用通過以便查看服務是否恢復健康之前Hystrix的休眠時間。
最后兩個Hystrix屬性metrics.rollingStats.timeInMilliseconds和metrics.rollingStats.numBuckets的命名與前面的屬性有所不同,但它們仍然是控制斷路器的行為的。第一個屬性metrics.rollingStats.timeInMilliseconds用於控制Hystrix用來監視服務調用問題的窗口大小,其默認值為10 000ms(即10 s)。
第二個屬性metrics.rollingStats.numBuckets控制在定義的滾動窗口中收集統計信息的次數。在這個窗口中,Hystrix在桶(bucket)中收集度量數據,並檢查這些桶中的統計信息,以確定遠程資源調用是否失敗。給metrics.rollingStats.timeInMilliseconds設置的值必須能被定義的桶的數量值整除。例如,在代碼清單 5-7 所示的自定義設置中,Hystrix將使用15 s的窗口,並將統計數據收集到長度為3 s的5個桶中。
注意
檢查的統計窗口越小且在窗口中保留的桶的數量越多,就越會加劇高請求服務的CPU利用率和內存利用率。要意識到這一點,避免將度量收集窗口和桶設置為太細的粒度,除非你需要這種可見性級別。
重新審視Hystrix配置
Hystrix 庫是高度可配置的,可以讓開發人員嚴格控制使用它定義的斷路器模式和艙壁模式的行為。開發人員可以通過修改Hystrix斷路器的配置,控制Hystrix在超時遠程調用之前需要等待的時間。開發人員還可以控制Hystrix斷路器何時跳閘以及Hystrix何時嘗試重置斷路器。
使用Hystrix,開發人員還可以通過為每個遠程服務調用定義單獨的線程組,然后為每個線程組配置相應的線程數來微調艙壁實現。這允許開發人員對遠程服務調用進行微調,因為某些遠程資源調用具有較高的請求量。
在配置Hystrix環境時,需要記住的關鍵點是,開發人員可以使用Hystrix的3個配置級別:
(1)整個應用程序級別的默認值;
(2)類級別的默認值;
(3)在類中定義的線程池級別。
每個Hystrix屬性都有默認設置的值,這些值將被應用程序中的每個@HystrixCommand注解所使用,除非這些屬性值在Java類級別被設置,或者被類中單個Hystrix線程池級別的值覆蓋。
Hystrix確實允許開發人員在類級別設置默認參數,以便特定類中的所有Hystrix命令共享相同的配置。類級屬性是通過一個名為@DefaultProperties的類級注解設置的。例如,如果希望特定類中的所有資源的超時時間均為10 s,
則可以按以下方式設置@DefaultProperties:
@DefaultProperties(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000")
}
class MyService { ... }
除非在線程池級別上顯式地覆蓋,否則所有線程池都將繼承應用程序級別的默認屬性或類中定義的默認屬性。Hystrix的threadPoolProperties和commandProperties也綁定到已定義的命令鍵。
注意
我在本章編碼示例的應用程序代碼中硬編碼了所有的Hystrix值。在生產環境中,最有可能需要調整的Hystrix數據(超時參數、線程池計數)將被外部化到Spring Cloud Config。通過這種方式,如果需要更改參數值,就可以在更改完參數值之后重新啟動服務實例,而無需重新編譯和重新部署應用程序。
對於單個Hystrix池,本書將保持配置盡可能接近代碼並將線程池配置置於@HystrixCommand注解中。
表5-1總結了用於創建和配置@HystrixCommand注解的所有配置值。
表5-1 @HystrixCommand注解的配置值
屬 性 名 稱
|
默認值
|
描 述
|
fallbackMethod
|
None
|
標識類中的方法,如果遠程調用超時,將調用該方法。
回調方法必須與@HystrixCommand注解在同一個類中,
並且必須具有與調用類相同的方法簽名。如果值不存在,
Hystrix會拋出異常
|
threadPoolKey
|
None
|
給予@HystrixCommand一個唯一的名稱,
並創建一個獨立於默認線程池的線程池。
如果沒有定義任何值,則將使用默認的Hystrix線程池
|
threadPoolProperties
|
None
|
核心的Hystrix注解屬性,用於配置線程池的行為
|
coreSize
|
1 0
|
設置線程池的大小
|
maxQueueSize
|
− 1
|
設置線程池前面的最大隊列大小。如果設置為−1,
則不使用隊列,Hystrix將阻塞請求,
直到有一個線程可用來處理
|
circuitBreaker. requestVolumeThreshold
|
20
|
設置Hystrix開始檢查斷路器是否跳閘之前滾動窗口中
必須處理的最小請求數 注意:
此值只能使用commandPoolProperties屬性設置
|
circuitBreaker. errorThresholdPercentage
|
50
|
在斷路器跳閘之前,滾動窗口內必須達到的故障百分比
注意:此值只能使用commandPoolProperties屬性設置
|
circuitBreaker. sleepWindowInMilliseconds
|
5,000
|
在斷路器跳閘之后,Hystrix嘗試進行服務調用之前將要等待的時間(以毫秒為單位) 注意:此值只能使用commandPoolProperties屬性設置
|
metricsRollingStats. timeInMilliseconds
|
10,000
|
Hystrix收集和監控服務調用的統計信息的滾動窗口
(以毫秒為單位)
|
metricsRollingStats. numBuckets
|
10
|
Hystrix在一個監控窗口中維護的度量桶的數量。
監視窗口內的桶數越多,
Hystrix在窗口內監控故障的時間越低
|