背景(分布式系统面临的问题)
复杂的分布式体系结构的应用程序有数十个依赖关系,每个依赖在某些时候都可能不可避免的出现问题(网络卡顿、程序出错、调用超时,甚至是机房断电),如果多个调用依赖中每个环节都正常,那么服务执行会一切顺利;但是如果其中某个服务出现问题,则可能会引起服务雪崩。
那什么是服务雪崩呢?
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的"扇出”如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”、
对于高流量的应用来说,单-的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
Hystrix是什么?
Hystrix是一个用于处理分 布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下, 不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。”断路器”本身是一种开关装置,当某个服务单元发生故障之后, 通过断路器的故障监控(类似熔断保险丝) ,向调用方返回一个符合预期的、可处理的备选响应(FallBack) ,而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。springcloud社区神仙打架,底下程序员遭殃,Hystrix很优秀,但是目前已经停更进维了,他虽然已经停更了,但是他优秀的设计理念依然被后面的替代框架所沿用,比如resilience4j、sentine。resilience4j在国外用的比较多,sentine在中国用的比较多。具体查看官网
重要的概念
服务降级 fallback
当某个服务单元发生故障之后,向调用方返回一个符合预期的、可处理的备选响应(FallBack) ,而不是长时间的等待或者抛出调用方无法处理的异常;不让客户端等待并立刻返回一个友好的提示,用来兜底的解决方案。什么时候会发生服务降级呢?程序运行异常、超时、服务熔断触发服务降级、线程池/带宽量满了也会导致服务降级。
服务熔断break
类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。处理流程是服务的降级->进而熔断->恢复调用链路
服务限流 flowlimit
秒杀高并发等操作,流量请求突然变多的时候,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,要么排队有序进行处理这些请求,要么友好提示立即返回,这样保证服务正常,限制⼀次只可以有多少请求,不让服务挂了。
基本案例
基本服务
Server 8081
SimulationService.java
@Service
public class SimulationService {
/**
* 模拟正常、迅速的服务
* @param id
* @return
*/
public String simulationSuccess(String id) {
return "thread:" + Thread.currentThread().getName() + " success:" + id;
}
/**
* 模拟耗时的服务
* @param id
* @return
*/
public String simulationTimeOut(String id) {
int timeout = 3;
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "thread:" + Thread.currentThread().getName() + " timeout:" + id;
}
}
SimulationController.java
@RestController
public class SimulationController {
@Autowired
SimulationService simulationService;
@GetMapping("/simulation/hystrix/success/{id}")
public String simulationSuccess(@PathVariable String id){
String simulationSuccess = simulationService.simulationSuccess(id);
return simulationSuccess;
}
@GetMapping("/simulation/hystrix/timeout/{id}")
public String simulationTimeOut(@PathVariable String id){
String simulationTimeOut = simulationService.simulationTimeOut(id);
return simulationTimeOut;
}
}
上面这个案例实现了一个端口为
8081
的服务提供者,实现了一个普通简易的接口,一个模拟耗时操作。当请求simulationSuccess
时,数据能立即返回,当请求simulationTimeOut
的时候,请求需要等待3秒才会返回数据。正常情况下,请求simulationSuccess
会迅速返回结果,但是,当我们用压力测试工具(比如postman、Jmeter)疯狂请求(比如1秒20000的请求)simulationTimeOut
时,我们再去访问simulationSuccess
时,发现结果返回没那么迅速了;如果压力给的足够大,这个服务也会被拖死,导致客户端请求数据出错;出现这种情况的原因是因为tomcat的默认的工作线程数被打满了,没有多余的线程来分解压力和处理。
构造微服务架构
基本环境
- 启动一个Eureka服务注册中心
- 将上述的服务注册进入Eureka
- 服务调用使用OpenFeign
搭建服务消费者 80
具体搭建过程参考我写的Eureka相关文章或者搜索网络上关于Eureka的用法
建立openFeign服务映射接口
@Component
@FeignClient(value = "cloud-eureka-provide-userinfo-service")
public interface SimulationService {
@GetMapping("/simulation/hystrix/success/{id}")
String simulationSuccess(@PathVariable("id") String id);
@GetMapping("/simulation/hystrix/timeout/{id}")
String simulationTimeOut(@PathVariable("id") String id);
}
消费接口编写
@RestController
public class SimulationController {
@Autowired
SimulationService simulationService;
@GetMapping("/consume/hystrix/success/{id}")
public String simulationSuccess(@PathVariable String id){
String simulationSuccess = simulationService.simulationSuccess(id);
return simulationSuccess;
}
@GetMapping("/consume/hystrix/timeout/{id}")
public String simulationTimeOut(@PathVariable String id){
String simulationTimeOut = simulationService.simulationTimeOut(id);
return simulationTimeOut;
}
}
上面我们建立了一个服务消费者来消费8081
提供的服务,端口为80
,正常情况下80
消费8081
的服务是正常的,但是量变容易引起质变,当我们用压力测试工具请求8081
时,这个时候80
去调用8081
服务的时候,响应就会变的非常缓慢,用户体验极差。
正因为有上述故障或不佳表现,才有我们的降级/容错/限流等技术诞生。
如何解决?
问题分析
-
网络/资源拥塞导致服务变慢
:超时策略。 -
程序错误或宕机
:兜底方案
解决方案
8081超时时,为了让调用方不卡死,服务端必须要有服务降级
如果8081宕机或程序出错了,为了让调用方不卡死等待,服务端必须要有服务降级
如果8081提供服务是可接受的,调用方出现故障或者自我要求比服务提供方更加苛刻,客户端自己处理降级。
服务降级
Hystrix提供的服务降级能非常方便的解决上述的问题,Hystrix可用在服务提供方和服务调用方,但是一般来说我们将Hystrix用在服务调用方比较多。
提供方服务降级
服务降级我们用到了
@HystrixCommand
这个注解,官网中提供的实例是继承HystrixCommand
来实现的。
添加依赖
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
启动Hystrix
只需要在主启动类上添加@EnableCircuitBreaker
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class EurekaHystrixProvideApplication8081 {
public static void main(String[] args) {
SpringApplication.run(EurekaHystrixProvideApplication8081.class,args);
}
}
修改SimulationService
- 超时限制
@HystrixCommand(
// 绑定兜底方法 timeoutHandle
fallbackMethod = "timeoutHandle",commandProperties = {
// 下面这个指定的是超时阈值
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String simulationTimeOut(String id) {
int timeout = 5;
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "thread:" + Thread.currentThread().getName() + " timeout:" + timeout;
}
/**
* 兜底的方法,请求超时或者出现异常都会跳转到这个方法
* @param id
* @return
*/
public String timeoutHandle(String id){
return "thread:" + Thread.currentThread().getName() + " request fail, id:" + id;
}
我们请求
8081
的simulationTimeOut
,因为该方法需要5秒才能处理完成,但是我们设置的阈值是3秒,所以程序跳转执行timeoutHandle
方法,并返回类似thread:HystrixTimer-4 request fail, id:1
,从线程名也说明了fallback
是Hystrix新起的一个线程去执行的。
- 异常限制
@HystrixCommand(
// 绑定兜底方法 timeoutHandle
fallbackMethod = "timeoutHandle",commandProperties = {
// 下面这个指定的是超时阈值
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String simulationTimeOut(String id) {
int a = 100/0;
return "thread:" + Thread.currentThread().getName() + " timeout:" + timeout;
}
/**
* 兜底的方法,请求超时或者出现异常都会跳转到这个方法
* @param id
* @return
*/
public String timeoutHandle(String id){
return "thread:" + Thread.currentThread().getName() + " request fail, id:" + id;
}
请求
simulationTimeOut
时,因为a=100/0
会导致程序出现异常,所以还是会执行timeoutHandle
方法来兜底。
调用方服务降级
添加依赖
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
修改yml
# 开启hystrix
feign:
hystrix:
enabled: true
启动Hystrix
启动类上添加
@EnableHystrix
注解即可
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class EurekaHystrixConsumeApplication80 {
public static void main(String[] args) {
SpringApplication.run(EurekaHystrixConsumeApplication80.class,args);
}
}
修改SimulationController
@GetMapping("/consume/hystrix/timeout/{id}")
@HystrixCommand(
// 绑定兜底方法 timeoutHandle
fallbackMethod = "timeoutHandle",commandProperties = {
// 下面这个指定的是超时阈值
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String simulationTimeOut(@PathVariable String id){
String simulationTimeOut = simulationService.simulationTimeOut(id);
return simulationTimeOut;
}
/**
* 兜底方法,异常或者超时都会触发该方法
* @param id
* @return
*/
public String timeoutHandle(String id){
return "thread:" + Thread.currentThread().getName() + " request fail, id:" + id;
}
调用方调用
simulationtimeOut
方法后,因为发生异常,所以会执行兜底的方法,会返回thread:hystrix-SimulationController-2 request fail, id:12
。从线程名也能看出Hystrix也是创建新的线程来执行兜底方法的。
全局fallback
使用
@DefaultProperties(defaultFallback="")
在调用类头部添加上来执行默认的兜底方法,如果@HystrixCommand
指定了fallback
的话就执行它指定的,如果没有,就执行默认的,避免了代码的膨胀,合理的减少了代码量,让通用的和独享的各自分开。
@RestController
@DefaultProperties(defaultFallback = "globalHandle")
public class SimulationController {
@Autowired
SimulationService simulationService;
@GetMapping("/consume/hystrix/success/{id}")
@HystrixCommand
public String simulationSuccess(@PathVariable String id){
String simulationSuccess = simulationService.simulationSuccess(id);
return simulationSuccess;
}
@GetMapping("/consume/hystrix/timeout/{id}")
@HystrixCommand(
// 绑定兜底方法 timeoutHandle
fallbackMethod = "timeoutHandle",commandProperties = {
// 下面这个指定的是超时阈值
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String simulationTimeOut(@PathVariable String id){
String simulationTimeOut = simulationService.simulationTimeOut(id);
return simulationTimeOut;
}
/**
* 兜底方法,异常或者超时都会触发该方法
* @param id
* @return
*/
public String timeoutHandle(String id){
return "thread:" + Thread.currentThread().getName() + " request fail, id:" + id;
}
/**
* 全局服务降级兜底的方法
* @param id
* @return
*/
public String globalHandle(String id){
return "thread:" + Thread.currentThread().getName() + " globalHandle, id:" + id;
}
}
feign的服务降级处理
该方式可以针对某个服务来编写服务降级处理,当面对服务宕机的时候,这个时候就会进行服务降级。
上面我们知道了80
上基于openFeign是编写了接口类SimulationService
,我们实现该接口编写SimulationServiceFallBack
,这个类就可以统一的为这个类进行异常处理。
-
SimulationServiceFallBack
@Component public class SimulationServiceFallBack implements SimulationService { @Override public String simulationSuccess(String id) { return "---simulationSuccess fail-----"; } @Override public String simulationTimeOut(String id) { return "---simulationTimeOut fail-----"; } }
-
修改SimulationService
@Component @FeignClient(value = "cloud-eureka-provide-userinfo-service",fallback = SimulationServiceFallBack.class) public interface SimulationService { @GetMapping("/simulation/hystrix/success/{id}") String simulationSuccess(@PathVariable("id") String id); @GetMapping("/simulation/hystrix/timeout/{id}") String simulationTimeOut(@PathVariable("id") String id); }
这样就针对某个服务做了服务降级处理,这样的方式适用于Feign的服务调用的方式,只需要在编写的服务映射接口上绑定
fallback
即可。这样的方式在不改动接口的情况下,又做了服务降级处理,高内聚低耦合,让代码更加的层次分明。让客户端在服务端不可用的情况下,也会获得提示,而不是耗死服务。
服务熔断
什么是断路器?
类比家里电路中的保险丝。
什么是熔断?
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。在Spring Cloud框架里,熔断机制通过Hystrix实现。 Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand
。 参考大神论文
实现案例
这里我们修改
8081
-
simulationService
... /** * 下面是服务熔断测试方法 * @param id * @return */ @HystrixCommand(fallbackMethod = "circuitBreakerFallBack",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") // 失败率达到60%的时候开启熔断 }) public String simulationCircuitBreaker(Integer id){ if (id<0){ throw new RuntimeException("id 必须大于等于 0"); } return "simulationCircuitBreaker:"+ UUID.randomUUID().toString(); } /** * 兜底方法 * @param id * @return */ public String circuitBreakerFallBack(Integer id){ return "circuitBreakerFallBack:"+ UUID.randomUUID().toString(); }
-
simulationController
... /** * 服务熔断测试 * @param id * @return */ @GetMapping("/simulation/hystrix/circuitBreaker/{id}") public String simulationCircuitBreaker(@PathVariable Integer id){ String simulationCircuitBreaker = simulationService.simulationCircuitBreaker(id); return simulationCircuitBreaker; }
我们使用
@HystrixCommand
来配置服务熔断,上面的配置可选项可参考HystrixCommandProperties
这个类,该实例的配置的效果是10s内如果错误率达到60%会发生服务熔断,如果请求次数超过10次才会开启熔断
当我们请求http://localhost:8081/simulation/hystrix/circuitBreaker/1
的时候,服务执行正常,当我们传入id为负数时,例如http://localhost:8081/simulation/hystrix/circuitBreaker/-10
的时候,服务降级,执行circuitBreakerFallBack
方法,当我们在短时间内,这种请求出错(降级)过多的时候,会发生服务熔断
,这时我们再传入正数id,发现请求依然是降级的,但是发出正确请求一段时间之后,返回会正常,这就是服务的降级->进而熔断->恢复调用链路
,也是Hystrix的强大之处。
熔断的类型
- 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入熔断状态
- 熔断关闭:熔断关闭不会对服务进行熔断
- 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
官方流程
详细参考官网
请求流程
- 构造一个
HystrixCommand
或HystrixObservableCommand
对象- 执行命令
- 响应是否已缓存?
- 电路开路了吗?
- 线程池/队列/信号量是否已满?
HystrixObservableCommand.construct()
要么HystrixCommand.run()
- 计算电路健康
- 获取后备
- 返回成功的回应
熔断流程
- 假设电路上的音量达到某个阈值(
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
)...- 并假设误差百分比超过阈值误差百分比(
HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
)...- 然后,断路器从转换
关闭
为开启
。- 当它断开时,它会使针对该断路器的所有请求短路。
- 经过一段时间(
HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
)后,下一个单个请求被允许通过(这是半开
状态)。如果请求失败,断路器将开启
在睡眠窗口期间返回到该状态。如果请求成功,断路器将切换到,关闭状态
并且再次循环步骤一。
可视化仪表盘
除了隔离依赖服务的调用以外,Hystrix还提供 了准实时的调用监控(HystrixIDashboard) ,Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。
搭建 DashBoard 模块
这里yml文件没有什么需要配置的,就是指定端口,这里我指定的端口是9000
pom
org.springframework.cloud
spring-cloud-starter-netflix-hystrix-dashboard
org.springframework.boot
spring-boot-starter-actuator
改启动
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashBoard9000 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashBoard9000.class,args);
}
}
对监控服务的要求
所有需要监控的服务都需要配置
spring-boot-starter-actuator
依赖
监控服务改造
-
添加依赖
org.springframework.boot spring-boot-starter-actuator -
主启动上添加
ServletRegistrationBean
@SpringBootApplication @EnableDiscoveryClient @EnableCircuitBreaker public class EurekaHystrixProvideApplication8081 { public static void main(String[] args) { SpringApplication.run(EurekaHystrixProvideApplication8081.class,args); } /** * 该配置为了Hystrix服务监控,与服务本身无关,是springcloud升级后的坑 * 主要因为ServletRegistrationBean中springboot的默认路径不是"/hystrix.stream" * @return */ @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; } }
web配置
Hystrix DashBoard 服务搭建好之后,我们可以浏览器访问
http://localhost:9000/hystrix
打开
地址栏输入需要监控的服务地址+/hystrix.stream
,例如http://localhost:8081/hystrix.stream
点击Monitor Stream
进入监控页面
自此,Hystrix的大部分功能就叙述完了,更多信息可以 参考官网