運用服務註冊發現,除了具有伸縮性之外,另一個好處是,掛掉的服務會從服務註冊表中移除,客戶端後續才不致於採用了故障的服務。
然而,有時怕的是服務要掛不掛,間歇性地提供服務或者是過長的處理時間等,由於服務請求往往是同步地,因而不佳的服務狀態,會連帶拖累客戶端,甚至被某服務拖累的另一服務也開始緩慢,從而使得依賴它的客戶端也被拖累,接連下去,引發服務連鎖性的癱瘓。
若請求服務之後等待的處高時間若過長,就放棄並引發例外,客戶端就有機會做進一步處理,例如回應此次處理失敗,或者是採取替代的應變方案。
你可以使用 Netflix 開放的 Hystrix 程式庫,Spring Cloud 對 Hystrix 作了整合,可以更方便地實現以上的功能,Hystrix 也提供速錯(Fail-Fast),可在請求服務的失敗率過高時,直接斷開服務,而不是每次都等待逾時,也可以為個別服務配置個別執行緒池(而不是全部服務共用一個執行緒池),儘管不全然只是斷開服務的功能,不過社群中通常會以斷路器來描述 Hystrix 提供的特性。
就撰寫本文的這個時間點,Hystrix 的版本為 1.5.18,處於維護狀態,因為 Netflix 認為已符合其目前應用需求,不再開發新功能,然而,Netflix 在〈Hystrix Status〉談到,歡迎也鼓勵社群成員接手。
雖然處於維護狀態,如果沒接觸過斷路器之類的服務,從 Hystrix 認識起還是有價值,特別是透過 Spring Cloud 整合,更可以把焦點放在其解決了什麼樣的問題上,若必要 Spring 大概會接手 Hystrix,或者自己搞一個吧!
(例如,Zuul 2 曾預計於 2016 年底左右開放,然而拖到了 2018 年 4 月,Spring 後來不整合 Zuul 2,自己搞了個 Spring Cloud Gateway。)
如果想要在 Spring 中使用 Hystrix,要在 build.gradle 加入相依:
implementation('org.springframework.cloud:spring-cloud-starter-netflix-hystrix')
然後,在啟動 Spring Boot 應用程式的主類別上,加註 @EnableCircuitBreaker
,在這邊以〈gossip 服務(二)拆分〉中的 gossip 成果為例:
...
@SpringBootApplication(
scanBasePackages={
"cc.openhome.controller",
"cc.openhome.model",
"cc.openhome.aspect"
}
)
@EnableCircuitBreaker
@PropertySource("classpath:path.properties")
public class GossipApplication {
...
接著,對於某些方法若要設定超時監控,可以加註 @HystrixCommand
,例如,MessageServiceRest
的 newestMessages
方法:
@HystrixCommand
public List<Message> newestMessages(int n) {
sleep(2000); // 故意加上阻斷
RequestEntity<Void> request = RequestEntity
.get(URI.create(String.format("http://localhost:8083/newestMessages?n=%d", n)))
.build();
ResponseEntity<Resources<Message>> response =
restTemplate.exchange(request, new TypeReferences.ResourcesType<Message>() {});
return new ArrayList<>(response.getBody().getContent());
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Hystrix 使用 Command 模式來實現請求監控,在 Spring Cloud 的整合下,@HystrixCommand
標註的方法會自動生成 Command 物件作為代理,若是請求逾時(預設為 1000 毫秒),那麼會引發 TimeoutException
,雖然不建議更改預設的逾時的毫秒數(應該盡可能地解決服務對象造成逾時的原因),不過是可以透過 @HystrixProperty
,設定 execution.isolation.thread.timeoutInMilliseconds
值:
@HystrixCommand(
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value = "3000")
}
)
public List<Message> newestMessages(int n) {
如果想在無法完成服務請求時,提供替代的應變方案,可以使用 fallbackMethod
來指定要呼叫的方法,被呼叫的替代方法,必須位於同一個類別中,而且與 @HystrixCommand
標註的方法,具有相同的參數列與傳回型態,fallbackMethod
指定的方法可以重載,呼叫替代方案的方法時會傳入原本的引數,例如,在逾時發生時提供替代的訊息清單:
@HystrixCommand(fallbackMethod = "fallbackMessages")
public List<Message> newestMessages(int n) {
sleep(2000);
RequestEntity<Void> request = RequestEntity
.get(URI.create(String.format("http://localhost:8083/newestMessages?n=%d", n)))
.build();
ResponseEntity<Resources<Message>> response =
restTemplate.exchange(request, new TypeReferences.ResourcesType<Message>() {});
return new ArrayList<>(response.getBody().getContent());
}
private List<Message> fallbackMessages(int n) {
return Arrays.asList(new Message("gossiper", 0L, "啊嗚!發生問題,自 epoch 之後都沒有八卦!… XD"));
}
若個別服務請求的 @HystrixCommand
設定上有重複之處,可以在類別上標註 @DefaultProperties
,將共用的屬性集中。例如:
@DefaultProperties(
defaultFallback = "fallbackMessages",
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value = "3000")
}
)
public class MessageServiceRest implements MessageService {
在設定 defaultFallback
時要注意,指定的方法不能有參數。
這邊先初步認識一下 Hystrix,至於提供斷路器功能的配置、個別服務執行緒池設定等,留待之後的文件再來談。