假若分布式系统的可靠性由一个极弱的控件决定,那么一个很小的内部功能均可能致使整个系统不稳定。了解稳定模式如何预知分布式网络热点,进而了解应用于Jersey和RESTEasy RESTFUL事务中的五种模式。html
要实现高可用、高可靠分布式系统,须要预测一些可不预测的情况。假设你运行规模更大的软件系统,产品发布以后早晚会面临的各类突发情况,通常你会发现两个重要的漏洞。第一个和功能相关,好比计算错误、或者处理和解释数据的错误。这类漏洞很容易产生,一般产品上线前这些bug都会被检测到并获得处理。java
第二类漏洞更具挑战性,只有在特定的底层框架下这些漏洞才会显现。所以这些漏洞很难检测和重现,一般状况下不易在测试中发现。相反的,在产品上线运行时几乎总会遇到这些漏洞。更好的测试以及软件质量控制能够提升漏洞移除的机率,然而这并不能确保你的代码没有漏洞。git
最坏的状况下,代码中的漏洞会触发系统级联错误,进而致使系统致命的失败。特别是在分布式系统中,其服务位于其它服务与客户端之间。github
系统致命失败热点首要是网络通讯。不幸的是,分布式系统的架构师和设计者经常以错误的方式假设网络行为。二十年前,L. Peter Deutsch和其余Sun公司同事就撰文分布式错误,直到今天依然广泛存在。数据库
今天的多数开发人员依赖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应用决定采用哪一种支付手段(信用卡、PayPal帐户、预付款或者发票)。restful
这个demo解决了一个关键场景:若是信用检测失败会发生什么?订单应该被拒绝么?多数状况下,支付系统回退接收一个更加可靠的支付方式。处理这种外部控件失败便是一种技术也是一种业务决策,它须要在失去订单和一个爽约支付可能之间作出权衡。
图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中,我用JAX-RS 2.0客户端接口调用信用评分服务。但怎样的超时周期才算合理呢?这个取决于你的JAX-RS供应商。好比,眼下的Jersey版本采用HttpURLConnection。但缺省的Jersey设定链接超时或者Socket超时为0毫秒,即超时是无限的,假若你不认为这样设置有问题,请三思。
考虑到JAX-RS客户端会在一个服务器/servlet引擎中获得处理,利用一个工做线程池处理进来的HTTP请求。若咱们利用经典的阻塞请求处理方法,列表1中的 getPaymentMethods() 会被线程池中一个排他的线程调用。在整个请求处理过程当中,一个既定线程与请求处理绑定。若是内在的信用评分服务(由thecreditScoreURI
提供地址)调用相应很慢,全部工做池中的线程最终会被挂起。接着,支付服务其它方法,好比getPayments() 会被调用。由于全部线程都在等待信用评分响应,这个请求没有被处理。最糟糕的多是,一个很差的信用评分服务行为可能拖累整个支付服务功能。
合理的超时是可用性的基础。但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定义中设置超时。
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。
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方法,设置一个断路器过滤器:
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以下所示:
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)经常用来将船只或者飞机分割成几部件,一旦部件有裂缝部件能够进行加封。同理,在软件系统中利用隔离壁分割系统能够应对系统的级联错误。重要的是,隔离壁分派有限的资源给特定的客户端、应用、运算和客户终端等。
创建隔离壁或者系统分区方式有不少种,接下来我会一一展现。
每客户资源(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个线程。
// ... 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为你们提供了另外一种支持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
[ 转载请保留原文出处、译者和译文连接。]