初識 Hystrix


運用服務註冊發現,除了具有伸縮性之外,另一個好處是,掛掉的服務會從服務註冊表中移除,客戶端後續才不致於採用了故障的服務。

然而,有時怕的是服務要掛不掛,間歇性地提供服務或者是過長的處理時間等,由於服務請求往往是同步地,因而不佳的服務狀態,會連帶拖累客戶端,甚至被某服務拖累的另一服務也開始緩慢,從而使得依賴它的客戶端也被拖累,接連下去,引發服務連鎖性的癱瘓。

若請求服務之後等待的處高時間若過長,就放棄並引發例外,客戶端就有機會做進一步處理,例如回應此次處理失敗,或者是採取替代的應變方案。

你可以使用 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,例如,MessageServiceRestnewestMessages 方法:

@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,至於提供斷路器功能的配置、個別服務執行緒池設定等,留待之後的文件再來談。