第5章 使用Spring Cloud和Netflix Hystrix的客户端弹性模式

 本章主要内容
    实现断路器模式、后备模式和舱壁模式
    使用断路器模式来保护微服务客户端资源
    当远程服务失败时使用Hystrix
    实施Hystrix的舱壁模式来隔离远程资源调用
    调节Hystrix的断路器和舱壁的实现
    定制Hystrix的并发策略
 
所有的系统,特别是分布式系统,都会遇到故障。如何构建应用程序来应对这种故障,是每个软件开发人员工作的关键部分。然而,当涉及构建弹性系统时,大多数软件工程师只考虑到基础设施或关键服务彻底发生故障。他们专注于在应用程序的每一层构建冗余,使用诸如集群关键服务器、服务间的负载均衡以及将基础设施分离到多个位置的技术。
    尽管这些方法考虑到系统组件的彻底(通常是惊人的)损失,但它们只解决了构建弹性系统的一小部分问题。当服务崩溃时,很容易检测到该服务已经不在了,因此应用程序可以绕过它。然而,当服务运行缓慢时,检测到这个服务性能不佳并绕过它是非常困难的,这是因为以下几个
 
原因。
    (1)服务的降级可以以间歇性问题开始,并形成不可逆转的势头——降级可能只发生在很小的爆发中。故障的第一个迹象可能是一小部分用户抱怨某个问题,直到突然间应用程序容器耗尽了线程池并彻底崩溃。
    (2)对远程服务的调用通常是同步的,并且不会缩短长时间运行的调用——服务的调用者没有超时的概念来阻止服务调用的永久挂起。应用程序开发人员调用该服务来执行操作并等待服务返回。
    (3)应用程序经常被设计为处理远程资源的彻底故障,而不是部分降级——通常,只要服务没有彻底失败,应用程序将继续调用这个服务,并且不会采取快速失败措施。该应用程序将继续调用表现不佳的服务。调用的应用程序或服务可能会优雅地降级,但更有可能因为资源耗尽而崩溃。资源耗尽是指有限的资源(如线程池或数据库连接)消耗殆尽,而调用客户端必须等待该资源变为可用。
    性能不佳的远程服务所导致的潜在问题是,它们不仅难以检测,还会触发连锁效应,从而影响整个应用程序生态系统。如果没有适当的保护措施,一个性能不佳的服务可以迅速拖垮多个应用程序。基于云、基于微服务的应用程序特别容易受到这些类型的中断的影响,因为这些应用程序由大量细粒度的分布式服务组成,这些服务在完成用户的事务时涉及不同的基础设施。
    5.1 什么是客户端弹性模式
    客户端弹性软件模式的重点是,在远程服务发生错误或表现不佳时保护远程资源(另一个微服务调用或数据库查询)的客户端免于崩溃。这些模式的目标是让客户端“快速失败”,而不消耗诸如数据库连接和线程池之类的宝贵资源,并且可以防止远程服务的问题向客户端的消费者进行“上游”传播。
    有4种客户端弹性模式,它们分别是:
    (1)客户端负载均衡(client load balance)模式;
    (2)断路器(circuit breaker)模式;
    (3)后备(fallback)模式;
    (4)舱壁(bulkhead)模式。
    图5-1展示了如何将这些模式用于微服务消费者和微服务之间。
 
图5-1 这4个客户端弹性模式充当服务消费者和服务之间的保护缓冲区
 
这些模式是在调用远程资源的客户端中实现的,它们的实现在逻辑上位于消费远程资源的客户端和资源本身之间。
    5.1.1 客户端负载均衡模式
    在讨论服务发现时,我们在第4章中介绍了客户端负载均衡模式。客户端负载均衡涉及让客户端从服务发现代理(如Netflix Eureka)查找服务的所有实例,然后缓存服务实例的物理位置。 每当服务消费者需要调用该服务实例时,客户端负载均衡器将从它维护的服务位置池返回一个位置。
    因为客户端负载均衡器位于服务客户端和服务消费者之间,所以负载均衡器可以检测服务实例是否抛出错误或表现不佳。如果客户端负载均衡器检测到问题,它可以从可用服务位置池中移除该服务实例,并防止将来的服务调用访问该服务实例。
    这正是Netflix的Ribbon库提供的开箱即用的功能,而不需要额外的配置。因为第4章介绍了Netflix Ribbon的客户端负载均衡,所以本章就不再赘述了。
    5.1.2 断路器模式
    断路器模式是模仿电路断路器的客户端弹性模式。在电气系统中,断路器将检测是否有过多电流流过电线。如果断路器检测到问题,它将断开与电气系统的其余部分的连接,并保护下游部件不被烧毁。
    有了软件断路器,当远程服务被调用时,断路器将监视这个调用。如果调用时间太长,断路器将会介入并中断调用。此外,断路器将监视所有对远程资源的调用,如果对某一个远程资源的调用失败次数足够多,那么断路器实现就会出现并采取快速失败,阻止将来调用失败的远程资源。
    5.1.3 后备模式
    有了后备模式,当远程服务调用失败时,服务消费者将执行替代代码路径,并尝试通过其他方式执行操作,而不是生成一个异常。这通常涉及从另一数据源查找数据或将用户的请求进行排队以供将来处理。用户的调用结果不会显示为提示问题的异常,但用户可能会被告知,他们的请求要在晚些时候被满足。
 
例如,假设我们有一个电子商务网站,它可以监控用户的行为,并尝试向用户推荐其他可以购买的产品。通常来说,可以调用微服务来对用户过去的行为进行分析,并返回针对特定用户的推荐列表。但是,如果这个偏好服务失败,那么后备策略可能是检索一个更通用的偏好列表,该列表基于所有用户的购买记录分析得出,并且更为普遍。这些更通用的偏好列表数据可能来自完全不同的服务和数据源。
    5.1.4 舱壁模式
    舱壁模式是建立在造船的概念基础上的。采用舱壁设计,一艘船被划分为完全隔离和防水的隔间,这称为舱壁。即使船的船体被击穿,由于船被划分为水密舱(舱壁),舱壁会将水限制在被击穿的船的区域内,防止整艘船灌满水并沉没。
    同样的概念可以应用于必须与多个远程资源交互的服务。通过使用舱壁模式,可以把远程资源的调用分到线程池中,并降低一个缓慢的远程资源调用拖垮整个应用程序的风险。线程池充当服务的“舱壁”。每个远程资源都是隔离的,并分配给线程池。如果一个服务响应缓慢,那么这种服务调用的线程池就会饱和并停止处理请求,而对其他服务的服务调用则不会变得饱和,因为它们被分配给了其他线程池。
 
5.2 为什么客户端弹性很重要
    我们已经抽象地介绍了这些不同的模式,让我们来深入了解一些可以应用这些模式的更具体的例子。接下来我们来看看我遇到过的一个常见场景,看看为什么客户端弹性模式(如断路器模式)对于实现基于服务的架构至关重要,尤其是在云中运行的微服务架构。
    图5-2展示了一个典型的场景,它涉及使用远程资源,如数据库和远程服务。
图5-2 应用程序是相互关联依赖的图形结构。如果不管理这些依赖之间的远程调用,那么一个表现不佳的远程资源可能会拖垮图中的所有服务
    在图5-2所示的场景中,3个应用程序分别以这样或那样的方式与3个不同的服务进行通信。应用程序A和应用程序B与服务A直接通信。服务A从数据库检索数据,并调用服务B来为它工作。服务B从一个完全不同的数据库平台中检索数据,并从第三方云服务提供商调用另一个服务——服务C,该服务严重依赖于内部网络区域存储(Network Area Storage,NAS)设备,以将数据写入共享文件系统。此外,应用程序C直接调用服务C。
    在某个周末,网络管理员对NAS配置做了一个他认为是很小的调整,如图5-2所示。这个调整似乎可以正常工作,但是在周一早上,所有对特定磁盘子系统的读取开始变得非常慢。
    编写服务B的开发人员从来没有预料到会发生调用服务C缓慢的事情。他们所编写的代码中,在同一个事务中写入数据库和从服务C读取数据。当服务C开始运行缓慢时,不仅请求服务C的线程池开始堵塞,服务容器的连接池中的数据库连接也会耗尽,因为这些连接保持打开状态,这一切的原因是对服务C的调用从来没有完成。
    最后,服务A耗尽资源,因为它调用了服务B,而服务B的运行缓慢则是因为它调用了服务C。最后,所有3个应用程序都停止响应了,因为它们在等待请求完成中耗尽了资源。
    如果在调用分布式资源(无论是调用数据库还是调用服务)的每一个点上都实现了断路器模式,则可以避免这种情况。在图5-2中,如果使用断路器实现了对服务C的调用,那么当服务C开始表现不佳时,对服务C的特定调用的断路器就会跳闸,并且快速失败,而不会消耗掉一个线程。如果服务B有多个端点,则只有与服务C特定调用交互的端点才会受到影响。服务B的其余功能仍然是完整的,可以满足用户的要求。
    断路器在应用程序和远程服务之间充当中间人。在上述场景中,断路器实现可以保护应用程序A、应用程序B和应用程序C免于完全崩溃。
    在图5-3中,服务B(客户端)永远不会直接调用服务C。相反,在进行调用时,服务B把服务的实际调用委托给断路器,断路器将接管这个调用,并将它包装在独立于原始调用者的线程(通常由线程池管理)中。通过将调用包装在一个线程中,客户端不再直接等待调用完成。相反,断路器会监视线程,如果线程运行时间太长,断路器就可以终止该调用。
 
 
图5-3 断路器跳闸,让表现不佳的服务调用迅速而优雅地失败
 
图5-3展示了这3个场景。第一种场景是愉快路径,断路器将维护一个定时器,如果在定时器的时间用完之前完成对远程服务的调用,那么一切都非常顺利,服务B可以继续工作。在部分降级的场景中,服务B将通过断路器调用服务C。但是,如果这一次服务C运行缓慢,在断路器维护的线程上的定时器超时之前无法完成对远程服务的调用,断路器就会切断对远程服务的连接。
    然后,服务B将从发出的调用中得到一个错误,但是服务B不会占用资源(也就是自己的线程池或连接池)来等待服务C完成调用。如果对服务C的调用被断路器超时中断,断路器将开始跟踪已发生故障的数量。
 
如果在一定时间内在服务C上发生了足够多的错误,那么断路器就会电路“跳闸”,并且在不调用服务C的情况下,就判定所有对服务C的调用将会失败。
    电路跳闸将会导致如下3种结果。
    (1)服务B现在立即知道服务C有问题,而不必等待断路器超时。
    (2)服务B现在可以选择要么彻底失败,要么执行替代代码(后备)来采取行动。
    (3)服务C将获得一个恢复的机会,因为在断路器跳闸后,服务B不会调用它。这使得服务C有了喘息的空间,并有助于防止出现服务降级时发生的级联死亡。
 
最后,断路器会让少量的请求调用直达一个降级的服务,如果这些调用连续多次成功,断路器就会自动复位。
    以下是断路器模式为远程调用提供的关键能力。
    (1)快速失败——当远程服务处于降级状态时,应用程序将会快速失败,并防止通常会拖垮整个应用程序的资源耗尽问题的出现。在大多数中断情况下,最好是部分服务关闭而不是完全关闭。
    (2)优雅地失败——通过超时和快速失败,断路器模式使应用程序开发人员有能力优雅地失败,或寻求替代机制来执行用户的意图。例如,如果用户尝试从一个数据源检索数据,并且该数据源正在经历服务降级,那么应用程序开发人员可以尝试从其他地方检索该数据。
    (3)无缝恢复——有了断路器模式作为中介,断路器可以定期检查所请求的资源是否重新上线,并在没有人为干预的情况下重新允许对该资源进行访问。
    在大型的基于云的应用程序中运行着数百个服务,这种优雅的恢复能力至关重要,因为它可以显著减少恢复服务所需的时间,并大大减少因疲劳的运维人员或应用工程师直接干预恢复服务(重新启动失败的服务)而造成更严重问题的风险。
 
5.3 进入Hystrix
    构建断路器模式、后备模式和舱壁模式的实现需要对线程和线程管理有深入的理解。编写健壮的线程代码是一门艺术(这是我从未掌握的),并且正确地做到这一点很困难。高质量地实现断路器模式、后备模式和舱壁模式需要做大量的工作。幸运的是,开发人员可以使用Spring Cloud和Netflix的Hystrix库,这些库每天都在Netflix的微服务架构中使用,因此它们久经考验。
    
本章的后面几节将讨论如下内容。
    如何配置许可证服务的Maven构建文件(pom.xml)以包含SpringCloud/Hystrix包装器。
    如何通过Spring Cloud/Hystrix注解来运用断路器模式包装远程调用。
    
如何在远程资源上定制断路器,以便为每个调用使用定制超时。这里还将演示如何配置断路器,以便控制断路器在“跳闸”之前发生的故障次数。
    如何在调用失败或断路器必须中断调用时实现后备策略。
    如何在服务中使用单独的线程池来隔离服务调用,并在被调用的不同远程资源之间构建舱壁。
 
5.4 搭建许可服务器以使用Spring Cloud和Hystrix
    
要开始对Hystrix的探索,需要创建项目的pom.xml文件来导入Spring Hystrix依赖项。
我们将使用之前一直在构建的许可证服务,并通过添加Hystrix的Maven依赖项来修改 pom.xml文件:
    
<dependency>   
<groupId>org.springframework.cloud</groupId>   
<artifactId>spring-cloud-starter-hystrix</artifactId> 
</dependency> 
 
<dependency>   
<groupId>com.netflix.hystrix</groupId>   
<artifactId>hystrix-javanica</artifactId>   
<version>1.5.9</version> 
</dependency>
    
第一个<dependency>标签(spring-cloud-starter-hystrix)告诉Maven去拉取Spring Cloud Hystrix依赖项。第二个<dependency>标签(hystrix-javanica)将拉取核心Netflix Hystrix库。
创建完Maven依赖项后,我们可以继续,
使用在前几章中构建的许可证服务和组织服务来开始Hystrix的实现。
    
注意
    读者不一定要在pom.xml中直接包含hystrix-javanica依赖项。在默认情况下,spring-cloud- starter-hystrix包括一个hystrix-javanica依赖项的版本。本书使用的Camden.SR5发行版本使用了hystrix-javanica-1.5.6。这个hystrix-javanica的版本有一个不一致的地方,
它导致Hystrix代码在没有后备的情况下会抛出java.lang.reflect.UndeclaredThrowableException而不是com.netflix.hystrix.exception.HystrixRuntimeException。对于使用旧版Hystrix的许多开发人员来说,这是一个破坏性的变化。hystrix-javanica库在后来的版本中解决了这个问题,所以我专门使用了更高版本的hystrix-javanica,而不是使用Spring Cloud引入的默认版本。
    
在应用程序代码中开始使用Hystrix断路器之前,需要完成的最后一件事情是,使用@EnableCircuitBreaker注解来标注服务的引导类。例如,对于许可证服务,最好将@EnableCircuitBreaker注解添加到licensing-service/src/main/java/com/thoughtmechanix/licenses/Application.java中。代码清单5-1展示了这段代码。
    
代码清单5-1 用于在服务中激活Hystrix的@EnableCircuitBreaker注解
    package com.thoughtmechanix.licenses 
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; 
// 为了简洁,省略了其余的import语句 
@SpringBootApplication 
@EnableEurekaClient 
@EnableCircuitBreaker  ⇽---  告诉Spring Cloud将要为服务使用Hystrix 
public class Application {     
@LoadBalanced     
@Bean
public RestTemplate restTemplate() {        
 return new RestTemplate();    
 }     
public static void main(String[] args) {         
SpringApplication.run(Application.class, args);     
}
    
注意
    
    
如果忘记将@EnableCircuitBreaker注解添加到引导类中,那么Hystrix断路器不会处于活动状态。
在服务启动时,不会收到任何警告或错误消息。
 
 
posted @ 2019-12-02 21:24  mongotea  阅读(269)  评论(0编辑  收藏  举报