穩定模式在RESTful架構中的應用
分布式系統中保持網絡穩定的五種方式
- 重試模式
- 超時模式
- 斷路器模式
- 握手模式
- 隔離壁模式 </ol>
- 網絡是可靠的
- 零延遲
- 無限寬帶
- 網絡是安全
- 不變的拓撲結構
- 只有一個管理員
- 傳輸成本為零
- 同質化的網絡 </ol>
- 連接超時 指建立連接或者錯誤發生前消耗的最長時間。
- Socket超時表示,連接建立以后兩個連續數據包抵達客戶端之間非活躍的最大周期。 </ol>
- closed狀態的調用被執行,事務度量被存儲,這些度量是實現一個健康策略所必備的。
- 倘若系統健康狀況變差,斷路器就處在open狀態。此種狀態下,所有調用會被立即回退并且不會產生新的調用。open狀態的目的是給服務器端回復和處理問題的時間。
- 一旦斷路器進入一個open狀態,超時計時器開始計時。如果計時器超時,斷路器切換到half-open狀態。在half-open狀態調用間歇性執行以確定問題是否已解決。如果解決,狀態切換回closed狀態。 </ol>
- 文本代碼托管在GitHub。
- 通過REST for Java developers系列了解更多REST架構風格和RESTful編程( Brian Sletten, JavaWorld)。
- Michael T. Nygard在 Release It!: Design and Deploy Production-Ready Software (Pragmatic Programmers, March 2007)書中介紹了系統穩定性中的模式和反模式。
- 在這個視頻教程中,Michael Nygard介紹了分布式、高可用性系統的模式和范模式(InfoQ, QCon 2009)。
- L. Peter Deutsch和其他Sun公司的員工在 Fallacies of Distributed Computing中記錄了分布式系統中8種錯誤的假設。
- 了解更多 Retry pattern(Microsoft Developer Network)。
- Gregor Roth為JavaWorld編寫了異步HTTP在開發高性能HTTP代理和非阻塞式HTTP客戶端中的角色(March 2008)。 </ul> 原文鏈接: javaworld 翻譯: ImportNew.com - 喬永琪
倘若分布式系統的可靠性由一個極弱的控件決定,那么一個很小的內部功能都可能導致整個系統不穩定。了解穩定模式如何預知分布式網絡熱點,進而了解應用于Jersey和RESTEasy RESTFUL事務中的五種模式。
要實現高可用、高可靠分布式系統,需要預測一些可不預測的狀況。假設你運行規模更大的軟件系統,產品發布之后遲早會面臨的各種突發狀況,一般你會發現兩個重要的漏洞。第一個和功能相關,比如計算錯誤、或者處理和解釋數據的錯誤。這類漏洞很容易產生,通常產品上線前這些bug都會被檢測到并得到處理。
第二類漏洞更具挑戰性,只有在特定的底層框架下這些漏洞才會顯現。因此這些漏洞很難檢測和重現,通常情況下不易在測試中發現。相反的,在產品上線運行時幾乎總會遇到這些漏洞。更好的測試以及軟件質量控制可以提高漏洞移除的機率,然而這并不能確保你的代碼沒有漏洞。
最壞的情況下,代碼中的漏洞會觸發系統級聯錯誤,進而導致系統致命的失敗。特別是在分布式系統中,其服務位于其它服務與客戶端之間。
穩定分布式操作系統的網絡行為
系統致命失敗熱點首要是網絡通信。不幸的是,分布式系統的架構師和設計者常常以錯誤的方式假設網絡行為。二十年前,L. Peter Deutsch和其他Sun公司同事就撰文分布式錯誤,直到今天依然普遍存在。
今天的多數開發人員依賴RESTFUL系統解決分布式系統網絡通信帶來的諸多挑戰。REST最重要的特點是,它不會隱藏存在高層的RPC樁(Stub)后面的網絡通信限制。但RESTful接口和終端不能單獨確保系統內在的穩定性,還需要做一些額外的工作。
本文介紹了四種穩定模式來解決分布式系統中常見的失敗。本文關注REStful終端,不過這些模式也能應用于其他通信終端。本文應用的模式來自Michael Nygard的書,Release It! Design and Deploy Production-Ready Software。示例代碼和demo是自己的。
下載本文源代碼,Gregor Roth在2014年10月JavaWorld大會上關于穩定模式在RESTful架構中的應用的源代碼。
應用穩定模式
穩定模式(Stability Pattern)用來提升分布式系統的彈性,利用我們熟知的網絡行為熱點去保護系統免遭失敗。本文所引用的模式用來保護分布式系統在網絡通信中常見的失敗,網絡通信中的集成點比如Socket、遠程方法調用、數據庫調用(數據庫驅動隱藏了遠程調用)是第一個系統穩定風險。用這些模式能避免一個分布式系統僅僅因為系統的一部分失敗而宕機。
網店demo
在線電子支付系統通常沒有新的客戶數據。相反,這些系統常常基于新用戶住址信息為外部在線信用評分檢查。基于用戶信用得分,網店demo應用決定采用哪種支付手段(信用卡、PayPal賬戶、預付款或者發票)。
這個demo解決了一個關鍵場景:如果信用檢測失敗會發生什么?訂單應該被拒絕么?多數情況下,支付系統回退接收一個更加可靠的支付方式。處理這種外部控件失敗即是一種技術也是一種業務決策,它需要在失去訂單和一個爽約支付可能之間做出權衡。
圖1顯示了網店系統藍圖
網店應用采用內部支付服務決定選用何種支付方式,即支付服務提供針對某個用戶支付信息以及采用何種支付方式。本例中服務采用RESTful方式實現,意味著諸如GET或者POST的HTTP方法會被顯示調用,進而由URI對服務資源進行處理。此方法在JAX-RS 2.0特殊注解所在代碼樣品中同樣有體現。JAX-RS 2.0文檔實現了REST與Java的綁定,并作為Java企業版本平臺。
列表1、采用何種支付手段
@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中,我用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定義中設置超時。
列表2、在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。
列表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的重試模式不同,斷路器模式可以解決持續化錯誤。
利用斷路器存儲客戶端資源中那些注定失敗的調用,如同存儲服務器端資源一樣。若服務器處在錯誤狀態,比如過高的負載狀態,多數情形下,服務器添加額外的負載就不太明智。
一個斷路器可以裝飾并且檢測了一個受保護的功能調用。根據當前的狀態決定調用時被執行還是回退。通常情況下,一個斷路器實現三種類型的狀態:open、half-open以及closed:
客戶端斷路器
圖3展示了如何利用JAX-RS過濾器接口實現一個斷路器,注意有好幾處攔截請求的地方,比如HttpClient底層一個攔截器接口同樣適用整合一個斷路器。
在客戶端調用JAX-RS客戶端接口register方法,設置一個斷路器過濾器:
client.register(new ClientCircutBreakerFilter());
斷路器過濾器實現了前置處理(pre-execution)和后置處理(post-execution)方法。在前置處理方法中,系統會檢測請求執行是否允許。一個目標主機會用一個專門的斷路器實例對應,避免產生副作用。如果調用允許,HTTP事務就會被記錄在度量中。存在于后執行方法中事務度量對象分派結果給事務被關閉。一個5xx狀態響應會被處理為成錯誤。
列表4、斷路器模式中的前置和后置執行方法
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比率。
列表5、健康策略邏輯
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狀態。在這個簡單的例子中百分之二的調用會檢測服務器端是否處在正常狀態。
列表6、健康響應策略
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() 都無法調用。
列表7、服務端斷路器的過濾器
@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如下所示:
列表8、服務端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請求頭信息,這個跟一個恰當的流控制響應頭信息一樣。
列表9、握手過濾器的流程控制頭信息
@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新請求引發的系統失敗,都應該被限制在移動通道里面。
每應用資源(Resources-per-application)。如圖5展示的那樣,一個排他的隔離壁實現方式,比如,支付服務不僅利用信用評分服務,同時也利用匯率服務。如果這兩種方式放在同一個容器中,不好的信用評分服務行為可能拆分匯率服務。從隔離壁的角度看,將每個應用放在各自的容器中,這樣可以保護彼此不受干擾。
此種方式不好的地方就是一個既定資源池添加海量資源開銷很大。不過虛擬化可以減少這種開銷。
每操作資源(Resources-per-operation)是一種更加細粒度方式,分派單個系統資源給運算。比如,支付服務中的getAcceptedPaymentMethods() 運算運行有漏洞,getPayments() 運算依舊能處理。Netflix的Hystrix框架是支持這種細粒度隔離壁典型系統。
每終端資源(Resources-per-endpoint)為既定客戶終端管理資源,比如在電子支付系統中單個客戶端實例對應單個服務終端,如圖6所示。
在本例中Apache HttpClient缺省狀態最大可以利用20個網絡連接,單個HTTP事務消費一個連接。利用經典的阻塞方式,最大連接數等于HttpClient 實例可以利用的最大線程數。下面的例子中,客戶端可以消費30個連接數最多可利用30個線程。
列表10、隔離壁在系統終端控制資源應用
// ... 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() 就會被處理,這個函數要么異步調用信用評級服務,要么返回基于用戶先前支付記錄的評分,接著分數會映射到所支持的支付方法上。
列表11、獲得異步支付方法
@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容器這樣的服務器處理需要管理者們監控,管理者追蹤容器是否健康,一旦處理臨近崩潰需要重啟;很多例證表明,重啟服務比讓它處于活躍狀態更有益,畢竟一個錯誤幾乎沒有響應的服務節點比一個移除的死節點更要命。
更多資源
譯文鏈接: http://www.importnew.com/16027.html