第5章 Hystrix客戶端彈性模式
所有的系統,包括分佈式系統,都會遇到故障,可能還會多些,哈哈。我們經常會通過處理策略繞過死掉的服務,但是有時候性能不佳的服務確成爲了服務的瓶頸,他們難以檢測,而且有可能引起連鎖反應,使整個系統崩潰。
面對這種情況,一種處理方式是實現客戶端彈性模式,即在遠程服務發生錯誤或者表現不佳時保護遠程資源的調用客戶端免於崩潰,這些模式的目標是讓客戶端“快速失敗”,而不是消耗類似數據庫連接和線程池之類的寶貴資源。下面通過一張圖簡單介紹下4種客戶端彈性模式:
如果使用斷路器來保護服務,那麼斷路器的角色就是中間人,其處於應用程序和遠程服務之間,簡單示意下:
在上圖中,A不會直接調用B,而是把實際的調用委託給斷路器,斷路器將接管這個調用,並將它包裝在獨立於原始調用者的線程中,那麼客戶端不再直接等待調用,相當於是異步的。斷路器會監視這個調用,如果在一定時間內在調用服務B上發生了足夠多的錯誤,那麼斷路器就會跳閘,並且在不調用服務B的情況下,判斷所有對於服務B的調用都會失敗。跳閘後,服務B就有了喘息的機會,而服務A可以執行後備方案。最後,斷路器會讓少量的請求調用直達服務B,如果這些調用連續多次成功,斷路器就會復位。
下面我們進入實戰環節,使用Hystrix。首先需要添加對於Hystrix的依賴,如下:
<!--告訴Maven去拉取Hystrix的依賴項-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<!--拉取核心Hystrix庫-->
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>1.5.9</version>
</dependency>
接下來需要,使用註解@EnableCircuitBreaker標註引導類,其目的就是告訴SpringCloud將要爲服務使用Hystrix。
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class LicenseApplication {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(LicenseApplication.class, args);
}
}
本節的實例代碼功能如下:
- 使用Hystrix斷路器包裝licenseservice對於數據庫的調用
- 使用Hystrix包裝licenseservice和organazitionservice之間的內部調用
雖然是兩種不同的用法,但是我們應該明白,其實是一樣的,其都是對於本地客戶端的保護,調用的都是遠程資源。
下面實現第一種調用,許可證服務將通過同步調用來檢索數據庫,但是當其繼續進行處理之前會等待SQL語句完成或者斷路器超時。實現這一功能只要加個@HystrixCommand註解即可,其作用是將方法標記爲由斷路器進行管理,當Spring看到這一註解時,它將生成一個動態代理類,該代理類將包裝該方法,並通過專門用於處理遠程資源調用的線程池來管理該方法的調用。實例代碼如下:
@HystrixCommand
public List<License> getLicensesByOrg(String organizationId){
logger.debug("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
每當調用超過1秒時,斷路器將中斷對getLicensesByOrg()的調用。其中 randomlyRunLong()方法用於模擬超時。下面我們調用下,看下效果
現在如果查詢時間過長,許可證服務將中斷其對數據庫的調用,此時拋出了一個異常。
那麼,我們可以定製Hystrix的超時時間,可以在註解上加上commandProperties屬性,其允許開發人員提供附加的屬性來定製Hystrix,比如,我們定義超時時間爲12秒,那麼我們前面的調用就不會超時 ,代碼如下:
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "12000")
})
public License getLicense(String organizationId,String licenseId) {
License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId);
randomlyRunLong();
Organization org = getOrganization(organizationId);
return license
.withOrganizationName( org.getName())
.withContactName( org.getContactName())
.withContactEmail( org.getContactEmail() )
.withContactPhone( org.getContactPhone() )
.withComment(config.getExampleProperty());
}
其實到這裏,斷路器模式,我相信大家都已經有了一個概念了。下面我們簡單瞭解下後備模式。後備模式讓開發人員有機會攔截服務故障,並提供替代方案。我們實現的方式比較簡單,在@HystrixCommand註解中添加fallbackMethod屬性,其值代表了後備方法的名稱,需要注意的是後備方法必須與原方法具有相同的簽名,比較容易理解,因爲,你必須提供的是一個一模一樣的東西給客戶端,就像我給你要個蘋果,你沒有熟的了,你給我個半熟的也行,但是你不能給我一個橘子。雖然後備策略寫起來簡單,但是其中卻有些門道:
- 如果自己使用後備來捕獲超時異常,如果只做日誌記錄,就應該使用try…catch來捕獲,並將日誌記錄在catch中進行。
- 後備方法中如果還要調用其他微服務,還應該使用後備方案,這不是過度防禦。
簡單後備模式的代碼如下:
@HystrixCommand(fallbackMethod = "buildFallbackLicenseList",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "1000")
})
public License getLicense(String organizationId,String licenseId) {
License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId);
randomlyRunLong();
Organization org = getOrganization(organizationId);
return license
.withOrganizationName( org.getName())
.withContactName( org.getContactName())
.withContactEmail( org.getContactEmail() )
.withContactPhone( org.getContactPhone() )
.withComment(config.getExampleProperty());
}
其中buildFallbackLicenseList()的代碼如下:
private License buildFallbackLicenseList(String organizationId,String licenseId){
License license = new License()
.withId("0000000-00-00000")
.withOrganizationId( organizationId )
.withProductName("Sorry no licensing information currently available");
return license;
}
,多運行幾次,結果如下:
後備模式到這裏,應該也有了一個概念,下面來介紹艙壁模式。艙壁模式的重要性在於,在不適用艙壁模式的情況下,微服務的調用默認是使用同一批線程來執行調用的,這些線程是爲了處理java容器的請求而預留的。在存在大量請求的情況下,一個服務出現問題,其最終會佔據默認線程池中的所有線程,造成新請求的堵塞,從而導致JAVA容器崩潰,艙壁模式將遠程資源調用隔離在自己的線程池中,以便控制單個表現不佳的服務,避免容器的崩潰。Hystrix提供了這種易用的機制,在不同的遠程資源調用之間建立艙壁。如下圖所示:
接下來在代碼中實現,也比較方便,只需要幾個註解即可,如下:
關於Hystrix的基礎,上面已經講完了,如果只是想了解下這塊內容,下面的東西不用看了,如果想更深入瞭解Hystrix,那麼我們一起來探祕。
前文提到過,Hystrix不僅可以中斷長時間的調用,還可以統計調用失敗的次數,如果失敗過多,其會在請求發送到遠程資源之前,直接使調用失敗來阻止未來的調用。這樣做有兩個原因:一可以避免調用應用程序所導致的資源耗盡問題二可以給出現問題的服務以喘息時間。下面瞭解下Hystrix的調用失敗時使用的決策。
基於上述表述,可以使用幾個屬性來定義這些配置:
@HystrixCommand(fallbackMethod = "buildFallbackLicenseList",
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "1000"),
//用於控制斷路器跳閘之前,在10s內必須連續發生的調用數量
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
//失敗百分比
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"),
//跳閘之後,允許一個調用通過斷路器以便查看服務是否恢復健康之前的休眠時間
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"),
//窗口的大小
@HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="10000"),
//窗口時間內,收集統計信息的次數,即10秒內,收集長度爲2秒的信息到5個bucket中
@HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")
},
//定義線程池的唯一名稱,其實也是一個信號,即我們要定義一個新的線程池
threadPoolKey = "licenceThreadPool",
threadPoolProperties = {
//定義線程池中線程的最大數量
@HystrixProperty(name = "coreSize",value = "30"),
//在線程池前創建一個隊列進行緩衝,類似於線程池的設計
@HystrixProperty(name = "maxQueueSize",value = "10")
}
)
public License getLicense(String organizationId,String licenseId) {
License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId);
randomlyRunLong();
Organization org = getOrganization(organizationId);
return license
.withOrganizationName( org.getName())
.withContactName( org.getContactName())
.withContactEmail( org.getContactEmail() )
.withContactPhone( org.getContactPhone() )
.withComment(config.getExampleProperty());
}
這一節,就是這個樣子