概述
一、分布式系统面临着什么
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务雪崩
多个微服务之间调用的时候,如果某个链路上某个微服务的调用响应时间过长或者不可用,对微服务的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
二、Hystrix是什么
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,类似断路器”当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
概念
1.服务降级
当服务器出现问题,或者处理不过来时,不让客户端等待,立即返回一个友好提示,比如服务器繁忙请稍后再试,callback
2.服务熔断
类似保险丝,当服务器达到最大处理数时,直接拒绝访问,然后调用服务降级的方法,返回友好提示
3.服务限流
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
案例
1.搭建服务cloud-provider-hystrix-payment8001
2.配置pom
<!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!--eureka client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
3.配置yml
server: port: 8001 spring: application: name: cloud-provider-hystrix-payment eureka: client: register-with-eureka: true fetch-registry: true service-url: #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka defaultZone: http://localhost:7001/eureka
4.配置主启动
@SpringBootApplication @EnableEurekaClient public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class,args); } }
5.业务类以及service
controller
@RestController @Slf4j public class PaymentController { @Autowired private PaymentServiceImpl paymentService; @Value("${server.port}") private String serverPort; @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentService.paymentInfoOK(id); log.info("****result: "+result); return result; } @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException { String result = paymentService.paymentInfoTimeOut(id); log.info("****result: "+result); return result; }
service
@Service public class PaymentServiceImpl implements PaymentService { /** * 正常访问,一切OK * * @param id * @return */ public String paymentInfoOK(Integer id) { return "线程池:" + Thread.currentThread().getName() + "paymentInfo_OK,id: " + id + "\t" + "O(∩_∩)O"; } /** * 超时访问,演示降级 * * @param id * @return */ public String paymentInfoTimeOut(Integer id) { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOut,id: " + id + "\t" + "O(∩_∩)O,耗费2秒"; } }
6.测试
超时访问
7.高并发测试
1.配置jmeter 20000并发
测试发现访问http://localhost:8001/payment/hystrix/ok/1 也在转圈
是因为tomcat的工作线程被占满了,没有多余的线程来分解压力
8.加入80客户端访问
8.1.配置(cloud-consumer-feign-hystrix-order80)
8.2.配置pom
<!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!--eureka client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
8.3.配置yml
spring: application: name: cloud-order-feign-hystrix-server server: port: 80 eureka: client: service-url: defaultZone: http://localhost:7001/eureka/ register-with-eureka: false ribbon: #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 ReadTimeout: 6000 #指的是建立连接后从服务器读取到可用资源所用的时间 ConnectTimeout: 5000
8.4.配置主启动
@SpringBootApplication @EnableFeignClients public class OrderHystrixMain80 { public static void main(String[] args) { SpringApplication.run(OrderHystrixMain80.class,args); } }
8.5.业务类以及service
controller
@RestController public class OrderHystirxController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_OK(id); return result; } @GetMapping("/consumer/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; } }
service
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT") @Component public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") String paymentInfo_OK(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") String paymentInfo_TimeOut(@PathVariable("id") Integer id); }
9.服务降级实现
9.1.故障现象及原因
8001同一层次的其它接口服务被困死,因为tomcat线程池里面的工作线程已经被挤占完毕
80此时调用8001,客户端访问响应缓慢,转圈圈
9.2.如何解决
9.2.1.方案
1.服务超时(8001),调用者(80)不能一直卡死等待,需要服务降级
2.服务器宕机了,调用者不能一直卡死等待,需要服务降级
3.服务器ok,调用者自身故障或自我要求(自身等待时间小于服务器响应时间),自己处理降级
9.2.2.降级配置 @HystrixCommand
9.2.1.先配置服务提供方8001,设置超时峰值,峰值内可以正常运行,峰值外或运行异常,做服务降级走fallback
9.2.2.配置service 一旦调用服务方法失败并抛出了错误信息后,
会自动调用@HystrixCommand标注好的
fallbackMethod调用类中的指定方法//峰值设置3s @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000") }) public String paymentInfoTimeOut(Integer id) { //超时 // try { // TimeUnit.SECONDS.sleep(5); // } catch (InterruptedException e) { // e.printStackTrace(); // } //运行异常 int i = 10/0; return "线程池:" + Thread.currentThread().getName() + "====paymentInfo_TimeOut,id: " + id + "\t" + "O(∩_∩)O,耗费5秒"; } //回调 public String paymentInfo_TimeOutHandler(Integer id) { return "线程池:" + Thread.currentThread().getName() + "====paymentInfo_TimeOut,id: " + id + "\t" + "系统运行异常,亲稍后再试"; }
9.2.3.配置启动类服务发现 @EnableCircuitBreaker
@SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class,args); } }
9.3. 测试
9.4.结论
ps1:一般配置在客户端,客户端启动类注解是@EnableHystrix 代码如下: @SpringBootApplication @EnableFeignClients @EnableHystrix public class OrderHystrixMain80 { public static void main(String[] args) { SpringApplication.run(OrderHystrixMain80.class,args); } } ps2:在峰值时间内调用且代码没有错误,但是一直报错timeout, 可能原因如下: 1.没有配置hystrix超时时间,可以设置 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000//熔断器的超时时长默认1秒,最常修改的参数 2.feign启用了熔断器,默认是false
9.5.问题
1.每个方法都要配置一个回调函数,导致代码膨胀严重,使用@DefaultProperties 注解配置全局默认回调
@RestController @Slf4j @DefaultProperties(defaultFallback = "paymentGlobalcallback") public class OrderHystirxController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/timeout/{id}") // @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = { // @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3500") // }) @HystrixCommand public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { // int i = 10/0; System.out.println("================================================"); String result = paymentHystrixService.paymentInfo_TimeOut(id); System.out.println(result); return result; } public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) { return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o"; } public String paymentGlobalcallback() { return "全局错误处理方法"; }
2.统一回调跟接口方法混合在一起,导致代码混乱,耦合度高
编写PaymentFallbackService 实现 PaymentHystrixService 在PaymentFallbackService内设置callback降级返回
@Component public class PaymentFallbackService implements PaymentHystrixService { @Override public String paymentInfo_OK(Integer id) { return "服务调用失败,提示来自:cloud-consumer-feign-order80 的 paymentInfo_OK "; } @Override public String paymentInfo_TimeOut(Integer id) { return "服务调用失败,提示来自:cloud-consumer-feign-order80 的 paymentInfo_TimeOut "; } }
在PaymentHystrixService 内配置回调的class
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class) @Component public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") String paymentInfo_OK(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") String paymentInfo_TimeOut(@PathVariable("id") Integer id); }
这样既解决了代码膨胀也解决了代码混乱,统一管理服务降级返回
10.服务熔断实现
10.1 服务熔断是什么
类似于保险丝,当服务出现问题或者高并发时,自动拒绝请求,调用服务降级,并且会在服务恢复的时候恢复正常,连接通路
10.2.实现
//配置在类上可以全方法通用 public class PaymentServiceImpl implements PaymentService { //服务熔断================================================================================================================================ @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = { //开启熔断器 @HystrixProperty(name = "circuitBreaker.enabled",value = "true"), // 时间区间内请求次数 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), //时间区间 多少毫秒内 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), //请求成功率 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"), }) public String paymentCircuitBreaker(@PathVariable("id") Long id){ if (id < 0 ){ throw new RuntimeException("***********id 不能为负数"); } String s = IdUtil.simpleUUID(); return Thread.currentThread().getName()+ "\t 调用成功 流水号:" + s; } public String paymentCircuitBreaker_fallback(@PathVariable("id") Long id){ return "id 不能是负数,稍后再试"; } }
10.3.测试
多次错误,然后慢慢正确,发现刚开始不满足条件,就算是正确的访问地址也不能进行
10.4.总结
10.4.1 熔断器启用或关闭条件
- 在规定时间区间内达到访问次数阈值上线,并且超出错误率阈值时启用
- 当开启时所有请求都不会转发,直接进入服务降级
- 开启一段时间后(一般5S),会进入半开启状态,会让其中一个请求进行转发,如果成功,断路器会关闭,如果失败则继续开启,重复2和3
ps:熔断器所有配置
//========================All @HystrixCommand(fallbackMethod = "str_fallbackMethod", groupKey = "strGroupCommand", commandKey = "strCommand", threadPoolKey = "strThreadPool", commandProperties = { // 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离 @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"), // 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数) @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"), // 配置命令执行的超时时间 @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"), // 是否启用超时时间 @HystrixProperty(name = "execution.timeout.enabled", value = "true"), // 执行超时的时候是否中断 @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"), // 执行被取消的时候是否中断 @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"), // 允许回调方法执行的最大并发数 @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"), // 服务降级是否启用,是否执行回调函数 @HystrixProperty(name = "fallback.enabled", value = "true"), // 是否启用断路器 @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候, // 如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"), // 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过 // circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50, // 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"), // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后, // 会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态, // 如果成功就设置为 "关闭" 状态。 @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"), // 断路器强制打开 @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"), // 断路器强制关闭 @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"), // 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间 @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"), // 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据 // 设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。 // 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常 @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"), // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。 @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"), // 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。 @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"), // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。 @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"), // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数, // 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行, // 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。 @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"), // 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。 @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"), // 是否开启请求缓存 @HystrixProperty(name = "requestCache.enabled", value = "true"), // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中 @HystrixProperty(name = "requestLog.enabled", value = "true"), }, threadPoolProperties = { // 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量 @HystrixProperty(name = "coreSize", value = "10"), // 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列, // 否则将使用 LinkedBlockingQueue 实现的队列。 @HystrixProperty(name = "maxQueueSize", value = "-1"), // 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。 // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue // 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"), } ) public String strConsumer() { return "hello 2020"; } public String str_fallbackMethod() { return "*****fall back str_fallbackMethod"; }
工作流程
1创建 HystrixCommand(用在依赖的服务返回单个操作结果的时候) 或 HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候) 对象。2命令执行。其中 HystrixComand 实现了下面前两种执行方式;而 HystrixObservableCommand 实现了后两种执行方式:execute():同步执行,从依赖的服务返回一个单一的结果对象, 或是在发生错误的时候抛出异常。queue():异步执行, 直接返回 一个Future对象, 其中包含了服务执行结束时要返回的单一结果对象。observe():返回 Observable 对象,它代表了操作的多个结果,它是一个 Hot Obserable(不论 "事件源" 是否有 "订阅者",都会在创建后对事件进行发布,所以对于 Hot Observable 的每一个 "订阅者" 都有可能是从 "事件源" 的中途开始的,并可能只是看到了整个操作的局部过程)。toObservable(): 同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有 "订阅者" 的时候并不会发布事件,而是进行等待,直到有 "订阅者" 之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程)。
3若当前命令的请求缓存功能是被启用的, 并且该命令缓存命中, 那么缓存的结果会立即以 Observable 对象的形式 返回。
4检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到 fallback 处理逻辑(第 8 步);如果断路器是关闭的,检查是否有可用资源来执行命令(第 5 步)。
5线程池/请求队列/信号量是否占满。如果命令依赖服务的专有线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满, 那么 Hystrix 也不会执行命令, 而是转接到 fallback 处理逻辑(第8步)。
6Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。HystrixCommand.run() :返回一个单一的结果,或者抛出异常。HystrixObservableCommand.construct(): 返回一个Observable 对象来发射多个结果,或通过 onError 发送错误通知。
7Hystrix会将 "成功"、"失败"、"拒绝"、"超时" 等信息报告给断路器, 而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行 "熔断/短路"。
8当命令执行失败的时候, Hystrix 会进入 fallback 尝试回退处理, 我们通常也称该操作为 "服务降级"。而能够引起服务降级处理的情况有下面几种:第4步: 当前命令处于"熔断/短路"状态,断路器是打开的时候。第5步: 当前命令的线程池、 请求队列或 者信号量被占满的时候。第6步:HystrixObservableCommand.construct() 或 HystrixCommand.run() 抛出异常的时候。9当Hystrix命令执行成功之后, 它会将处理结果直接返回或是以Observable 的形式返回。
tips:如果我们没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常, Hystrix 依然会返回一个 Observable 对象, 但是它不会发射任何结果数据, 而是通过 onError 方法通知命令立即中断请求,并通过onError()方法将引起命令失败的异常发送给调用者。
服务监控 Hystrix Dashboard
查看服务状态监控,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。
新建cloud-consumer-hystrix-dashboard9001
配置pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
配置yml
配置启动类,新注解 EnableHystrixDashboard
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class,args);
}
}
所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置
<!-- actuator监控信息完善 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
启动cloud-consumer-hystrix-dashboard9001该微服务后续将监控微服务8001
修改cloud-provider-hystrix-payment8001
注意:新版本Hystrix需要在主启动类MainAppHystrix8001中指定监控路径
1.添加@EnableCircuitBreaker 2.配置服务监控 @SpringBootApplication() @EnableEurekaClient @EnableCircuitBreaker public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class,args); } /** *此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑 *ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream", *只要在自己的项目里配置上下面的servlet就可以了 */ @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } }
如果没配可能会报错 Unable to connect to Command Metric Stream.
启动集群填写监控地址信息http://localhost:8001/hystrix.stream 并查看
信息带表的含义