阅读更多

3顶
0踩

企业架构

转载新闻 稳定模式在RESTful架构中的应用

2015-06-03 13:21 by 副主编 mengyidan1988 评论(3) 有6423人浏览
分布式系统中保持网络稳定的五种方式

1.重试模式
2.超时模式
3.断路器模式
4.握手模式
5.隔离壁模式
倘若分布式系统的可靠性由一个极弱的控件决定,那么一个很小的内部功能都可能导致整个系统不稳定。了解稳定模式如何预知分布式网络热点,进而了解应用于Jersey和RESTEasy RESTFUL事务中的五种模式。

要实现高可用、高可靠分布式系统,需要预测一些可不预测的状况。假设你运行规模更大的软件系统,产品发布之后迟早会面临的各种突发状况,一般你会发现两个重要的漏洞。第一个和功能相关,比如计算错误、或者处理和解释数据的错误。这类漏洞很容易产生,通常产品上线前这些bug都会被检测到并得到处理。

第二类漏洞更具挑战性,只有在特定的底层框架下这些漏洞才会显现。因此这些漏洞很难检测和重现,通常情况下不易在测试中发现。相反的,在产品上线运行时几乎总会遇到这些漏洞。更好的测试以及软件质量控制可以提高漏洞移除的机率,然而这并不能确保你的代码没有漏洞。

最坏的情况下,代码中的漏洞会触发系统级联错误,进而导致系统致命的失败。特别是在分布式系统中,其服务位于其它服务与客户端之间。
稳定分布式操作系统的网络行为

系统致命失败热点首要是网络通信。不幸的是,分布式系统的架构师和设计者常常以错误的方式假设网络行为。二十年前,L. Peter Deutsch和其他Sun公司同事就撰文分布式错误,直到今天依然普遍存在。

1.网络是可靠的
2.零延迟
3.无限宽带
4.网络是安全
5.不变的拓扑结构
6.只有一个管理员
7.传输成本为零
8.同质化的网络
今天的多数开发人员依赖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显示了网店系统蓝图



图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.连接超时 指建立连接或者错误发生前消耗的最长时间。
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定义中设置超时。

列表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的重试模式不同,断路器模式可以解决持续化错误。

利用断路器存储客户端资源中那些注定失败的调用,如同存储服务器端资源一样。若服务器处在错误状态,比如过高的负载状态,多数情形下,服务器添加额外的负载就不太明智。



图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方法,设置一个断路器过滤器:
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新请求引发的系统失败,都应该被限制在移动通道里面。



图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个线程。

列表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容器这样的服务器处理需要管理者们监控,管理者追踪容器是否健康,一旦处理临近崩溃需要重启;很多例证表明,重启服务比让它处于活跃状态更有益,毕竟一个错误几乎没有响应的服务节点比一个移除的死节点更要命。

更多资源
文本代码托管在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)。
原文链接:javaworld 翻译: ImportNew.com - 乔永琪
译文链接:http://www.importnew.com/16027.html
  • 大小: 56.6 KB
  • 大小: 40.4 KB
  • 大小: 54.2 KB
  • 大小: 55.1 KB
  • 大小: 65.3 KB
  • 大小: 47.7 KB
3
0
评论 共 3 条 请登录后发表评论
3 楼 llnyxxzj 2015-06-05 22:28
这个太理论了,我觉得还是这篇文章通俗易懂的感觉,先理解restful到底是什么东西再看这篇文章http://blog.360chwl.net/detail/8a2390184d76d30e014d79e265920002.html
2 楼 fanfeiyang 2015-06-03 22:20
太多了,看了一半,以后在看
1 楼 yuanhotel 2015-06-03 21:02
太多了,看了一半,以后在看

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 数据总线和数据平台技术

    数据总线系统设计说明书.docx

  • 数据总线, 地址总线, 控制总线详解.

    ◆ 总线的概念       所谓总线(Bus),一般指通过分时复用的方式,将信息以一个或多个源部件传送到一个或多个目的部件的一组传输线。是电脑中传输数据的公共通道。 ◆ 工作原理 当       总线空闲(其他器件都以高阻态形式连接在总线上)且一个器件要与目的器件通信时,发起通信的器件驱动总线,发出地址和数据。其他以高阻态形式连接在总线上的器件如果收到(或能够收到)与自己相符的地址信息

  • 计算机总线详解(数据总线、地址总线、控制总线)

    计算机总线详解(数据总线、地址总线、控制总线)

  • ESB服务&数据总线平台介绍

    ESB企业服务总线作为集成龙骨满足应用、数据和业务等集成需要,应用集成如统一认证、单点登录等主要实现业务系统间的对接;数据集成如主数据治理和数据分析等,通过ESB实现数据的聚合以及分发;业务集成如业财一体化等,实现企业业务之间的互联互通。在集成的过程中,ESB的作用至关重要,它连接着上下游业务系统,构建了数据集成传输的通道,是企业信息化建设由无序到有序、由散乱到规范、由点对点到总线式的有效工具与手段。ESB企业服务总线在实际项目中主要用于各业务系统之间的集成,集成包括数据集成、应用集成以及业务单据集成。

  • 【计算机组成原理】总线系统

    总线(Bus)是一组电子线路,用于在计算机内部或不同计算机设备之间传输数据、控制信号和电源信号。计算机中的总线可以分为内部总线和外部总线两类。1. 内部总线内部总线连接计算机内部的各种硬件组件,如CPU、内存、I/O设备等,它们通过内部总线进行数据传输和控制信号的传递。内部总线分为三种类型:- 数据总线(Data Bus):用于传输数据的总线,是计算机中最宽的总线,数据总线的宽度决定了CPU一次可以处理的数据位数。

  • 阿里十年技术沉淀|深度解析百PB级数据总线技术

    数据总线作为大数据架构下的流量中枢,在不同的大数据组件之间承载着数据桥梁的作用。通过数据总线,可以实时接入来自服务器、K8s、APP、Web、IoT/移动端等产生的各类异构数据,进行统一数据管理,进而实现与下游系统的解耦。

  • 用C++实现数据总线的方法系列(下):消息总线和消息处理

    用C++实现数据总线的方法系列(下):消息总线和消息处理 原文: link. 本文实现一个完整功能的消息总线MessageBus,同时介绍下消息的处理方法。 这里定义了消息类型的枚举MesageType,消息优先级的枚举MessagePriority,以及消息的结构类Message,它包含消息类型(type),消息优先级(priority)和消息数据(info)。同时定义了MessagePtr作为Message的指针指针类。 消息总线MessageBus的实现和数据总线一样,需要有存放消息的消息链表,构成锁

  • 用C++实现数据总线的方法系列(中):数据总线的实现

    用C++实现数据总线的方法系列(中):数据总线的实现 原文: link. 数据总线以及传输的数据的定义和实现 传输数据类Data,它是由数据的地址和数据的大小两个成员组成的。 数据总线类DataQueue,需要有存放总线数据的数据链表,构成锁的互斥量和用于多线程同步的条件变量,同时也需要具有最基本的Push和Pop函数,还有就是Clear和Empty函数。 #ifndef DATA_QUEUE_H #define DATA_QUEUE_H #include&lt;stdint.h&gt; #include

  • 数据总线

    数据总线(Data Bus)是一种技术概念和一种实施规范。在应用系统中并不存在实际程序维护这个数据总线,也不存在实际的进行数据交换的通道。数据总线规范了应用系统之间、程序之间、容器之间进行数据交换和共享的设计和实施思想,统一了数据共享方面的编程规范和集成规范。 数据总线(Data Bus)是应用系统集成的重要理论基础。规范了一个大的集成应用系统中同构系统、异构系统等方面进行数...

  • 用C++实现数据总线的方法系列(上):基本概念&同步队列

    用C++实现数据总线的方法系列(上):基本概念&amp;同步队列 原文链接:link. 本文主要介绍多线程中数据同步的方法,技术包括:线程锁,同步变量,原子变量,消息处理等;以及三种同步队列的实现方法。 1.std::unique_lock 与std:::lock_gurad基本一致,但更加灵活的锁管理类模板,构造时是否加锁是可选的,在对象析构时如果持有锁会自动释放锁,所有权可以转移。对象生命期内允许手动加锁和释放锁。但提供了更好的上锁和解锁控制接口(lock,try_lock,try_lock_for,t

  • 数据总线,地址总线,存储容量计算题理解

    1.地址总线:一个cpu的N根地址总线,则可以说这个CPU的地址总线宽度为N。这样cpu最多可以寻址2的N次方个内存单元。2.8根数据总线传送一个8位二进制,数据线数量相当于每单元的位数3.存储容量=单元数*每单元的位数,一般每单元位数都是8例1.若256KB的SRAM具有8条数据线,则他具有多少条地址线     分析:256KB为他的存储容量,则一般表达为单元数*每单元位数,8条数据总线代表8位...

  • 统一数据交换平台(服务总线)的三大特点

    服务总线特点比较明显,主要以下3点: 1.消息管理:消息订阅与发布(支持对等、非对等数据交换,跨平台、不同数据库、系统接口规范);可靠性(支持断点续传)。 2.流程管理:跨流程(节点),流程的自动监控。 3.集群服务:数据平台。多个服务实现统一发布。

  • 传统的企业数据总线(ESB)和目前的分布式消息系统有什么区别?两者的关系是?

    传统的企业数据总线(ESB):Oracle SOA Suites,Apache ServiceMIX,JBOSS EBS,等 分布式消息系统:kafka,RabbitMQ、Apache ActiveMQ,MetaQ等 首先我们看到ESB是用于系统间集成的,那么分布式消息系统能否用于系统间集成,答案是当然也可以,但是只支持通过消息方式进行集成,而系统间集成的场景包

  • c++11实现的一个消息总线框架

    最近在看C++11的特性,然后按照网上的例子实现了一个消息总线框架。 https://github.com/hejiajie1989/MessageBus 项目里README文档里有详细的设计说明, 使用的时候 g++ test.cpp -o test -std=c++11 g++ TestMessageBus.cpp -o TestMessageBus -std=c++11 原文链接:...

  • c++事件总线简单实现

    文章目录1. 事件总线2. 任意类型参数3. 注册机制4. 线程处理5. BOOST库链接时提示找不到“libxxx”6. multimap用法7.事件总线简单实现 1. 事件总线 用于多线程操作,降低库与库之间的耦合,提高执行效率。 2. 任意类型参数 当你需要一个可变的类型时,有三种可能的解决方案: 无限制的类型,如 void*. 这种方法不可能是类型安全的,应该象逃避灾难一样避免它。 可变...

  • 数据总线技术框架说明

      1 描述以及约定 1.1 约定 1.1.1 应用,在本文中的应用是指一个application,他可以是一个windows 应用程序,也可以是一个web 站点,也可以是一个移动终端应用程序。 1.1.2 ws服务,在文本中是指Web Services服务 1.1.3 CXF类库,是指Apache CXF Services Framework http://cxf.apache.o...

  • 【C++11】对象消息总线(1)

    本系列文档从属于:C++11应用实践系列 部分Demo和结论引用自&amp;amp;amp;amp;amp;amp;amp;amp;lt;深入应用C++11代码优化与工程&amp;amp;amp;amp;amp;amp;amp;amp;gt;这本书 什么是消息总线? 对象的关系之间一般有:依赖、关联、聚合、组合和继承,他们的耦合程度也是依次加强! 未完待续。。先整理文章...

  • 数据总线和地址总线

    地址总线决定了cpu寻址的范围

  • 拆解大数据总线平台DBus的系统架构

    大体来说,Dbus支持两类数据源: RDBMS数据源 日志类数据源 一、RMDBMS类数据源的实现 以mysql为例子. 分为三个部分: 日志抽取模块 增量转换模块 全量拉取模块 1.1 日志抽取模块(Extractor) mysql 日志抽取模块由两部分构成: canal server:负责从mysql中抽取增量日志。 mysql-extractor storm程序:负责将增量日志输...

  • 数据总线,地址总线,外围设备

          利用数据总线扩展外围设备:首先,通过硬件连接确定外围设备的地址;其次,通过外围设备的地址进行与外围设备进行数据的交换。想要读取外围设备输出到总线上的数据,可以通过读取对应外围设备的地址中的数据,即是外围设备输出到总线上的数据;对外围设备写数据,同样对外围设备的地址写入数据,系统就会自动把数据通过数据总线写入外围设备。     有很多外设连接到数据总线,这时就要注意不要把多个外设的数据同时输出到数据总线上,这就是外设的片选问题,一般通过不同的硬件地址连接就可以实现片选问题,现在的问题是地址总线怎么

Global site tag (gtag.js) - Google Analytics