springcloud 總集:https://www.tapme.top/blog/detail/2019-02-28-11-33
本次用到全部程式碼見文章最下方。
一、為什麼要有客戶端彈性模式
所有的系統都會遇到故障,分散式系統單點故障概率更高。如何構建應用程式來應對故障,是每個軟體開發人員工作的關鍵部分。但是通常在構建系統時,大多數工程師只考慮到基礎設施或關鍵服務徹底發生故障,使用諸如叢集關鍵伺服器、服務間的負載均衡以及異地部署等技術。儘管這些方法考慮到元件系統的徹底故障,但他們之解決了構建彈性系統的一小部分問題。當服務崩潰時,很容易檢測到該服務以及失效,因此應用程式可以饒過它。然而,當服務執行緩慢時,檢測到這個服務效能越發低下並繞過它是非常困難的,因為以下幾個原因:
- 服務的降級可以是以間歇性的故障開始,並形成不可逆轉的勢頭————可能開始只是一小部分服務呼叫變慢,直到突然間應用程式容器耗盡了執行緒(所有執行緒都在等待呼叫完成)並徹底崩潰。
- 應用程式通常的設計是處理遠端資源的徹底故障,而不是部分降級————通常,只要服務沒有完全死掉,應用程式將繼續呼叫這個服務,直到資源耗盡崩潰。
效能較差的遠端服務會導致很大的潛在問題,它們不僅難以檢測,還會觸發連鎖反應,從而影響整個應用程式生態系統。如果沒有適當的保護措施,一個效能不佳的服務可以迅速拖垮整個應用程式。基於雲、基於微服務的應用程式特別容易受到這些型別的終端影響,因為這些應用由大量細粒度的分散式服務組成,這些服務在完成使用者的事務時涉及不同的基礎設施。
二、什麼是客戶端彈性模式
客戶端彈性模式是在遠端服務發生錯誤或表現不佳時保護遠端資源(另一個微服務呼叫或者資料庫查詢)免於崩潰。這些模式的目標是為了能讓客戶端“快速失敗”,不消耗諸如資料庫連線、執行緒池之類的資源,還可以避免遠端服務的問題向客戶端的消費者進行傳播,引發“雪崩”效應。spring cloud 主要使用的有四種客戶端彈性模式:
- 客戶端負載均衡(client load balance)模式
上一篇已經說過,這裡不再贅述。
- 斷路器(circuit breaker)模式
本模式模仿的是電路中的斷路器。有了軟體斷路器,當遠端服務被呼叫時,斷路器將監視這個呼叫,如果呼叫時間太長,斷路器將介入並中斷呼叫。此外,如果對某個遠端資源的呼叫失敗次數達到某個閾值,將會採取快速失敗策略,阻止將來呼叫失敗的遠端資源。
- 後備(fallback)模式
當遠端呼叫失敗時,將執行替代程式碼路徑,並嘗試通過其他方式來處理操作,而不是產生一個異常。也就是為遠端操作提供一個應急措施,而不是簡單的丟擲異常。
- 艙壁(bulkhead)模式
艙壁模式是建立在造船的基礎概念上。我們都知道一艘船會被劃分為多個水密艙(艙壁),因而即使少數幾個部位被擊穿漏水,整艘船並不會被淹沒。將這個概念帶入到遠端呼叫中,如果所有呼叫都使用的是同一個執行緒池來處理,那麼很有可能一個緩慢的遠端呼叫會拖垮整個應用程式。在艙壁模式中可以隔離每個遠端資源,並分配各自的執行緒池,使之互不影響。
下圖展示了這些模式是如何運用到微服務中的:
三、spring cloud 中使用
使用 Netflix 的 Hystrix 庫來實現上述彈性模式。繼續使用上一節的專案,給 licensingservice 服務實現彈性模式。
1、程式碼修改
1、依賴引入
首先修改 POM 檔案,新增下面兩個依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<!--本依賴不是必須的,spring-cloud-starter-hystrix已經帶了,但是在Camden.SR5發行版本中使用了1.5.6,這個版本有一個不一致的地方,在沒有後備的情況下會丟擲java.lang.reflect.UndeclaredThrowableException而不是com.netflix.hystrix.exception.HystrixRuntimeException,
在後續版本中修復了這個問題-->
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>1.5.9</version>
</dependency>
然後在啟動類上加入@EnableCircuitBreaker
啟用 Hystrix。
2、實現斷路器
首先修改 organizationservice 專案中的 OrganizationController,模擬延遲,每隔兩次讓執行緒 sleep 2 秒
@RestController
public class OrganizationController {
private static int count=1;
@GetMapping(value = "/organization/{orgId}")
public Object getOrganizationInfo(@PathVariable("orgId") String orgId) throws Exception{
if(count%2==0){
TimeUnit.SECONDS.sleep(2);
}
count++;
Map<String, String> data = new HashMap<>(2);
data.put("id", orgId);
data.put("name", orgId + "公司");
return data;
}
}
只需在方法上新增@HystrixCommand
,即可實現超時短路。如果 Spring 掃描到該註解註釋的類,它將動態生成一個代理,來包裝這個方法,並通過專門用於處理遠端呼叫的執行緒池來管理對該方法的所有呼叫。
修改 licensingservice 服務中的 OrganizationByRibbonService,OrganizationFeignClient,給其中的方法加上@HystrixCommand
的註解。然後再訪問介面localhost:10011/licensingByRibbon/11313,localhost:10011/licensingByFeign/11313。多次訪問可發現丟擲錯誤com.netflix.hystrix.exception.HystrixRuntimeException
,斷路器生效,預設情況下操時時間為 1s。
{
"timestamp": 1543823192424,
"status": 500,
"error": "Internal Server Error",
"exception": "com.netflix.hystrix.exception.HystrixRuntimeException",
"message": "OrganizationFeignClient#getOrganization(String) timed-out and no fallback available.",
"path": "/licensingByFeign/11313/"
}
可通過設定註解引數來修改操時時間。設定超時時間大於 2s 後便不會報操時錯誤。(不知道為什麼在 Feign 中設定失敗,ribbon 中正常。)。一般都是將配置寫在配置檔案中。
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "20000")
})
3、後備處理
由於遠端資源的消費者和資源本身之間存在存在一個"中間人",因此開發人員能夠攔截服務故障,並選擇替代方案。在 Hystrix 中進行後備處理,非常容易實現。
- 在 ribbon 中的實現
只需在@HystrixCommand
註解中加入屬性 fallbackMethod="methodName",那麼在執行失敗時,便會執行後備方法。注意防備方法必須和被保護方法在同一個類中,並且方法簽名必須相同。修改 licensingservice 中 service 包下的 OrganizationByRibbonService 類,改為如下:
@Component
public class OrganizationByRibbonService {
private RestTemplate restTemplate;
@Autowired
public OrganizationByRibbonService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
},fallbackMethod = "getOrganizationWithRibbonBackup")
public Organization getOrganizationWithRibbon(String id) throws Exception {
ResponseEntity<Organization> responseEntity = restTemplate.exchange("http://organizationservice/organization/{id}",
HttpMethod.GET, null, Organization.class, id);
return responseEntity.getBody();
}
public Organization getOrganizationWithRibbonBackup(String id)throws Exception{
Organization organization = new Organization();
organization.setId("0");
organization.setName("組織服務呼叫失敗");
return organization;
}
}
啟動應用,多次訪問localhost:10011/licensingByRibbon/11313/,可以發現呼叫失敗時,會啟用後備方法。
- 在 feign 中實現
在 feign 中實現後備模式,需要編寫一個 feign 介面的實現類,然後在 feign 介面中指定該類。以 licensingservice 為例。首先在 client 包中新增一個 OrganizationFeignClientImpl 類,程式碼如下:
@Component
public class OrganizationFeignClientImpl implements OrganizationFeignClient{
@Override
public Organization getOrganization(String orgId) {
Organization organization=new Organization();
organization.setId("0");
organization.setName("後備模式返回的資料");
return organization;
}
}
然後修改 OrganizationFeignClient 介面的註解,將@FeignClient("organizationservice")
改為@FeignClient(name="organizationservice",fallback = OrganizationFeignClientImpl.class
。
重啟專案,多次訪問localhost:10011/licensingByFeign/11313/,可發現後備服務起作用了。
在確認是否要啟用後備服務時,要注意以下兩點:
後備是一種在資源操時或失敗時提供行動方案的機制。如果只是用後備來捕獲操時異常然後只做日誌記錄,那隻需要 try..catch 即可,捕獲 HystrixRuntimeException 異常。
注意後備方法所執行的操作。如果在後備服務中呼叫另一個分散式服務,需要注意用@HystrixCommand 方法註解包裝後備方法。
4、實現艙壁模式
在基於微服務的應用程式中,通常需要呼叫多個微服務來完成特定的任務,在不適用艙壁的模式下,這些呼叫預設是使用同一批執行緒來執行呼叫的,而這些執行緒是為了處理整個 Java 容器的請求而預留的。因此在存在大量請求的情況下,一個服務出現效能問題會導致 Java 容器內的所有執行緒被佔用,同時阻塞新請求,最終容器徹底崩潰。
Hystrix 使用執行緒池來委派所有對遠端服務的呼叫,預設情況下這個執行緒池有 10 個工作執行緒。但是這樣很容易出現一個執行緩慢的服務佔用全部的執行緒,所有 hystrix 提供了一種一種易於使用的機制,在不同的遠端資源呼叫間建立‘艙壁’,將不同服務的呼叫隔離到不同的執行緒池中,使之互不影響。
要實現隔離的執行緒池,只需要在@HystrixCommand
上加入執行緒池的註解,這裡以 ribbon 為例(Feign 類似)。修改 licensingservice 中 service 包下的 OrganizaitonByRibbonService 類,將getOrganizationWithRibbon
方法的註解改為如下:
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
}, fallbackMethod = "getOrganizationWithRibbonBackup",
threadPoolKey = "licenseByOrgThreadPool",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30"),
@HystrixProperty(name = "maxQueueSize", value = "10")
})
如果將maxQueueSize
屬性值設為-1,將使用SynchronousQueue
儲存所有的傳入請求,同步佇列會強制要求正在處理中的請求數量永遠不能超過執行緒池的大小。設為大於 1 的值將使用LinkedBlockingQueue
。
注意:示例程式碼中都是硬編碼屬性值到 Hystrix 註解中的。在實際應用環境中,一般都是將配置項配置在 Spring Cloud Config 中的,方便統一管理。
本次用到全部程式碼:點選跳轉
本篇原創釋出於:FleyX 的個人部落格