Java EE的斷路器API設計

banq發表於2018-12-12

如何使用Java EE API,MicroProfile或某些Java EE擴充套件實現不同的彈性方法,例如斷路器,隔板或背壓?此外,企業Java彈性方法如何與Kubernetes和Istio等新的雲原生技術一起發揮作用?

定義彈性
首先,我們需要弄清楚應用程式彈性的含義。
應用程式應保證以穩定,負責任的方式執行功能,而不會導致應用程式無法恢復崩潰。因此,應用程式應該能夠容忍並從執行期間的小錯誤中恢復,尤其是在不影響其他無關功能的情況下。它還意味著應該考慮系統的整體執行狀況,這要求所有應用程式不要盲目地使當前可能負載的應用程式過載。
此外,有一種說法是你所做的是保守而你所接受的是自由。同樣,企業應用程式在拒絕技術上易於理解但不完全遵循規範的訊息時不應過於嚴格。

超時
為了避免死鎖情況,建立同步通訊的超時至關重要。超時是活動之間的關係,當應用程式繼續能夠處理傳入請求時,以及當我們拒絕處理可能很快成功完成的請求時,進行權衡。
雖然超時對於各種同步通訊至關重要,但我們將專注於HTTP呼叫。

@ApplicationScoped
public class MakerBot {

    private Client client;
    private WebTarget target;

    @PostConstruct
    private void initClient() {
        client = ClientBuilder.newBuilder()
                .connectTimeout(2, TimeUnit.SECONDS)
                .readTimeout(4, TimeUnit.SECONDS)
                .build();
        target = client
                .target("http://maker-bot:9080/maker-bot/resources/jobs");
    }

    public void printInstrument(InstrumentType type) {
        JsonObject requestBody = createRequestBody(type);
        Response response = sendRequest(requestBody);
        validateResponse(response);
    }

    // ...
}

從JAX-RS 2.1版開始,ClientBuilder透過connectTimeout()和readTimeout()方法支援標準化的超時配置。根據所使用的HTTP實現,不指定超時值可能最終會無限制地阻塞呼叫。
當然,實際的超時值取決於實際的應用程式和環境設定。

斷路器
與電氣工程中的斷路器類似,軟體中的斷路器檢測故障或響應緩慢,並透過抑制註定要失敗的動作來防止進一步損壞。我們可以指定斷路器應該根據先前的執行中斷某些功能的執行情況。
有多個第三方庫可用於實現斷路器,包括MicroProfile Fault Tolerance專案,該專案與Java EE非常好地整合,並得到少數應用程式容器供應商的支援。以下宣告該類的printInstrument方法MakerBot由具有預設行為的MicroProfile斷路器保護:

@CircuitBreaker
public void printInstrument(InstrumentType type) {
    JsonObject requestBody = createRequestBody(type);
    Response response = sendRequest(requestBody);
    validateResponse(response);
}

如果@CircuitBreaker方法執行失敗,那麼註釋將導致方法執行被中斷 - 也就是說,如果它在20次呼叫中超過50%的時間丟擲異常 - 預設情況下。電路開啟後,執行將預設中斷至少5秒。可以使用註釋覆蓋這些預設值。
可以使用@Fallback註釋定義回退行為,註釋分別引用回退處理程式類或方法。

重試
重試背後的動機是透過立即重試失敗的操作來消除暫時的失敗。此重試對呼叫功能透明地發生。
使用MicroProfile Fault Tolerance實現技術動機重試很簡單。@Retry如果發生異常,註釋將導致方法呼叫最多重新執行三次。我們可以使用註釋值進一步配置行為,例如延遲時間或異常型別。
與斷路器類似,@Fallback如果在最大重試次數後呼叫仍然失敗,也可以定義行為。

隔板
與船上的隔間類似,隔板旨在將軟體功能劃分為可單獨失效的部分,而不會導致整個應用程式無響應。它們可以防止錯誤進一步級聯,同時應用程式的其餘部分保持正常執行。
在企業Java中,透過定義多個池(例如資料庫連線池或執行緒池)來應用Bulkhead模式。對於多個執行緒池,如果另一個執行緒池當前用盡,我們可以確保應用程式的特定部分不受影響。
但是,企業Java應用程式不應該啟動或管理自己的執行緒; 相反,他們必須使用平臺功能來提供託管執行緒。為此,Java EE附帶一個ManagedExecutorService提供容器管理執行緒的執行緒,通常基於單個執行緒池。
由Java EE專家Adam Bien提供的Porcupine庫支援進一步定義可以單獨配置的容器管理執行緒池。以下顯示了兩個專用ExecutorService的檢索和建立工具的定義和用法:

@Inject
@Dedicated("instruments-read")
ExecutorService readExecutor;

@Inject
@Dedicated("instruments-write")
ExecutorService writeExecutor;


// usage within method body ...
CompletableFuture.supplyAsync(() -> instrumentCraftShop.getInstruments(), readExecutor);


// usage within method body ...
CompletableFuture.runAsync(() -> instrumentCraftShop.craftInstrument(instrument), writeExecutor)


通常,這些執行程式服務可以在我們的整個應用程式中使用。但是,在使用HTTP資源時需要考慮另一種情況。
應用程式伺服器通常使用單個執行緒池來處理傳入請求的HTTP請求執行緒。當然,單個請求執行緒池使我們很難在我們的應用程式中構建多個隔板。
因此,我們可以使用非同步JAX-RS資源來立即管理對專用執行程式服務的傳入請求的處理:

@Path("instruments")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class InstrumentsResource {

    @Inject
    InstrumentCraftShop instrumentCraftShop;

    @Inject
    @Dedicated("instruments-read")
    ExecutorService readExecutor;

    @Inject
    @Dedicated("instruments-write")
    ExecutorService writeExecutor;

    @GET
    public CompletionStage<List<Instrument>> getInstruments() {
        return CompletableFuture
                .supplyAsync(() -> instrumentCraftShop.getInstruments(), readExecutor);
    }

    @POST
    public CompletionStage<Response> createInstrument(@Valid @NotNull Instrument instrument) {
        return CompletableFuture.runAsync(
                () -> instrumentCraftShop.craftInstrument(instrument), writeExecutor)
                .thenApply(c -> Response.noContent().build())
                .exceptionally(e -> Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                        .header("X-Error", e.getMessage())
                        .build());
    }
}

檢索和建立工具的傳入請求將傳遞到單獨的執行緒池。從JAX-RS 2.1開始,返回一個CompletionStage或相容的型別就足以將JAX-RS資源宣告為非同步。非同步處理完成後,請求將被暫停並恢復。
如果分別用於檢索或建立工具的這兩個功能中的一個將被過載並且池中的執行緒用完,則另一個將不受此影響。共享請求執行緒池不太可能用完執行緒,因為請求的處理會立即傳遞給其他受管執行緒。
我們使用Porcupine庫而不是MicroProfile Fault Tolerance的Bulkhead功能的原因是:前者使我們能夠直接訪問和控制我們與非同步JAX-RS資源連線的託管執行程式服務。

背壓
負載較重的應用程式可以透過向客戶通知其當前狀態來應用背壓。這可以透過多種方式實現,例如,透過向響應新增後設資料或者透過返回失敗響應來更加徹底地新增後設資料。
如果我們認為應用程式保持響應更為重要,尤其是它能夠在其服務級別協議(SLA)內響應而不是延遲響應,那麼我們將希望實現背壓。關於滿足整個系統的SLA,立即響應錯誤以使客戶端可以呼叫不同的應用程式或例項,而不是消耗所有SLA時間並且仍然無法正常執行可能更有幫助處理請求。
為了指示我們的執行程式服務在所有執行緒都忙的情況下立即拒絕超過執行程式等待佇列的呼叫,我們需要進一步配置行為。ExecutorConfigurator是Porcupine使用的託管bean,我們可以使用CDI專門使用它:

@Specializes
public class CustomExecutorConfigurator extends ExecutorConfigurator {

    @Override
    public ExecutorConfiguration forPipeline(String name) {
        if ("instruments-read".equals(name))
            return new ExecutorConfiguration.Builder()
                    .abortPolicy()
                    .queueCapacity(4)
                    .build();

        return new ExecutorConfiguration.Builder()
                .abortPolicy()
                .build();
    }
}


覆蓋方法forPipeline用於為限定名稱構造執行程式服務。該abortPolicy呼叫將指示底層的執行緒池立即拒絕與超過資源的新的呼叫RejectedExecutionException。這是我們的目的所期望的行為。
為了通知客戶端我們的應用程式不可用,我們將此異常對映到HTTP 503響應:

@Provider
public class RejectedExecutionHandler implements ExceptionMapper<RejectedExecutionException> {

    @Override
    public Response toResponse(RejectedExecutionException exception) {
        return Response.status(Response.Status.SERVICE_UNAVAILABLE).build();
    }
}

如果客戶端現在呼叫使用當前處於水下的隔板的功能,則它們會立即獲得HTTP 503響應,並且仍然可以在SLA時間內連線到另一個系統。
但是,我們應該配置哪些佇列大小以滿足我們的SLA時間,而不是過分拒絕將要及時處理的請求?
如果我們在排隊論中看一下Little's定律,我們會看到平均響應時間是系統中(請求數)的平均數除以平均吞吐量。如果我們進一步考慮到我們可能在系統中有一個多處理單元,我們可以得出最大延遲,如Martin Thompson的文章所述:最大延遲=(事務時間/執行緒數)*(佇列長度),給定非零佇列長度並且給定最大延遲大於或等於事務時間。轉換該公式使我們得到:佇列長度=最大延遲/(事務時間/執行緒數)。
例如,假設我們要保證SLA時間為200毫秒(最大延遲),測量的平均事務時間為20毫秒,並且有四個可用執行緒。應用此公式使我們的佇列長度為50(因為200 /(20/5)等於50)。如果系統中當前的執行緒數超過此佇列大小,則會立即拒絕新請求,而不是在200毫秒的等待時間之後。這可作為如何ExecutorService在我們的應用程式中配置各個定義的指南。

結論​​​​​​​
在生產中執行企業應用程式時,需要考慮一些事項。我們希望確保我們的應用程式能夠在艱難的生產生命中生存,而不會出現級聯故障,系統過載或活動問題。
透過使用普通企業Java及其擴充套件,可以實現彈性問題,例如超時,重試,斷路器,隔板和背壓,作為我們應用程式的一部分。Java EE API已經解決了一些問題; 其餘的可以透過諸如Porcupine和MicroProfile Fault Tolerance之類的擴充套件來涵蓋。​​​​​​​

相關文章