这篇文章选自
John Camell的《Spring Micoservices IN ACTION》,之前写过几篇Spring Cloud的文章,因为都是看完,动手操作过后,在去写心得。但是写的质量真的很差,总结、分析都不是很到位。这次直接将书中某一章节照搬出来,分享给大家,这本书非常推荐大家去看,真的很不错的。
什么是熔断器
在电力系统中,熔断器将检测是否有过多电流流过电线,如果熔断器检测到问题,他将断开与电力的其他的连接,并保护下游部分不被烧毁。有了软件熔断器,当远程服务被调用时,熔断器将监视这个调用,如果调用时间太长,熔断器将会介入中继调用。此外,熔断器将监视所有对远程资源的调用,如果对每一个远程资源的调用失败次数足够多,那么熔断器实现就会出现并采取快速失败,阻止将来对调用失败的远程资源。
熔断器为远程调用提供的关键能力
- 快速失败: 当远程服务处于降级状态时,应用程序将会快速失败,并防止通常会拖垮整个应用的资源消耗尽问题的出现。在大多数中断情况下,最好时部分服务关闭而不是完全关闭。
- 优雅地失败: 通过超时和快速失败,熔断器模式使用应用程序开发人员有能力优雅地失败,或寻求替代机制来执行用户的意图。例如,如果用户尝试从一个数据源检索数据,并且该数据源正经历服务降级,那么应用程序开发人员可以尝试从其他地方检索该数据。
- 无缝恢复: 有了熔断器模式作为中介,熔断器可以定期检查所有请求的资源是否重新上线,并在没有人为干预的情况下重新允许对该资源进行访问。在大型的基于云的应用程序中运行着百个服务,这种优雅的恢复能力非常重要。因为他可以显著减少恢复所需的时间,并大大减少因疲劳的运维人员或应用工程时直接干预恢复服务(重新启动失败的服务)而造成更严重问题的风险。
Hystrix
搭建工程,引入依赖
project(":hystrix-core"){
dependencies{
compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
compile('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')
compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix')
}
}
配置文件application.yml
server:
port: 9080
spring:
application:
name: hystrix
eureka:
client:
service-url:
defaultZone: http://localhost/eureka #服务发现物理路径
启动类配置
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker //引导Hystrix熔断器使用
public class HystrixApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixApplication.class, args);
}
@LoadBalanced
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
调用服务
@Service
public class UserService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "error")
public User findOne() {
User user = restTemplate.getForObject("http://app/user/1", User.class);
return user;
}
public User error() {
User user = new User();
user.setEmail("[email protected]");
user.setName("error");
user.setId(3);
return user;
}
}
controller代码
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("user")
public User getUser() {
return userService.findOne();
}
}
访问http://localhost:9080/user 出现服务异常,或者超时的情况,会直接返回error()方法内的后备数据处理。
Spring Cloud使用@HystrixCommand注解来将Java类方法标记为有Hystrix熔断器进行管理,它将动态生成一个代理,该代理将包装该方法,并通过专门用于远程调用的线程池来管理所有的调用。每当调用的时间超过1000ms时,熔断器会中断对包装方法调用,使用error的方法进行失败处理。
如果觉得熔断器的默认时间不合理,可以根据自身的业务需求,自行定制超时时间
@HystrixCommand(fallbackMethod = "error",
commandProperties = {@HystrixProperty(name =
"execution.isolation.thread.timeoutInMilliseconds", value = "2000")} ) //设置超时时间2000ms
public User findOne() {
User user = restTemplate.getForObject("http://app/user/1", User.class);
return user;
}
舱壁模式?
舱壁模式时建立在造船的概念基础上。采用舱壁设计,一艘船被划分为完全隔离和防水的间隔,这就是舱壁。即使 船体被击穿,由于船被划分为水密舱(舱壁),舱壁会将水限制在被击穿的船的区域内,防止整艘船满水并沉船。
同样的概念可以应用于必须与多个远程资源交互的服务。通过应用舱壁模式,可以把远程资源的调用飞到线程池中,并降低一个缓慢的远程资源调用拖垮整个应用程序的风险。线程池充当服务的“舱壁”。每一个资源都是隔离的,并分配给线程池。如果一个服务响应缓慢,那么这种服务调用的线程就会饱和并停止处理请求,而对其他的服务调用则不会变得饱和。
在基于微服务的应用程序中,开发人员通常需要调用多个微服务来完成特定的任务,在不使用舱壁模式的情况下,这些调用默认是使用同一批线程来执行调用,这些线程是为了处理整个Java容器的请求而预留的。在存在大量请求的情况下,一个服务出现性能问题会导致Java容器所有线程被刷爆并等待处理,同时堵塞新请求,最终导致Java容器崩溃。舱壁模式将远程资源调用隔离在他们自己的线程中,以便可以控制单个表现不佳的服务,而不会使该容器崩溃。
在默认情况下,所有的Hystrix命令都将共享同一个线程池来处理请求。这个线程池将有10个线程来处理远程服务调用,而这些远程服务调用可以是任何东西,包括REST服务,数据库等待。
在应用程序访问少量的远程资源时,这些模式运行良好,并且各个服务的调用分布相对均匀。问题是,如果某些服务具有比其他服务高得多得得请求量或更长得完成时间。那么最终可能会导致Hystrix线程池中得线程耗尽。
Hystrix提供另一个分离线程得机制,在不同得远程资源调用之间创建舱壁。
要实现隔离得线程池,我们需要使用
@HystrixCommend
注解得其他属性,接下来得代码将完成以下操作。
@HystrixCommand(fallbackMethod="error",
threadPoolKey="hystrixThreadPool", //自定义线程名称
threadPoolProperties= {
@HystrixProperty(name = "coreSize",value = "30"), //定义线程中线程得最大数量
@HystrixProperty(name = "maxQueueSize",value = "10") //等待线程处理队列得数量
})
public User findOne() {
User user = restTemplate.getForObject("http://app/user/1", User.class);
return user;
}
threadPoolkey
属性告诉Hystrix,我们想要建立一个新的线程。如果在线程池中没有设置任何一步得值,Hystrix会使用threadPoolkey属性中得名称搭建一个线程池,并使用所有得默认值来对线程池进行配置。
要定制线程池,使用@HystrixCommend上threadPoolProperties属性。使用HystrixProperty对象得数组,控制线程池得行为,coreSize可以设置线程池的大小。maxQueueSize设置队列的大小,一旦请求数超过队列大小,对线程池任何请求都将失败,直到队列中有空间。如果队列数设置为小于0,则会使用Java SynchronousQueue(同位队列)在保存所有的请求,如果设置大于1的话,则使用Java LinkedBlockingQueue。
到底怎么自定义线程池的大小呢,Netflix 推荐以下公式:
服务在健康状态时每秒支撑的最大请求数 x 第99百分位延迟的时间(秒)+用于缓存的少量额外线程
定制熔断功能
Hystrix会监控调用失败的次数,如果调用失败的次数足够多,那么Hystrix会在发送请求之前,通过调用失败来自动阻止未来的调用到达服务。这要做有两个原因,如果远程服务有性能问题,那么快速失败将防止应用程序等待调用超时,第二快速失败和阻止来自服务客户端的调用有助于苦苦挣扎的服务保持其负载,而不会崩溃。快速失败给了性能下降的系统一些时间去经行恢复。
每当Hystrix命运遇到服务错误时,他将开始一个10s的计数器,用于检查服务调用失败的频率。如果调用失败次数少于这个窗口需要发生最小调用次数,那么即使有几个调用失败,Hystrix不会采取行动,默认配置是在10钟之内,如果失败的次数在20以上,或者失败的频率超过50% ,直接快速失败。
如果超过错误的阀值,Hystirix将会跳闸,防止更多的调用访问远程资源,如果远程调用失败百分比为达到要求的阀值,并且10s窗口已过去,Hystrix将重置熔断器的统计信息。
当Hystrix已经跳闸了,它会尝试启动一个新的活动窗口,每隔5s,会让一个调用到达这个苦苦挣扎的服务,如果调用成功,Hystrix将重置熔断器并,重新开始调用通过,如果调用失败,Hysttix将保存熔断器断开,并在下一个5s继续尝试。
自定义熔断的参数
@HystrixCommand(fallbackMethod="error",
threadPoolKey="hystrixThreadPool",
threadPoolProperties= {
@HystrixProperty(name = "coreSize",value = "30"),
@HystrixProperty(name = "maxQueueSize",value = "10")
},
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), //开始检查是否跳闸之前,窗口必须处理最小参数,默认值是20
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "75"),//窗口必须达到故障百分比,默认值50
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "7000"), //熔断之后,尝试进行服务之间调用之前要等待 的时间,默认值5000
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value="15000"), //收集和监控服务调用的统计信息,默认值10000ms
@HystrixProperty(name = "metrics.rollingStats.numBuckets",value = "3") //监控窗口中维护的度量的数量,监控窗口内的bucket越多,监控故障的时间越低
})
public User findOne() {
User user = restTemplate.getForObject("http://app/user/1", User.class);
return user;
}
Hystrix线程上下文
当一个@HystrixCommend被执行时,他可以使用两种不同的隔离策略—THREAD(线程)和
SEMAPHORE(信号量)。在默认情况下,Hystrix以THREAD隔离策略运行。用于保护调用这个Hystrix命令都在一个单独的线程池中运行,该线程池不与父线程共享他的上下文。这意味卓Hystrix可以在他的控制下中继线程的执行,而不必担心中继与执行原始调用的线程父线程相关的其他活动。
通过基于SEMAPHORE的隔离,Hystrix管理有@HystrixCommand注解保护的分布式调用,而不需要启动一个线程,并且如果调用超时,就会中断父线程,在同步容器服务器中,中断父线程将导致异常的抛出,开发人员事先线程未考虑到的话,不能捕获到这个异常。会带来意想不到的后果。
如果想更改策略:
@HystrixProperty(name = "execution.isolation.strategy",value = "SEMAPHORE")
在默认情况下,Hystrix团队建议开发人员对大多数命令使用默认的THREAD隔离策略,保持开发人员和父线程之间更高层次隔离。THREAD隔离比SEMAPHORE隔离更重,SEMAPHORE隔离跟轻量级,比较适用于服务量很大,并且使用异步I/O编程模式就是Netty这样的异步I/O容器。