使用Resilience4J實現斷路器模式

banq發表於2019-06-22

斷路器是一種模式,可以防止整個架構中單個微服務的故障級聯,從而確保系統具有彈性。該模式可以通過像Hystrix或Resilience4j這樣的程式碼庫實現,或者通過底層基礎設施來實現,例如使用Istio。

Hystrix vs. Resilience4j簡介

Hystrix是Netflix提供的一個開源庫,旨在提高分散式系統的彈性,使HTTP請求在其分散式元件之間進行通訊。它通過實現斷路器模式實現。

Resilience4J是一個受Hystrix啟發的獨立庫,它建立在功能程式設計的原理之上。兩者之間最顯著的區別在於,雖然Hystrix採用物件導向的設計,其中對外部系統的呼叫必須包含在HystrixCommand提供多種功能中,但Resilience4J依賴於函式組合來讓您堆疊所需的特定裝飾器。

那些裝飾器當然包括斷路器,還包括速率限制器,重試和隔板。這些裝飾器可以同步或非同步執行,充分利用Java 8中引入的lambda。

Resilience4J的其他優點包括更精細的配置選項(例如,關閉斷路器模式所需的成功執行次數)和更輕的依賴性足跡。

Java中的函式程式設計簡介

函式組合背後的想法是:

  • 如果函式f被定義為Function<X, Y>- 將型別X作為輸入並返回型別的函式Y
  • 如果函式g定義為Function<Y, Z>
  • 然後可以將新函式h定義為Function<X, Z>組成函式f和g

Java 8在其API中引入了函式程式設計(FP)的一些方面。上面的函式組合可以在Java中翻譯:

public class F implements Function<Integer, Integer> {

    @Override
    public Integer apply(Integer x) {
        return x + 1;
    }
}

public class G implements Function<Integer, Integer> {

    @Override
    public Integer apply(Integer y) {
        return 2 * y;
    }
}

public class H implements  Function<Integer, Integer> {

    @Override
    public Integer apply(Integer x) {
        var f = new F();
        var g = new G();
        Integer y = f.apply(x);
        return g.apply(y);
    }
}

這非常麻煩,因為Java最初設計時考慮了物件導向程式設計(OOP)。

一切都需要屬於一個類,即使這沒有多大意義。因此,為了彌合OOP和FP之間的這種差距,並使FP程式碼更容易編寫,Java 8帶來了函式介面的概念:功能介面是一個帶有單個抽象方法的介面,並且可選擇帶註釋@FunctionalInterface。

可以使用lambda表示法以簡化的方式編寫任何功能介面。例如,Function<T, V>是一個函式介面,因為它有一個抽象方法 - apply()。因此,可以使用lambdas重寫上面的程式碼:

Function<Integer, Integer> h = x -> {
    Function<Integer, Integer> f = y -> y + 1;
    Function<Integer, Integer> g = y -> y + 1;
    return g.apply(f.apply(x));
};

FP的另一個基礎是高階函式。這隻意味著函式是類似於任何其他型別的函式,並且可以作為函式中的引數傳遞,並作為結果返回。

例如,Functioninterface定義以下方法:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

使用這種方法,我們可以簡單地重寫h函式:

var h = f.compose(g);

Resilience4J完全基於函數語言程式設計,並且使用了很多公開的概念。重要的是要記住從Hystrix遷移,因為與通常的Java思維方式相比,這需要進行更改。

Resilience4J入門

HTTP呼叫可以被認為是一個函式:它接受HTTP請求作為輸入,並返回HTTP響應。

同樣,Circuit Breaker可以被認為是一個函式,輸入相同的HTTP請求,返回值如果呼叫成功則返回HTTP響應,如果失敗則返回預設HTTP響應。

因此,使用斷路器就像用第二個“斷路器”函式組成第一個“呼叫”函式。

這是一個示例,用於說明如何使用它:

public class Command {
    public Long run() {
        // Does the actual job of making the HTTP request
    }
}

var cb = CircuitBreaker.ofDefaults("circuit-breaker");
var result = cb.executeSupplier(new Command()::run);

因為Resilience4J中的每個特徵都被建模為一個函式,所以組合這些特徵只需要應用上述的函式組合原理。

這相當於物件導向程式設計中的Decorator模式:目標被“包裝”到裝飾器物件中。

在這裡,我們應用此設計來組成三個函式呼叫。第一個呼叫HTTP端點,第二個呼叫Circuit Breaker,第三個呼叫,如果呼叫失敗則重試。

var cb = CircuitBreaker.ofDefaults("circuit-breaker");
var retry = Retry.ofDefaults("retry");
var cbSupplier = CircuitBreaker.decorateSupplier(cb, new Command()::run);
var retrySupplier = Retry.decorateSupplier(retry, cbSupplier);
var result = retrySupplier.get();

自定義快取裝飾器

使用Resilience4J實現快取功能,我們的要求是:只有在修飾函式呼叫失敗時才應從快取返回。設計我們自己的快取實現功能非常簡單。“函式”這個詞很重要,因為根據Resilience4J的設計原則,狀態 - 快取 - 應該是外部的並傳遞給函式以保持其純淨。為了簡化實現,快取將保留一個值,當裝飾函式成功返回時,可能會替換該值:

public class Cache<T> {

    private T value;

    static <T> Supplier<T> decorateSupplier(Cache<T> cache, Supplier<T> supplier) {
        return Try.ofSupplier(supplier)
                .fold(
                        throwable -> () -> cache.value,
                        value -> {
                            cache.value = value;
                            return () -> value;
                        });
    }
}

本Try類來自於Vavr庫,函式程式設計API的Java語言和Resilience4J唯一的依賴。它需要兩個lambdas:

  • 第一個接受一個 Throwable並返回一個返回結果的函式
  • 第二個接受該值,並返回一個返回結果的函式

請注意,兩者都是惰性的:它們不直接返回結果,而是返回Supplier結果。使用此自定義快取,現在可以裝飾Circuit Breaker呼叫,以便在電路開啟時返回快取值:

var cb = CircuitBreaker.ofDefaults("circuit-breaker");
var cache = new Cache<Long>();
var cbDecorated = CircuitBreaker.decorateSupplier(cb, new Command()::run);
var cacheDecorated = Cache.decorateSupplier(cache, cbDecorated);
cacheDecorated.get();

 

相關文章