第5章 Hystrix客户端弹性模式
所有的系统,包括分布式系统,都会遇到故障,可能还会多些,哈哈。我们经常会通过处理策略绕过死掉的服务,但是有时候性能不佳的服务确成为了服务的瓶颈,他们难以检测,而且有可能引起连锁反应,使整个系统崩溃。
面对这种情况,一种处理方式是实现客户端弹性模式,即在远程服务发生错误或者表现不佳时保护远程资源的调用客户端免于崩溃,这些模式的目标是让客户端“快速失败”,而不是消耗类似数据库连接和线程池之类的宝贵资源。下面通过一张图简单介绍下4种客户端弹性模式:
如果使用断路器来保护服务,那么断路器的角色就是中间人,其处于应用程序和远程服务之间,简单示意下:
在上图中,A不会直接调用B,而是把实际的调用委托给断路器,断路器将接管这个调用,并将它包装在独立于原始调用者的线程中,那么客户端不再直接等待调用,相当于是异步的。断路器会监视这个调用,如果在一定时间内在调用服务B上发生了足够多的错误,那么断路器就会跳闸,并且在不调用服务B的情况下,判断所有对于服务B的调用都会失败。跳闸后,服务B就有了喘息的机会,而服务A可以执行后备方案。最后,断路器会让少量的请求调用直达服务B,如果这些调用连续多次成功,断路器就会复位。
下面我们进入实战环节,使用Hystrix。首先需要添加对于Hystrix的依赖,如下:
<!--告诉Maven去拉取Hystrix的依赖项-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<!--拉取核心Hystrix库-->
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>1.5.9</version>
</dependency>
接下来需要,使用注解@EnableCircuitBreaker标注引导类,其目的就是告诉SpringCloud将要为服务使用Hystrix。
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class LicenseApplication {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(LicenseApplication.class, args);
}
}
本节的实例代码功能如下:
- 使用Hystrix断路器包装licenseservice对于数据库的调用
- 使用Hystrix包装licenseservice和organazitionservice之间的内部调用
虽然是两种不同的用法,但是我们应该明白,其实是一样的,其都是对于本地客户端的保护,调用的都是远程资源。
下面实现第一种调用,许可证服务将通过同步调用来检索数据库,但是当其继续进行处理之前会等待SQL语句完成或者断路器超时。实现这一功能只要加个@HystrixCommand注解即可,其作用是将方法标记为由断路器进行管理,当Spring看到这一注解时,它将生成一个动态代理类,该代理类将包装该方法,并通过专门用于处理远程资源调用的线程池来管理该方法的调用。实例代码如下:
@HystrixCommand
public List<License> getLicensesByOrg(String organizationId){
logger.debug("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
每当调用超过1秒时,断路器将中断对getLicensesByOrg()的调用。其中 randomlyRunLong()方法用于模拟超时。下面我们调用下,看下效果
现在如果查询时间过长,许可证服务将中断其对数据库的调用,此时抛出了一个异常。
那么,我们可以定制Hystrix的超时时间,可以在注解上加上commandProperties属性,其允许开发人员提供附加的属性来定制Hystrix,比如,我们定义超时时间为12秒,那么我们前面的调用就不会超时 ,代码如下:
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "12000")
})
public License getLicense(String organizationId,String licenseId) {
License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId);
randomlyRunLong();
Organization org = getOrganization(organizationId);
return license
.withOrganizationName( org.getName())
.withContactName( org.getContactName())
.withContactEmail( org.getContactEmail() )
.withContactPhone( org.getContactPhone() )
.withComment(config.getExampleProperty());
}
其实到这里,断路器模式,我相信大家都已经有了一个概念了。下面我们简单了解下后备模式。后备模式让开发人员有机会拦截服务故障,并提供替代方案。我们实现的方式比较简单,在@HystrixCommand注解中添加fallbackMethod属性,其值代表了后备方法的名称,需要注意的是后备方法必须与原方法具有相同的签名,比较容易理解,因为,你必须提供的是一个一模一样的东西给客户端,就像我给你要个苹果,你没有熟的了,你给我个半熟的也行,但是你不能给我一个橘子。虽然后备策略写起来简单,但是其中却有些门道:
- 如果自己使用后备来捕获超时异常,如果只做日志记录,就应该使用try…catch来捕获,并将日志记录在catch中进行。
- 后备方法中如果还要调用其他微服务,还应该使用后备方案,这不是过度防御。
简单后备模式的代码如下:
@HystrixCommand(fallbackMethod = "buildFallbackLicenseList",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "1000")
})
public License getLicense(String organizationId,String licenseId) {
License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId);
randomlyRunLong();
Organization org = getOrganization(organizationId);
return license
.withOrganizationName( org.getName())
.withContactName( org.getContactName())
.withContactEmail( org.getContactEmail() )
.withContactPhone( org.getContactPhone() )
.withComment(config.getExampleProperty());
}
其中buildFallbackLicenseList()的代码如下:
private License buildFallbackLicenseList(String organizationId,String licenseId){
License license = new License()
.withId("0000000-00-00000")
.withOrganizationId( organizationId )
.withProductName("Sorry no licensing information currently available");
return license;
}
,多运行几次,结果如下:
后备模式到这里,应该也有了一个概念,下面来介绍舱壁模式。舱壁模式的重要性在于,在不适用舱壁模式的情况下,微服务的调用默认是使用同一批线程来执行调用的,这些线程是为了处理java容器的请求而预留的。在存在大量请求的情况下,一个服务出现问题,其最终会占据默认线程池中的所有线程,造成新请求的堵塞,从而导致JAVA容器崩溃,舱壁模式将远程资源调用隔离在自己的线程池中,以便控制单个表现不佳的服务,避免容器的崩溃。Hystrix提供了这种易用的机制,在不同的远程资源调用之间建立舱壁。如下图所示:
接下来在代码中实现,也比较方便,只需要几个注解即可,如下:
关于Hystrix的基础,上面已经讲完了,如果只是想了解下这块内容,下面的东西不用看了,如果想更深入了解Hystrix,那么我们一起来探秘。
前文提到过,Hystrix不仅可以中断长时间的调用,还可以统计调用失败的次数,如果失败过多,其会在请求发送到远程资源之前,直接使调用失败来阻止未来的调用。这样做有两个原因:一可以避免调用应用程序所导致的资源耗尽问题二可以给出现问题的服务以喘息时间。下面了解下Hystrix的调用失败时使用的决策。
基于上述表述,可以使用几个属性来定义这些配置:
@HystrixCommand(fallbackMethod = "buildFallbackLicenseList",
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "1000"),
//用于控制断路器跳闸之前,在10s内必须连续发生的调用数量
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
//失败百分比
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"),
//跳闸之后,允许一个调用通过断路器以便查看服务是否恢复健康之前的休眠时间
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"),
//窗口的大小
@HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="10000"),
//窗口时间内,收集统计信息的次数,即10秒内,收集长度为2秒的信息到5个bucket中
@HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")
},
//定义线程池的唯一名称,其实也是一个信号,即我们要定义一个新的线程池
threadPoolKey = "licenceThreadPool",
threadPoolProperties = {
//定义线程池中线程的最大数量
@HystrixProperty(name = "coreSize",value = "30"),
//在线程池前创建一个队列进行缓冲,类似于线程池的设计
@HystrixProperty(name = "maxQueueSize",value = "10")
}
)
public License getLicense(String organizationId,String licenseId) {
License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId);
randomlyRunLong();
Organization org = getOrganization(organizationId);
return license
.withOrganizationName( org.getName())
.withContactName( org.getContactName())
.withContactEmail( org.getContactEmail() )
.withContactPhone( org.getContactPhone() )
.withComment(config.getExampleProperty());
}
这一节,就是这个样子