穩定模式在RESTful架構中的應用

分佈式系統中保持網絡穩定的五種方式

  1. 重試模式
  2. 超時模式
  3. 斷路器模式
  4. 握手模式
  5. 隔離壁模式

假若分佈式系統的可靠性由一個極弱的控件決定,那麼一個很小的內部功能均可能致使整個系統不穩定。瞭解穩定模式如何預知分佈式網絡熱點,進而瞭解應用於Jersey和RESTEasy RESTFUL事務中的五種模式。html

要實現高可用、高可靠分佈式系統,須要預測一些可不預測的情況。假設你運行規模更大的軟件系統,產品發佈以後早晚會面臨的各類突發情況,通常你會發現兩個重要的漏洞。第一個和功能相關,好比計算錯誤、或者處理和解釋數據的錯誤。這類漏洞很容易產生,一般產品上線前這些bug都會被檢測到並獲得處理。java

第二類漏洞更具挑戰性,只有在特定的底層框架下這些漏洞纔會顯現。所以這些漏洞很難檢測和重現,一般狀況下不易在測試中發現。相反的,在產品上線運行時幾乎總會遇到這些漏洞。更好的測試以及軟件質量控制能夠提升漏洞移除的機率,然而這並不能確保你的代碼沒有漏洞。git

最壞的狀況下,代碼中的漏洞會觸發系統級聯錯誤,進而致使系統致命的失敗。特別是在分佈式系統中,其服務位於其它服務與客戶端之間。github

穩定分佈式操做系統的網絡行爲

系統致命失敗熱點首要是網絡通訊。不幸的是,分佈式系統的架構師和設計者經常以錯誤的方式假設網絡行爲。二十年前,L. Peter Deutsch和其餘Sun公司同事就撰文分佈式錯誤,直到今天依然廣泛存在。數據庫

  1. 網絡是可靠的
  2. 零延遲
  3. 無限寬帶
  4. 網絡是安全
  5. 不變的拓撲結構
  6. 只有一個管理員
  7. 傳輸成本爲零
  8. 同質化的網絡

今天的多數開發人員依賴RESTFUL系統解決分佈式系統網絡通訊帶來的諸多挑戰。REST最重要的特色是,它不會隱藏存在高層的RPC樁(Stub)後面的網絡通訊限制。但RESTful接口和終端不能單獨確保系統內在的穩定性,還須要作一些額外的工做。apache

本文介紹了四種穩定模式來解決分佈式系統中常見的失敗。本文關注REStful終端,不過這些模式也能應用於其餘通訊終端。本文應用的模式來自Michael Nygard的書,Release It! Design and Deploy Production-Ready Software。示例代碼和demo是本身的。編程

下載本文源代碼Gregor Roth在2014年10月JavaWorld大會上關於穩定模式在RESTful架構中的應用的源代碼。安全

應用穩定模式

穩定模式(Stability Pattern)用來提高分佈式系統的彈性,利用咱們熟知的網絡行爲熱點去保護系統免遭失敗。本文所引用的模式用來保護分佈式系統在網絡通訊中常見的失敗,網絡通訊中的集成點好比Socket、遠程方法調用、數據庫調用(數據庫驅動隱藏了遠程調用)是第一個系統穩定風險。用這些模式能避免一個分佈式系統僅僅由於系統的一部分失敗而宕機。服務器

網店demo

在線電子支付系統一般沒有新的客戶數據。相反,這些系統經常基於新用戶住址信息爲外部在線信用評分檢查。基於用戶信用得分,網店demo應用決定採用哪一種支付手段(信用卡、PayPal帳戶、預付款或者發票)。restful

這個demo解決了一個關鍵場景:若是信用檢測失敗會發生什麼?訂單應該被拒絕麼?多數狀況下,支付系統回退接收一個更加可靠的支付方式。處理這種外部控件失敗便是一種技術也是一種業務決策,它須要在失去訂單和一個爽約支付可能之間作出權衡。

圖1顯示了網店系統藍圖

圖1 電子支付系統流程圖

網店應用採用內部支付服務決定選用何種支付方式,即支付服務提供針對某個用戶支付信息以及採用何種支付方式。本例中服務採用RESTful方式實現,意味着諸如GET或者POST的HTTP方法會被顯示調用,進而由URI對服務資源進行處理。此方法在JAX-RS 2.0特殊註解所在代碼樣品中一樣有體現。JAX-RS 2.0文檔實現了REST與Java的綁定,並做爲Java企業版本平臺。

列表一、採用何種支付手段
 
@Singleton
@Path("/")
public class PaymentService {
    // ...
    private final PaymentDao paymentDao;
    private final URI creditScoreURI;
    private final static Function<Score, ImmutableSet<PaymentMethod>> SCORE_TO_PAYMENTMETHOD = score ->  {
                            switch (score) {
                            case Score.POSITIVE:
                                return ImmutableSet.of(CREDITCARD, PAYPAL, PREPAYMENT, INVOCE);
                            case Score.NEGATIVE:
                                return ImmutableSet.of(PREPAYMENT);
                            default:
                                return  ImmutableSet.of(CREDITCARD, PAYPAL, PREPAYMENT);
                            }
    };
    @Path("paymentmethods")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public ImmutableSet<PaymentMethod> getPaymentMethods(@QueryParam("addr") String address) {
        Score score = Score.NEUTRAL;
        try {
            ImmutableList<Payment> payments = paymentDao.getPayments(address, 50);
            score = payments.isEmpty()
                         ? restClient.target(creditScoreURI).queryParam("addr", address).request().get(Score.class)
                         : (payments.stream().filter(payment -> payment.isDelayed()).count() >= 1) ? Score.NEGATIVE : Score.POSITIVE;
        } catch (RuntimeException rt) {
            LOG.fine("error occurred by calculating score. Fallback to " + score + " " + rt.toString());
        }
        return SCORE_TO_PAYMENTMETHOD.apply(score);
    }
    @Path("payments")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public ImmutableList<Payment> getPayments(@QueryParam("userid") String userid) {
       // ...
    }
    // ...
}

列表1中 getPaymentMethods() 方法綁定了URI路徑片斷paymentmethods,這樣就會獲得諸如 http://myserver/paymentservice/paymentmethods的URI。@GET註解定義了註解方法,若一個HTTP GET請求過來,就會被這個URI所接收。網店應用調用 getPaymentMethods(),藉助用戶過往的信用歷史,爲用戶的可靠性打分。假若沒有歷史數據,一個信用評級服務會被調用。對於本例集成點的異常,系統採用getPaymentMethods() 來降級。即使這樣會從一個未知或授信度低客戶那裏接收到一個更不穩定的支付方法。若是內部的 paymentDao 查詢或者 creditScoreURI 查詢失敗,getPaymentMethods() 會返回缺省的支付方式。

重試模式

Apache HttpClient以及其它的網絡客戶端實現了一些穩定特性。好比,客戶端在某些場景內部反覆執行。這個策略有助於處理瞬時網絡錯誤,好比斷掉鏈接,或者服務器宕機。但重試無助於解決永久性錯誤,這會浪費客戶端和服務器雙方的資源和時間。

如今來看如何應用四種經常使用穩定模式解決存在外部信用評分組件中的不穩定錯誤。

使用超時模式

一種簡單卻極其有效的穩定模式就是利用合適的超時,Socket編程是一種基礎技術,使得軟件能夠在TCP/IP網絡上通訊。本質上說,Socket API定義了兩種超時類型:

  1. 鏈接超時 指創建鏈接或者錯誤發生前消耗的最長時間。
  2. Socket超時表示,鏈接創建之後兩個連續數據包抵達客戶端之間非活躍的最大週期。

列表1中,我用JAX-RS 2.0客戶端接口調用信用評分服務。但怎樣的超時週期纔算合理呢?這個取決於你的JAX-RS供應商。好比,眼下的Jersey版本採用HttpURLConnection。但缺省的Jersey設定鏈接超時或者Socket超時爲0毫秒,即超時是無限的,假若你不認爲這樣設置有問題,請三思。

考慮到JAX-RS客戶端會在一個服務器/servlet引擎中獲得處理,利用一個工做線程池處理進來的HTTP請求。若咱們利用經典的阻塞請求處理方法,列表1中的 getPaymentMethods() 會被線程池中一個排他的線程調用。在整個請求處理過程當中,一個既定線程與請求處理綁定。若是內在的信用評分服務(由thecreditScoreURI提供地址)調用相應很慢,全部工做池中的線程最終會被掛起。接着,支付服務其它方法,好比getPayments() 會被調用。由於全部線程都在等待信用評分響應,這個請求沒有被處理。最糟糕的多是,一個很差的信用評分服務行爲可能拖累整個支付服務功能。

實現超時:線程池 vs 鏈接池

合理的超時是可用性的基礎。但JAX-RS 2.0客戶端接口並無定一個設置超時的接口,因此你不得不利用供應商提供的接口。下面的代碼,我爲Jersey設置了客戶屬性。

 
restClient = ClientBuilder.newClient();
        restClient.property(ClientProperties.CONNECT_TIMEOUT, 2000); // jersey specific
        restClient.property(ClientProperties.READ_TIMEOUT,    2000); // jersey specific

與Jersey不一樣,RESTEasy採用Apache HttpClient,比HttpURLConnection更加有效,Apache HttpClient支持鏈接池。鏈接池確保鏈接在處理完了一個HTTP事務以後,能夠用來處理其它HTTP事務,假設該鏈接能夠被看做持久鏈接。這個方式能減小創建新TCP/IP鏈接的開銷,這一點很重要。

一個高負載系統內,單個HTTP客戶端實例每秒處理成千上萬的HTTP傳出事務也並不罕見。

爲了在Jersey中可以利用Apache HttpClient,如列表2所示,你須要設置ApacheConnectorProvider。注意在request-config定義中設置超時。

列表二、在Jersey中使用Apache HttpClient
 
ClientConfig clientConfig = new ClientConfig();                          // jersey specific
    ClientConfig.connectorProvider(new ApacheConnectorProvider());           // jersey specific
    RequestConfig reqConfig = RequestConfig.custom()                         // apache HttpClient specific
                                           .setConnectTimeout(2000)
                                           .setSocketTimeout(2000)
                                           .setConnectionRequestTimeout(200)
                                           .build();
    clientConfig.property(ApacheClientProperties.REQUEST_CONFIG, reqConfig); // jersey specific
    restClient = ClientBuilder.newClient(clientConfig);

注意,鏈接池特定鏈接請求超時同在上面的例子也有設置。鏈接請求超時表示,從發起一個鏈接請求到在HttpClient內在鏈接池管理返回一個請求鏈接花費的時間。缺省狀態不限制超時,意味着鏈接請求調用時會一直阻塞直到鏈接變爲可用,效果和無限鏈接、Socket超時同樣。

利用Jersey的另外一種方式,你能夠間接經過RESTEasy設置鏈接請求超時,參見列表3。

列表三、在RESTEasy中設置鏈接超時
 
RequestConfig reqConfig = RequestConfig.custom()   // apache HttpClient specific
                                           .setConnectTimeout(2000)
                                           .setSocketTimeout(2000)
                                           .setConnectionRequestTimeout(200)
                                           .build();
    CloseableHttpClient httpClient = HttpClientBuilder.create()
                                                      .setDefaultRequestConfig(reqConfig)
                                                      .build();
    Client restClient = new ResteasyClientBuilder().httpEngine(new ApacheHttpClient4Engine(httpClient, true)).build();  // RESTEasy specific

我所展現的超時模式實現都是基於RESTEasy和Jersey,這兩種RESTful網絡服務框架都實現了JAX-RS 2.0。同時,我也展現了兩種超時設置方法,JAX-RS 2.0供應商利用標準線程池或者鏈接池管理外部請求。

斷路器模式

與超時限制系統資源消費不一樣,斷路器模式(Circuit Breaker)更加積極主動。斷路器診斷失敗並防止應用嘗試執行註定失敗的活動。與HttpClient的重試模式不一樣,斷路器模式能夠解決持續化錯誤。

利用斷路器存儲客戶端資源中那些註定失敗的調用,如同存儲服務器端資源同樣。若服務器處在錯誤狀態,好比太高的負載狀態,多數情形下,服務器添加額外的負載就不太明智。

圖2 斷路器模式狀態圖

一個斷路器能夠裝飾而且檢測了一個受保護的功能調用。根據當前的狀態決定調用時被執行仍是回退。一般狀況下,一個斷路器實現三種類型的狀態:open、half-open以及closed:

  1. closed狀態的調用被執行,事務度量被存儲,這些度量是實現一個健康策略所必備的。
  2. 假若系統健康情況變差,斷路器就處在open狀態。此種狀態下,全部調用會被當即回退而且不會產生新的調用。open狀態的目的是給服務器端回覆和處理問題的時間。
  3. 一旦斷路器進入一個open狀態,超時計時器開始計時。若是計時器超時,斷路器切換到half-open狀態。在half-open狀態調用間歇性執行以肯定問題是否已解決。若是解決,狀態切換回closed狀態。

客戶端斷路器

圖3展現瞭如何利用JAX-RS過濾器接口實現一個斷路器,注意有好幾處攔截請求的地方,好比HttpClient底層一個攔截器接口一樣適用整合一個斷路器。

圖3 利用JAX-RS過濾器接口實現斷路器

在客戶端調用JAX-RS客戶端接口register方法,設置一個斷路器過濾器:

1
client.register( new ClientCircutBreakerFilter());

斷路器過濾器實現了前置處理(pre-execution)和後置處理(post-execution)方法。在前置處理方法中,系統會檢測請求執行是否容許。一個目標主機會用一個專門的斷路器實例對應,避免產生反作用。若是調用容許,HTTP事務就會被記錄在度量中。存在於後執行方法中事務度量對象分派結果給事務被關閉。一個5xx狀態響應會被處理爲成錯誤。

列表四、斷路器模式中的前置和後置執行方法
 
public class ClientCircutBreakerFilter implements ClientRequestFilter, ClientResponseFilter  {
    // ..
    @Override
    public void filter(ClientRequestContext requestContext) throws IOException, CircuitOpenedException {
        String scope = requestContext.getUri().getHost();
        if (!circuitBreakerRegistry.get(scope).isRequestAllowed()) {
            throw new CircuitOpenedException("circuit is open");
        }
        Transaction transaction = metricsRegistry.transactions(scope).openTransaction();
        requestContext.setProperty(TRANSACTION, transaction);
    }
    @Override
    public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
        boolean isFailed = (responseContext.getStatus() >= 500);
        Transaction.close(requestContext.getProperty(TRANSACTION), isFailed);
    }
}

系統健康實現策略

基於列表4事務記錄,一個斷路器系統健康策略實現可以獲得 totalRate/errorRate比率的度量。特別的是,邏輯健康一樣須要考慮異常行爲,好比在請求率極低的時候,健康策略能夠忽視 totalRate/errorRate比率。

列表五、健康策略邏輯
public class ErrorRateBasedHealthPolicy implements HealthPolicy  {
    // ...
    @Override
    public boolean isHealthy(String scope) {
        Transactions recorded =  metricsRegistry.transactions(scope).ofLast(Duration.ofMinutes(60));
        return ! ((recorded.size() > thresholdMinReqPerMin) &&      // check threshold reached?
                  (recorded.failed().size() == recorded.size()) &&  // every call failed?
                  (...                                        ));   // client connection pool limit almost reached?
    }
}
 

假若健康策略返回值爲負,斷路器會進入open、half-open狀態。在這個簡單的例子中百分之二的調用會檢測服務器端是否處在正常狀態。

列表六、健康響應策略
public class CircuitBreaker {
    private final AtomicReference<CircuitBreakerState> state = new AtomicReference<>(new ClosedState());
    private final String scope;
    // ..
    public boolean isRequestAllowed() {
        return state.get().isRequestAllowed();
    }
    private final class ClosedState implements CircuitBreakerState {
        @Override
        public boolean isRequestAllowed() {
            return (policy.isHealthy(scope)) ? true
                                             : changeState(new OpenState()).isRequestAllowed();
        }
    }
    private final class OpenState implements CircuitBreakerState {
        private final Instant exitDate = Instant.now().plus(openStateTimeout);
        @Override
        public boolean isRequestAllowed() {
            return (Instant.now().isAfter(exitDate)) ? changeState(new HalfOpenState()).isRequestAllowed()
                                                     : false;
        }
    }
    private final class HalfOpenState implements CircuitBreakerState {
        private double chance = 0.02;  // 2% will be passed through
        @Override
        public boolean isRequestAllowed() {
            return (policy.isHealthy(scope)) ? changeState(new ClosedState()).isRequestAllowed()
                                             : (random.nextDouble() <= chance);
        }
    }
    // ..
}
 

服務器端斷路器實現

斷路器也能夠在服務器端實現。服務器端過濾器範圍做爲目標運算取代目標主機。若目標運算處理出錯,調用會攜帶一個錯誤狀態當即回退。用服務端過濾器能夠避免某個錯誤運算消耗過多資源。

列表1的 getPaymentMethods() 方法實現中,信用評分服務會被 creditScoreURI 在內部調用。然而,一旦內部信用評級服務調用響應很慢(設置了不恰當的超時),信用評分服務調用可能會在後臺消耗掉Servlet引擎線程池中全部可用線程。這樣,即使 getPayments() 再也不查詢信用評分服務,其它支付服務的遠程運算,好比 getPayments() 都沒法調用。

列表七、服務端斷路器的過濾器
@Provider
public class ContainerCircutBreakerFilter implements ContainerRequestFilter, ContainerResponseFilter {
    //..
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        String scope = resourceInfo.getResourceClass().getName() + "#" + resourceInfo.getResourceClass().getName();
        if (!circuitBreakerRegistry.get(scope).isRequestAllowed()) {
            throw new CircuitOpenedException("circuit is open");
        }
        Transaction transaction = metricsRegistry.transactions(scope).openTransaction();
        requestContext.setProperty(TRANSACTION, transaction);
    }
    //..
}
 

注意,與客戶端的HealthPolicy不同,服務端例子採用OverloadBasedHealthPolicy。本例中,一旦全部工做池中線程都處於活躍狀態,超過百分之八十的線程被既定運算消費,而且超過最大慢速延遲閾值。接下來,運算會被認爲有誤。OverloadBasedHealthPolicy以下所示:

列表八、服務端OverloadBasedHealthPolicy
public class OverloadBasedHealthPolicy implements HealthPolicy  {
    private final Environment environment;
    //...
    @Override
    public boolean isHealthy(String scope) {
        // [1] all servlet container threads busy?
        Threadpool pool = environment.getThreadpoolUsage();
        if (pool.getCurrentThreadsBusy() >= pool.getMaxThreads()) {
            TransactionMetrics metrics = metricsRegistry.transactions(scope);
            // [2] more than 80% currently consumed by this operation?
            if (metrics.running().size() > (pool.getMaxThreads() * 0.8)) {
                // [3] is 50percentile higher than slow threshold?
                Duration current50percentile = metrics.ofLast(Duration.ofMinutes(3)).percentile(50);
                if (thresholdSlowTransaction.minus(current50percentile).isNegative()) {
                    return false;
                }
            }
        }
        return true;
    }
}
 

握手模式

斷路器模式要麼所有使用要麼徹底不用。根據記錄指標的質量和粒度,另外一種替代方法是提早檢測過量負載狀態。若檢測到一個即將發生的過載,客戶端可以被通知減小請求。在握手模式( Handshaking pattern)中,服務器會與客戶端通訊以便掌控自身工做負載。

握手模式經過一個負載均衡器爲服務器提供常規系統健康更新。負載均衡器利用諸如 http://myserver/paymentservice/~health 這樣的健康檢查URI決定那個服務器請求能夠轉發。出於安全的緣由,健康檢查頁一般不提供公共因特網接入,因此健康檢測的範圍僅僅侷限於公司內部通訊。

與pull方式不一樣,另外一種方式是添加一個流程控制頭信息(header)給響應以實現一個服務器push方式。這樣可以幫助服務器控制每一個客戶端的負載,固然須要對客戶端作甄別。我在列表9添加了一個私有的客戶端ID請求頭信息,這個跟一個恰當的流控制響應頭信息同樣。

列表九、握手過濾器的流程控制頭信息
@Provider
public class HandshakingFilter implements ContainerRequestFilter, ContainerResponseFilter {
    // ...
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        String clientId = requestContext.getHeaderString("X-Client-Id");
        requestContext.setProperty(TRANSACTION, metricsRegistry.transactions(clientId).openTransaction());
    }
    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        String clientId = requestContext.getHeaderString("X-Client-Id");
        if (flowController.isVeryHighRequestRate(clientId)) {
            responseContext.getHeaders().add("X-FlowControl-Request", "reduce");
        }
        Transaction.close(requestContext.getProperty(TRANSACTION), responseContext.getStatus() >= 500);
    }
}
 

本例中,一旦某個度量超出閾值,服務器就會通知客戶端減小請求。度量以客戶端ID形式被記錄下來,方便咱們爲某個特定客戶端做配備定額資源。一般客戶端會關閉諸如預獲取或者暗示功能直接減小請求響應,這些功能須要後臺請求。

隔離壁模式

在工業界隔離壁(Bulkhead)經常用來將船隻或者飛機分割成幾部件,一旦部件有裂縫部件能夠進行加封。同理,在軟件系統中利用隔離壁分割系統能夠應對系統的級聯錯誤。重要的是,隔離壁分派有限的資源給特定的客戶端、應用、運算和客戶終端等。

RESTful系統中的隔離壁

創建隔離壁或者系統分區方式有不少種,接下來我會一一展現。

每客戶資源(Resources-per-client)是一種爲特定客戶端創建單個集羣的隔離壁模式。好比圖4是一個新的移動網店應用版本示意圖。分割這些移動網店App能夠確保蜂擁而來的移動狀態請求不會對原始的網店應用產生副面影響。任何由移動App新請求引起的系統失敗,都應該被限制在移動通道里面。

圖4 移動網店應用

每應用資源(Resources-per-application)。如圖5展現的那樣,一個排他的隔離壁實現方式,好比,支付服務不只利用信用評分服務,同時也利用匯率服務。若是這兩種方式放在同一個容器中,很差的信用評分服務行爲可能拆分匯率服務。從隔離壁的角度看,將每一個應用放在各自的容器中,這樣能夠保護彼此不受干擾。

圖5 應用分區

此種方式很差的地方就是一個既定資源池添加海量資源開銷很大。不過虛擬化能夠減小這種開銷。

每操做資源(Resources-per-operation)是一種更加細粒度方式,分派單個系統資源給運算。好比,支付服務中的getAcceptedPaymentMethods() 運算運行有漏洞,getPayments() 運算依舊能處理。Netflix的Hystrix框架是支持這種細粒度隔離壁典型系統。

每終端資源(Resources-per-endpoint)爲既定客戶終端管理資源,好比在電子支付系統中單個客戶端實例對應單個服務終端,如圖6所示。

圖6 終端分區

在本例中Apache HttpClient缺省狀態最大能夠利用20個網絡鏈接,單個HTTP事務消費一個鏈接。利用經典的阻塞方式,最大鏈接數等於HttpClient 實例能夠利用的最大線程數。下面的例子中,客戶端能夠消費30個鏈接數最多可利用30個線程。

列表十、隔離壁在系統終端控制資源應用
// ...
    CloseableHttpClient httpClient = HttpClientBuilder.create()
                                                      .setMaxConnTotal(30)
                                                      .setMaxConnPerRoute(30)
                                                      .setDefaultRequestConfig(reqConfig)
                                                      .build();
    Client addrScoreClient = new ResteasyClientBuilder().httpEngine(new ApacheHttpClient4Engine(httpClient, true)).build();// RESTEasy specific
    CloseableHttpClient httpClient2 = HttpClientBuilder.create()
                                                       .setMaxConnTotal(30)
                                                       .setMaxConnPerRoute(30)
                                                       .setDefaultRequestConfig(reqConfig)
                                                       .build();
    Client exchangeRateClient = new ResteasyClientBuilder().httpEngine(new ApacheHttpClient4Engine(httpClient2, true)).build();// RESTEasy specific
 

另一種實現隔離壁模式的方式能夠利用不一樣的maxConnPerRoute和maxConnTotal值,maxConnPerRoute能夠限制特定主機的鏈接數。與兩個客戶端實例不一樣,單個客戶端實例會限制每一個目標主機的鏈接數。在本例中,你須要仔細觀察線程池,好比服務器容器利用300個工做線程,配置內部已用客戶端須要考慮最大空閒線程數。

Java8中的穩定模式:非阻塞異步調用

至今在多種模式和平常案例中,對線程的應用都是相當重要的一環,系統沒有響應大都是線程引發的。由一個枯竭線程池引起的系統嚴重失敗很是常見,在這個線程池中全部線程都被阻塞調用掛起,等待緩慢的響應。

Java8爲你們提供了另外一種支持lambda表達式的線程編程方式。lambda表達式經過更好的分佈式計算響應方式,讓Java非阻塞異步編程更容易。

響應式編程的核心原則就是事件驅動,即程序流由事件決定。與調用阻塞方法而且等到響應結果不一樣的是,事件驅動方式所定義的代碼響應諸如響應接受等事件。掛起等待響應的線程就再也不須要,程序中的handler代碼會對事件作出響應。

列表11中,thenCompose()、exceptionally()、thenApply()和whenComplete() 方法都是響應式的。方法參數都是Java8函數,只要諸如處理完成或者有錯誤等特定事件發生,這些參數就會被異步處理。

列表11展現了列表1中一個完全的異步、非阻塞的原始支付方法調用實現。本例中一旦請求被接收,數據庫就會以匿名的方式被調用,這就意味着 getPaymentMethodsAsync() 方法調用迅速返回,無需等待數據庫查詢響應。一旦數據庫響應請求,函數 thenCompose() 就會被處理,這個函數要麼異步調用信用評級服務,要麼返回基於用戶先前支付記錄的評分,接着分數會映射到所支持的支付方法上。

列表十一、得到異步支付方法
@Singleton
@Path("/")
public class AsyncPaymentService {
    // ...
    private final PaymentDao paymentDao;
    private final URI creditScoreURI;
    public AsyncPaymentService() {
        ClientConfig clientConfig = new ClientConfig();                    // jersey specific
        clientConfig.connectorProvider(new GrizzlyConnectorProvider());    // jersey specific
        // ...
        // use extended client (JAX-RS 2.0 client does not support CompletableFuture)
        restClient = Java8Client.newClient(ClientBuilder.newClient(clientConfig));
        // ...
        restClient.register(new ClientCircutBreakerFilter());
    }
    @Path("paymentmethods")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public void getPaymentMethodsAsync(@QueryParam("addr") String address, @Suspended AsyncResponse resp) {
        paymentDao.getPaymentsAsync(address, 50)      // returns a CompletableFuture<ImmutableList<Payment>>
           .thenCompose(pmts -> pmts.isEmpty()        // function will be processed if paymentDao result is received
              ? restClient.target(addrScoreURI).queryParam("addr", address).request().async().get(Score.class) // internal async http call
              : CompletableFuture.completedFuture((pmts.stream().filter(pmt -> pmt.isDelayed()).count() > 1) ? Score.NEGATIVE : Score.POSITIVE))
           .exceptionally(error -> Score.NEUTRAL)     // function will be processed if any error occurs
           .thenApply(SCORE_TO_PAYMENTMETHOD)         // function will be processed if score is determined and maps it to payment methods
           .whenComplete(ResultConsumer.write(resp)); // writes result/error into async response
    }
    // ...
}
 

注意,本實現中請求處理無需綁定在那個等待響應的線程上,是否意味着穩定模式無需這種響應模式?固然不是,咱們依舊要實現這些穩定模式。

非阻塞模式須要非阻塞代碼運行在調用路徑中,好比,PaymentDao的某個漏洞引發某些特定情形下的阻塞行爲,非阻塞協議就被打破,調用路徑所以變成阻塞式。並且,一個工做池線程隱式地綁定在某個調用路徑上,即便線程這會不是 鏈接/響應 管理等其餘資源的瓶頸,也有可能成爲下一個瓶頸。

最後結語

本文我所介紹的穩定模式描述了應對分佈式系統級聯失敗的最佳實踐。即使某個組件失敗,在這種降級的模式下,系統依舊作既定的運算。

本文例子用於RESTful終端的應用架構,一樣能夠應用於其它通訊終端。好比,不少系統包含數據庫客戶端,就不得不考慮這些。須要聲明的是,本文沒有闡述全部穩定相關模式。在一個產出很高的環境中,諸如Servlet容器這樣的服務器處理須要管理者們監控,管理者追蹤容器是否健康,一旦處理臨近崩潰須要重啓;不少例證代表,重啓服務比讓它處於活躍狀態更有益,畢竟一個錯誤幾乎沒有響應的服務節點比一個移除的死節點更要命。

更多資源

原文連接: javaworld 翻譯: ImportNew.com - 喬永琪
譯文連接: http://www.importnew.com/16027.html
[ 轉載請保留原文出處、譯者和譯文連接。]

關於做者: 喬永琪

相關文章
相關標籤/搜索