5.7 实现舱壁模式
在基于微服务的应用程序中,开发人员通常需要调用多个微服务来完成特定的任务。在不使用舱壁模式的情况下,这些调用默认是使用同一批线程来执行调用的,这些线程是为了处理整个Java容器的请求而预留的。在存在大量请求的情况下,一个服务出现性能问题会导致Java容器的所有线程被刷爆并等待处理工作,同时堵塞新请求,最终导致Java容器崩溃。舱壁模式将远程资源调用隔离在它们自己的线程池中,以便可以控制单个表现不佳的服务,而不会使该容器崩溃。
Hystrix使用线程池来委派所有对远程服务的请求。在默认情况下,所有的Hystrix命令都将共享同一个线程池来处理请求。这个线程池将有10个线程来处理远程服务调用,而这些远程服务调用可以是任何东西,包括REST服务调用、数据库调用等。图5-7说明了这一点。
图5-7 多种资源类型共享默认的Hystrix线程池
在应用程序中访问少量的远程资源时,这种模型运行良好,并且各个服务的调用量分布相对均匀。问题是,如果某些服务具有比其他服务高得多的请求量或更长的完成时间,那么最终可能会导致Hystrix线程池中的线程耗尽,因为一个服务最终会占据默认线程池中的所有线程。
幸好,Hystrix提供了一种易于使用的机制,在不同的远程资源调用之间创建舱壁。图5-8展示了Hystrix管理的资源被隔离到它们自己的“舱壁”时的情况。
图5-8 Hystrix命令绑定到隔离的线程池
要实现隔离的线程池,我们需要使用@HystrixCommand注解的其他属性。接下来的代码将完成以下操作。
(1)为getLicensesByOrg()调用建立一个单独的线程池。
(2)设置线程池中的线程数。
(3)设置单个线程繁忙时可排队的请求数的队列大小。
代码清单5-6展示了如何围绕服务调用建立一个舱壁,该服务调用从许可证服务查询许可证数据。
代码清单5-6 围绕getLicensesByOrg()方法创建舱壁
@HystrixCommand(fallbackMethod= "buildFallbackLicenseList",
threadPoolKey = "licenseByOrgThreadPool",
⇽--- threadPoolKey属性定义线程池的唯一名称
threadPoolProperties = {
⇽--- threadPoolProperties属性用于定义和定制threadPool的行为
@HystrixProperty(name = "coreSize", value="30"),
⇽--- coreSize属性用于定义线程池中线程的最大数量
@HystrixProperty(name = "maxQueueSize", value="10")}
⇽--- maxQueueSize用于定义一个位于线程池前的队列,它可以对传入的请求进行排队 )
public List<License> getLicensesByOrg(String organizationId){
return licenseRepository.findByOrganizationId(organizationId);
}
要注意的第一件事是,我们在@HystrixCommand注解中引入了一个新属性,即threadPoolkey。这向 Hystrix 发出信号,我们想要建立一个新的线程池。如果在线程池中没有设置任何进一步的值,Hystrix会使用threadPoolKey属性中的名称搭建一个线程池,并使用所有的默认值来对线程池进行配置。
要定制线程池,应该使用@HystrixCommand上的threadPoolProperties属性。此属性使用HystrixProperty对象的数组,这些HystrixProperty对象用于控制线程池的行为。使用coreSize属性可以设置线程池的大小。
开发人员还可以在线程池前创建一个队列,该队列将控制在线程池中线程繁忙时允许堵塞的请求数。此队列大小由maxQueueSize属性设置。一旦请求数超过队列大小,对线程池的任何其他请求都将失败,直到队列中有空间。
请注意有关maxQueueSize属性的两件事情。首先,如果将其值设置为−1,则将使用Java SynchronousQueue来保存所有传入的请求。同步队列本质上会强制要求正在处理中的请求数量永远不能超过线程池中可用线程的数量。将maxQueueSize设置为大于1的值将导致Hystrix使用Java LinkedBlockingQueue。LinkedBlockingQueue``的使用允许开发人员即使所有线程都在忙于处理请求,也能对请求进行排队。
要注意的第二件事是,maxQueueSize属性只能在线程池首次初始化时设置(例如,在应用程序启动时)。Hystrix允许通过使用queueSizeRejectionThreshold属性来动态更改队列的大小,但只有在maxQueueSize属性的值大于0时,才能设置此属性。
自定义线程池的适当大小是多少?Netflix推荐以下公式:
服务在健康状态时每秒支撑的最大请求数×第99百分位延迟时间 (以秒为单位)+ 用于缓冲的少量额外线程
通常情况下,直到服务处于负载状态,开发人员才能知道它的性能特征。线程池属性需要被调整的关键指标就是,即使目标远程资源是健康的,服务调用仍然超时。
5.8 基础进阶——微调Hystrix
我们目前已经研究了使用Hystrix创建断路器模式和舱壁模式的基本概念。现在我们来看看如何真正定制Hystrix断路器的行为。记住,Hystrix不仅能超时长时间运行的调用,它还会监控调用失败的次数,如果调用失败的次数足够多,那么Hystrix会在请求发送到远程资源之前,通过使调用失败来自动阻止未来的调用到达服务。
这样做有两个原因。首先,如果远程资源有性能问题,那么快速失败将防止应用程序等待调用超时。这显著降低了调用应用程序或服务所导致的资源耗尽问题和崩溃的风险。其次,快速失败和阻止来自服务客户端的调用有助于苦苦挣扎的服务保持其负载,而不会彻底崩溃。快速失败给了性能下降的系统一些时间去进行恢复。
要了解如何在Hystrix中配置断路器,需要先了解Hystrix如何确定何时跳闸断路器的流程。图5-9展示了Hystrix在远程资源调用失败时使用的决策过程。
图5-9 Hystrix经过一系列检查来确定是否跳闸
每当Hystrix命令遇到服务错误时,它将开始一个10 s的计时器,用于检查服务调用失败的频率。这个10 s窗口是可配置的。Hystrix做的第一件事就是查看在10 s内发生的调用数量。如果调用次数少于在这个窗口内需要发生的最小调用次数,那么即使有几个调用失败,Hystrix 也不会采取行动。例如,在Hystrix考虑采取行动之前,需要在10 s之内进行调用的次数的默认值为20。如果这些调用之中有15个在10 s内发生调用失败,只要在10 s之内调用次数达不到20次,那么即使15个调用都失败,这些调用的数量也不足以让断路器发生跳闸。Hystrix将继续让调用通过,到达远程服务。
在10 s窗口内达到最少的远程资源调用次数时,Hystrix将开始查看整体故障的百分比。如果故障的总体百分比超过阈值,Hystrix将触发断路器,使将来几乎所有的调用都失败。正如稍后即将讨论的那样,Hystrix将会让部分调用通过来进行“测试”,以查看服务是否恢复。错误阈值的默认值为50%。
如果超过错误阈值的百分比,Hystrix 将“跳闸”断路器,防止更多的调用访问远程资源。如果远程调用失败的百分比未达到要求的阈值,并且10 s窗口已过去,Hystrix将重置断路器的统计信息。
当Hystrix在一个远程调用上“跳闸”断路器时,它将尝试启动一个新的活动窗口。每隔5 s(这个值是可配置的),Hystrix会让一个调用到达这个苦苦挣扎的服务。如果调用成功,Hystrix将重置断路器并重新开始让调用通过。如果调用失败,Hystrix将保持断路器断开,并在另一个5 s里再次尝试上述步骤。
基于此,开发人员可以使用5个属性来定制断路器的行为。@HystrixCommand注解通过commandPoolProperties属性公开了这5个属性。其中,threadPoolProperties属性用于设置Hystrix命令中使用的底层线程池的行为,而commandPoolProperties属性用于定制与Hystrix命令关联的断路器的行为。代码清单5-7展示了这些属性的名称以及如何在每个属性中设置值。
5.8 基础进阶微调Hystrix
代码清单5-7 配置断路器的行为
@HystrixCommand( fallbackMethod = "buildFallbackLicenseList",
threadPoolKey = "licenseByOrgThreadPool",
threadPoolProperties = { @HystrixProperty(name = "coreSize",value="30"), @HystrixProperty(name="maxQueueSize"value="10"),
},
commandPoolProperties = {
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"),
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"), @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="15000"),
@HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")
}
)
public List<License> getLicensesByOrg(String organizationId){
logger.debug("getLicensesByOrg Correlation id: {}",
UserContextHolder .getContext() .getCorrelationId());
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
第一个属性circuitBreaker.requestVolumeThreshold用于控制Hystrix考虑将该断路器跳闸之前,在10 s之内必须发生的连续调用数量。第二个属性circuitBreaker.errorThresholdPercentage是在超过circuitBreaker.requestVolumeThreshold值之后在断路器跳闸之前必须达到的调用失败(由于超时、抛出异常或返回HTTP 500)百分比。上述代码示例中的最后一个属性circuitBreaker.sleepWindowInMilliseconds是在断路器跳闸之后,Hystrix允许另一个调用通过以便查看服务是否恢复健康之前Hystrix的休眠时间。
最后两个Hystrix属性metrics.rollingStats.timeInMilliseconds和metrics.rollingStats.numBuckets的命名与前面的属性有所不同,但它们仍然是控制断路器的行为的。第一个属性metrics.rollingStats.timeInMilliseconds用于控制Hystrix用来监视服务调用问题的窗口大小,其默认值为10 000ms(即10 s)。
第二个属性metrics.rollingStats.numBuckets控制在定义的滚动窗口中收集统计信息的次数。在这个窗口中,Hystrix在桶(bucket)中收集度量数据,并检查这些桶中的统计信息,以确定远程资源调用是否失败。给metrics.rollingStats.timeInMilliseconds设置的值必须能被定义的桶的数量值整除。例如,在代码清单 5-7 所示的自定义设置中,Hystrix将使用15 s的窗口,并将统计数据收集到长度为3 s的5个桶中。
注意
检查的统计窗口越小且在窗口中保留的桶的数量越多,就越会加剧高请求服务的CPU利用率和内存利用率。要意识到这一点,避免将度量收集窗口和桶设置为太细的粒度,除非你需要这种可见性级别。
重新审视Hystrix配置
Hystrix 库是高度可配置的,可以让开发人员严格控制使用它定义的断路器模式和舱壁模式的行为。开发人员可以通过修改Hystrix断路器的配置,控制Hystrix在超时远程调用之前需要等待的时间。开发人员还可以控制Hystrix断路器何时跳闸以及Hystrix何时尝试重置断路器。
使用Hystrix,开发人员还可以通过为每个远程服务调用定义单独的线程组,然后为每个线程组配置相应的线程数来微调舱壁实现。这允许开发人员对远程服务调用进行微调,因为某些远程资源调用具有较高的请求量。
在配置Hystrix环境时,需要记住的关键点是,开发人员可以使用Hystrix的3个配置级别:
(1)整个应用程序级别的默认值;
(2)类级别的默认值;
(3)在类中定义的线程池级别。
每个Hystrix属性都有默认设置的值,这些值将被应用程序中的每个@HystrixCommand注解所使用,除非这些属性值在Java类级别被设置,或者被类中单个Hystrix线程池级别的值覆盖。
Hystrix确实允许开发人员在类级别设置默认参数,以便特定类中的所有Hystrix命令共享相同的配置。类级属性是通过一个名为@DefaultProperties的类级注解设置的。例如,如果希望特定类中的所有资源的超时时间均为10 s,
则可以按以下方式设置@DefaultProperties:
@DefaultProperties(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000")
}
class MyService { ... }
除非在线程池级别上显式地覆盖,否则所有线程池都将继承应用程序级别的默认属性或类中定义的默认属性。Hystrix的threadPoolProperties和commandProperties也绑定到已定义的命令键。
注意
我在本章编码示例的应用程序代码中硬编码了所有的Hystrix值。在生产环境中,最有可能需要调整的Hystrix数据(超时参数、线程池计数)将被外部化到Spring Cloud Config。通过这种方式,如果需要更改参数值,就可以在更改完参数值之后重新启动服务实例,而无需重新编译和重新部署应用程序。
对于单个Hystrix池,本书将保持配置尽可能接近代码并将线程池配置置于@HystrixCommand注解中。
表5-1总结了用于创建和配置@HystrixCommand注解的所有配置值。
表5-1 @HystrixCommand注解的配置值
属 性 名 称
|
默认值
|
描 述
|
fallbackMethod
|
None
|
标识类中的方法,如果远程调用超时,将调用该方法。
回调方法必须与@HystrixCommand注解在同一个类中,
并且必须具有与调用类相同的方法签名。如果值不存在,
Hystrix会抛出异常
|
threadPoolKey
|
None
|
给予@HystrixCommand一个唯一的名称,
并创建一个独立于默认线程池的线程池。
如果没有定义任何值,则将使用默认的Hystrix线程池
|
threadPoolProperties
|
None
|
核心的Hystrix注解属性,用于配置线程池的行为
|
coreSize
|
1 0
|
设置线程池的大小
|
maxQueueSize
|
− 1
|
设置线程池前面的最大队列大小。如果设置为−1,
则不使用队列,Hystrix将阻塞请求,
直到有一个线程可用来处理
|
circuitBreaker. requestVolumeThreshold
|
20
|
设置Hystrix开始检查断路器是否跳闸之前滚动窗口中
必须处理的最小请求数 注意:
此值只能使用commandPoolProperties属性设置
|
circuitBreaker. errorThresholdPercentage
|
50
|
在断路器跳闸之前,滚动窗口内必须达到的故障百分比
注意:此值只能使用commandPoolProperties属性设置
|
circuitBreaker. sleepWindowInMilliseconds
|
5,000
|
在断路器跳闸之后,Hystrix尝试进行服务调用之前将要等待的时间(以毫秒为单位) 注意:此值只能使用commandPoolProperties属性设置
|
metricsRollingStats. timeInMilliseconds
|
10,000
|
Hystrix收集和监控服务调用的统计信息的滚动窗口
(以毫秒为单位)
|
metricsRollingStats. numBuckets
|
10
|
Hystrix在一个监控窗口中维护的度量桶的数量。
监视窗口内的桶数越多,
Hystrix在窗口内监控故障的时间越低
|