其实我们讲过所有的Spring Cloud知识,都为了解决两个问题:一个是高并发,一个是高可用。解决高并发&高可用问题的方法有很多,比如:
但是无论你怎么升级硬件、改善架构、改善算法,永远都会有上限,也永远。服务能力就是会存在某一时间段内无法达到高可用的要求,甚至崩溃。
在分布式服务的系统内,很多的用户请求在系统内部都是存在级联式远程调用的。如下图所示:一次请求先后经过Service A、B、C、D,如果此时服务D发生异常,长时间无法响应或者根本不响应,将导致Service C服务调用无法正常响应,进而导致Service B和Service A的响应也出现问题。这种因为服务调用链中某一个服务不可达或超时等异常情况,导致其上游的服务也出现响应异常或者崩溃。当这种情况在高并发环境下就会导致整个系统响应超时、资源等待耗尽,这种现象就是“服务雪崩”。
当一个服务Service1需要在其方法实现中,调用多个服务提供者时,其中一个服务不可达或者超时的的情况发生,也会导致请求失败。在高并发的环境下,这个问题会更加凸显,也会导致整个微服务系统资源出现等待、无法释放的情况。从而产生服务雪崩。
服务重试机制也会产生服务雪崩 很多朋友在遇到上面的问题时,很自然的想到我们之前为大家讲过的服务请求重试机制(Ribbon和OpenFeign都可以实现服务的请求失败重试)。
理解“熔断”这个词的由来,可以帮助我们跟好的理解“熔断”在微服务体系应用的意义。
服务熔断:指的是在服务提供者的错误率达到一定的比例之后, 断路器就会熔断一段时间,不再去请求服务提供者,从而避免上游服务被拖垮,进而达到保护整体系统可用性的目的。
熔断恢复:熔断时间过了以后再去尝试请求服务提供者,一旦服务提供者的服务能力恢复,请求将继续可以调用服务提供者,此过程完全不需认为参与。
上图是“断路器”的状态转换图
通过上面的讲解,相信大家已经知道了服务熔断的含义及意义是什么。但是明显遗留了一个问题:服务熔断之后就不在去请求服务调用者原本的方法,那该去请求谁?总不能没有响应吧!这就需要使用到“服务降级”机制了。
白话说服务降级:服务降级是一种兜底的服务策略,体现了一种“实在不行就怎么这么样”的思想。想去北京买不到飞机票,实在不行就开车去吧;感冒了想去看病挂不上号,实在不行就先回家吃点药睡一觉吧;实在不行之后的处理方法,被称为fallback方法。
当服务提供者故障触发调用者服务的熔断机制,服务调用者就不再调用远程服务方法,而是调用本地的fallback方法。此时你需要预先提供一个处理方法,作为服务降级之后的执行方法,fallback返回值一般是设置的默认值或者来自缓存。
除了可以在服务调用端实现服务降级,还可以在服务提供端实现服务降级。实际上在大型的微服务系统中,服务提供者和服务消费者并没有严格的区分,很多的服务既是提供者,也是消费者。
服务提供者原本的处理请求方法是AMethod(如运行时异常),已经不能响应请求,实在不行了就去执行预先定义好的fallback方法。fallback返回值一般是设置的默认值或者来自缓存。
当然,除了服务熔断会触发服务降级和程序运行时异常,还有其他几种异常也可以触发服务降级
服务限流:通过对并发访问/请求进行限速或者一个时间窗口内的请求数量进行限制来保护系统,一旦达到限制速率则可以拒绝服务。拒绝服务之后,可以有如下的处理方式:
Hystrix是一个用于微服务系统的延迟和容错库,旨在远程系统、服务和第三方库出现故障的时候,隔离服务之间的接口调用,防止级联故障导致服务雪崩。
笔者强烈建议:Spring Cloud微服务系统使用sentinel代替hystrix。除非你的既有项目代码改造难度比较大,新项目一定要用Sentinel。
在aservice-rbac和aservice-sms微服务项目中通过maven坐标引入hystrix。在旧的版本中引入hystrix使用spring-cloud-starter-hystrix
,但在笔者使用的Spring Cloud Hoxton.SR3版本中要使用spring-cloud-starter-netflix-hystrix
。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<!--artifactId>spring-cloud-starter-hystrix 新版不要用这个</artifactId-->
</dependency>
在Spring Cloud项目最开始的时候几乎所有组件都是netflix公司贡献的,随着netflix公司对spring cloud社区的支持减弱,更多的厂商加入spring cloud开源社区。Spring Cloud社区开始通过maven坐标区别类库,如:
spring-cloud-starter-alibaba-*
、spring-cloud-starter-netflix-*
@EnableCircuitBreaker
注解我们仍然以SystemService的密码重置接口为例,讲解服务熔断配置的代码实现方法。通过在方法上加上HystrixCommand注解和HystrixProperty注解来实现某个方法的服务熔断配置。
@PostMapping(value = "/pwd/reset")
@HystrixCommand(
commandProperties = {
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000") //统计窗口时间
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), //启用熔断功能
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"), //20个请求失败触发熔断
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), //请求错误率超过60%触发熔断
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "300000"),//熔断后开始尝试恢复的时间
}
)
public AjaxResponse pwdreset(@RequestParam Integer userId) {
sysuserService.pwdreset(userId);
return AjaxResponse.success("重置密码成功!");
}
通过上面的配置,我们就可以针对pwdreset实现服务熔断,下一节课我们将针对上面的配置信息进行测试。上面用到的配置项是我们在使用Hystrix进行服务熔断最常用的配置,如果你想了解更多的关于Hystrix的配置,请参考附录(其中一些关于服务降级的配置,我们后面章节还会讲)。
我们可以看到上面的使用注解针对方法进行服务熔断的配置,虽然可以实现功能,但是无疑增大了我们的代码量,而且非常冗余。为了解决这个问题,我们可以使用全局配置来实现:
hystrix:
command:
default:
circuitBreaker:
enabled: true
requestVolumeThreshold: 20
errorThresholdPercentage: 60
sleepWindowInMilliseconds: 300000
全局配置完成之后,想让哪一个方法实现断路器功能,就在哪一个方法上加上注解:
@HystrixCommand
比较好的实践方案是:针对系统内的绝大部分接口调用采用全局配置的方式,针对个别个性化重点业务接口使用注解配置。注解配置属性会覆盖全局配置属性,注解配置的优先级更高。
英文官方文档:https://github.com/Netflix/Hystrix/wiki/Configuration
JMeter是开源软件Apache基金会下的一个性能测试工具,用来测试部署在服务器端的应用程序的性能。模拟用户并发请求的操作,我们为了更好的测试服务熔断,所以使用Jmeter。 https://jmeter.apache.org/download_jmeter.cgi
右键“Test Plan”-Add,新建线程组。一个线程模拟一个用户,新建线程组就是新建一组用户。
模拟时间窗口(Ramp-up period)10秒钟内,执行30次(users)请求,执行1轮(Loop Count)。该条件足以触发我们上一节定义的服务熔断标准。
Jmeter的每个请求的响应结果,可以在这里查看
“/sysuser/pwd/reset”接口测试此前已经多次讲过,这里就不过多介绍了。可以回看《第一个微服务调用》章节
参考下面这张图,理解测试过程。
通过前面章节的讲解,我们都知道当服务熔断被触发之后,我们再次访问会返回如下结果:
这样的响应结果,提供给用户显然是不够友好的。上面的提示信息有两层含义:
当服务提供者故障触发熔断机制,此时你需要预先提供一个处理方法,作为降级后的执行方法一般叫fallback,fallback方法返回值一般是设置的默认值或者来自缓存,或者是一些友好提示信息。
满足下列条件任何一个异常条件,都会产生服务降级
Failure Type | Exception class | Exception.cause | subject to fallback |
---|---|---|---|
FAILURE | HystrixRuntimeException | underlying exception (user-controlled) | YES |
TIMEOUT | HystrixRuntimeException | j.u.c.TimeoutException | YES |
SHORT_CIRCUITED | HystrixRuntimeException | j.l.RuntimeException | YES |
THREAD_POOL_REJECTED | HystrixRuntimeException | j.u.c.RejectedExecutionException | YES |
SEMAPHORE_REJECTED | HystrixRuntimeException | j.l.RuntimeException | YES |
所以我们可以认为:服务降级实际上也是“异常处理”的一种方式,处理的是上面的5种异常。
在上一节服务熔断的代码的基础上加上服务降级方法配置
@HystrixCommand
注解加上属性fallbackMethod
属性
(重要) 为什么在控制层实现服务降级?
此时在服务熔断之后(或者其他的服务降级条件满足之后),我们再次访问“/sysuser/pwd/reset”接口。结果如下,说明执行了本地fallback方法。
第二小节中这种方法级别的服务降级配置方式的缺点十分的明显:那就是我们需要针对方法级别进行服务熔断和服务降级的配置;不只是配置,我们还需要针对每一个方法写fallback方法,无疑很大程度上增加了我们的代码量。那么有没有一种可以全局实现服务降级的配置方式呢?就是下面要为大家介绍的:
@DefaultProperties(defaultFallback = "commonFallbackMethod")
@DefaultProperties
是一个类方法级别的注解 public AjaxResponse commonFallbackMethod() {
return AjaxResponse.error(CustomExceptionType.SYSTEM_ERROR,
"系统繁忙,请稍后再试!");
}
最后在需要进行服务降级后执行fallback的方法的方法上面加上
@HystrixCommand
这样我们实现服务降级的代码就减少了很多,但是仍然存在一个问题让开发者不爽:我们需要在每一个类里面写一个commonFallbackMethod函数,为了降低fallback函数与实际Controller业务处理类的耦合,进一步减少代码的冗余,我们通常是可以定义一个BaseController,然后让其他的Controller类来继承。
public class BaseController {
//通用hystrix回退方法
public AjaxResponse commonFallbackMethod() {
return AjaxResponse.error(CustomExceptionType.SYSTEM_ERROR,
"系统繁忙,请稍后再试!");
}
}
@RestController
public class SmsController {
@GetMapping(value = "/pwd/reset")
@HystrixCommand(fallbackMethod = "fallBack")
public AjaxResponse pwdreset(@RequestParam Integer userId) {
int i=1/0;
return AjaxResponse.success("重置密码成功!");
}
public AjaxResponse fallBack(@RequestParam Integer userId,Throwable e)
{
System.out.println(e);
return AjaxResponse.error(CustomExceptionType.SYSTEM_ERROR, "用户ID为: "+userId+" 错误信息为: "+e.getMessage());
}
}
注意点一: fallback的方法参数和需要降级的方法必须一致,否则会因为方法参数不匹配报错
注意点二:如果想要接收异常,方法的最后一个参数作为异常参数接收处,并且必须使用Throwable来接收异常,否则会因为方法参数不匹配报错
hystrix源码解析——FallbackMethod是如何接收异常的
通过前面几个小节的说明,对于服务降级目前有两种方式:
下面为大家介绍服务降级的另一类方法:在FeignClient上实现服务降级。为什么我称它是另一类方法,而不是另一种方法?因为FeignClient上实现服务降级与上面两种方法的思考的角度是不同的:
feign:
hystrix:
enabled: true
在FeignClient注解增加fallback处理实现类,如:SmsServiceFallback。
@FeignClient(name="ASERVICE-SMS",fallback = SmsServiceFallback.class)
public interface SmsService {
@PostMapping(value = "/sms/send")
AjaxResponse send(@RequestParam("phoneNo") String phoneNo,
@RequestParam("content") String content);
}
书写SmsServiceFallback代码,该类要实现FeignClient注解的接口函数。当使用Feign客户端远程调用SmsService .send方法,如果远程服务不可达(网络不可达或宕机),就会执行SmsServiceFallback.send方法作为fallback。
@Component
public class SmsServiceFallback implements SmsService {
@Override
public AjaxResponse send(String phoneNo, String content) {
return AjaxResponse.error(CustomExceptionType.SYSTEM_ERROR
,"短信发送接口失败!");
}
}
为了让为大家更直观的感受,我们来做一个实验,感受一下远程服务异常传递问题。
public AjaxResponse commonFallbackMethod() {
return AjaxResponse.error(CustomExceptionType.SYSTEM_ERROR,
"系统繁忙,请稍后再试!");
}
代码中的error方法实际上做了几件事情:
sysUserMapper.updateByPrimaryKeySelective
操作成功。
这显然不是我们希望看到的结果。如果只是发短信失败还不是非常要紧,如果是购物网站,订单服务成功了,账务服务失败了,这个影响就大了!我们期望看到的结果是:要么都成功,要么都失败!
问自己几个问题:
所以远程服务降级之后返回的结果是:
也就是说,我们介绍了服务异常传递的两个渠道:一是HTTP状态码,二是HTTP的Response Body。目前我们只能使用第二种渠道传递异常!
所以针对以上的异常传递不到位导致的问题,最简单的处理方式就是:我们在接收到远程服务的响应结果Response Body(对于我们的项目是AjaxResponse)后,判断其内部的状态信息。如果状态信息是业务失败,throw new 自定义异常抛出,触发数据库回滚!
目前很多应用都采用RESTful风格的接口,特点就是
方法就是实现ResponseBodyAdvice接口:对项目的所有的Controller的JSON类型数据响应结果进行二次封装,然后再返回给服务调用端端。
response.setStatusCode(HttpStatus.valueOf(
((AjaxResponse) body).getCode())
);
有了这样一层封装,服务调用端就能根据HTTP状态码判断服务提供者的响应数据是否异常。完整实现如下:
@Component
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
//return returnType.hasMethodAnnotation(ResponseBody.class);
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
//对于JSON类型的响应数据
if(selectedContentType.equalsTypeAndSubtype(
MediaType.APPLICATION_JSON)){
if(body instanceof AjaxResponse){
//如果Controller返回值body的数据类型是AjaxResponse(body instanceof AjaxResponse)
//就将body直接返回
response.setStatusCode(HttpStatus.valueOf(
((AjaxResponse) body).getCode()) //将业务异常状态码赋值给HTTP状态码
);
return body;
}else{
//如果Controller返回值body的数据类型不是AjaxResponse,
//就将body封装为AjaxResponse类型返回,总之要统一数据响应的类型
AjaxResponse ajaxResponse = AjaxResponse.success(body);
response.setStatusCode(HttpStatus.valueOf(
ajaxResponse.getCode()) //将业务异常状态码赋值给HTTP状态码
);
return AjaxResponse.success(body);
}
}
return body;
}
}
全局返回值处理,再实际开发过程中,和swagger,knifej等框架使用时,会存在不兼容的问题,需要手动处理进行兼容或者不使用全局返回值处理,而使用@ResponseStatus注解加在方法上,决定当前方法的响应的状态码
除了上一节为大家介绍的异常拦截处理方式,还有另外一种异常的拦截处理方式:那就是使用Feign的ErrorDecoder进行异常信息转换。我们可以在FeignClient端的ErrorDecoder方法中将AjaxResponse转为RuntimeException抛出!
以下6种异常是Hystrix引入的异常:
Failure Type | Exception class | Exception.cause | subject to fallback |
---|---|---|---|
FAILURE | HystrixRuntimeException | 程序异常underlying exception (user-controlled) | YES |
TIMEOUT | HystrixRuntimeException | 超时异常 j.u.c.TimeoutException | YES |
SHORT_CIRCUITED | HystrixRuntimeException | 熔断异常 j.l.RuntimeException | YES |
THREAD_POOL_REJECTED | HystrixRuntimeException | 线程池满载拒绝异常j.u.c.RejectedExecutionException | YES |
SEMAPHORE_REJECTED | HystrixRuntimeException | 信号量满异常j.l.RuntimeException | YES |
BAD_REQUEST | HystrixBadRequestException | 请求参数错误等非系统产生的异常 | NO |
HystrixBadRequestException
中。有的朋友可能会问?
“异常信息”数据是不能触发数据库事务回滚的。
那FeignClient的Fallback 策略还有什么用?当然有用,在远程服务网络超时或服务宕机的时候,还是要依靠FeignClient的Fallback 策略。因为此时没有响应结果返回。
@Configuration
public class FeignClientErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
try {
if(response.body() != null){
String jsonStr = Util.toString(response.body().asReader());
//json字符串转对象
ObjectMapper mapper = new ObjectMapper();
AjaxResponse ajaxResponse = mapper.readValue(jsonStr, AjaxResponse.class);
// 将AjaxResponse包装成 HystrixBadRequestException,不会触发FeignClient的Fallback策略
if (!ajaxResponse.isIsok()) {
return new HystrixBadRequestException(ajaxResponse.getMessage());
}
}
} catch (IOException ex) {
return feign.FeignException.errorStatus(methodKey, response);
}
return feign.FeignException.errorStatus(methodKey, response);
}
}
@EnableFeignClients(
defaultConfiguration = FeignClientErrorDecoder.class
)
@FeignClient(name = "ASERVICE-SMS",
configuration = FeignClientErrorDecoder.class,
fallback = SmsServiceFallback.class)
public interface SmsService {
//省略
}
还有通过配置文件指定的方法,相对麻烦。笔者这里就不列举了!
在使用了Hystrix进行服务熔断降级之后,我们亟待于有一种方式可以查看Spring Cloud 微服务各个节点的哪些方法接口触发了熔断、请求成功数量、请求失败数量,包括断路器状态、服务请求频率等信息。我们获得了这些信息之后才能有效的进行服务的扩容、调整,从而更好地满足微服务系统的高并发以及高可用的要求!
Hystrix 项目包含了一个DashBoard子模块可以帮助我们完成相关数据的获取以及展示工作。
新建一个Spring Boot项目,该项目集成Hystrix DashBoard之后可以完成服务熔断统计信息的展示。通过maven坐标引入Hystrix DashBoard
* 新版本(笔者Spring CLoud H版)不要使用spring-cloud-starter-hystrix-dashboard
,使用spring-cloud-starter-netflix-hystrix-dashboard
* 不是web项目么?为什么没有spring-boot-starter-web
?答:spring-cloud-starter-netflix-hystrix-dashboard
已包含
<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>
application.yml,自定义一个不常用的项目启动端口
server:
port: 8772
在项目启动入口类上面加上@EnableHystrixDashboard
注解
浏览器中访问:http://localhost:8772/hystrix, 若显示如下图所示,则表示Hystrix DashBoard监控服务正常运行,搭建成功。
上面一小节的操作,我们只是将Hystrix DashBoard监控服务完成。它将监控我们系统内的所有的微服务,为了保证能正确的从系统内的微服务获取服务熔断降级相关的信息。被监控的服务需要满足一些要求:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management:
endpoints:
web:
exposure:
include: refresh,health,hystrix.stream
本节为大家讲的这种Hystrix DashBoard搭建方式,一次只能监控一个服务。
在地址栏输出某个微服务的actuator监控地址:http://locahost:8401/actuator/hystrix.stream ,点击monitor Stream显示该服务的监控信息:
注意:查看hystrix监控之前,一定要访问一次API,否则一直Loading。
所以通过该实心圆的展示,就可以在大量实例中快速的发现故障实例和高压力实例。
上节内容为大家介绍了使用Hystrix DashBoard进行服务熔断降级监控,但是上一节课中,我们只能每次监控一个服务,码。在大型的微服务系统中动则几百微服务,还是需要一种把监控信息聚合起来的方式,方便我们发现集群内个服务节点中的问题。
通过上面这张图大家可以看出来,为了达到聚合服务监控信息的效果,我们需要引入一个组件turbine,由它来完成监控信息的聚合工作。
新建一个Spring Boot项目,通过maven坐标引入turbin
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在启动类上使用@EnableTurbine
注解开启 Turbine
在 application.yml 加入 Eureka 和 Turbine 的相关配置
spring:
application:
name: turbine-server
server:
port: 8773
eureka:
client:
service-url:
defaultZone: http://zimug:centerpwd@peer1:8761/eureka/eureka/,http://zimug:centerpwd@peer2:8761/eureka/eureka/,http://zimug:centerpwd@peer3:8761/eureka/eureka/
turbine:
app-config: ASERVICE-SMS,ASERVICE-RBAC
cluster-name-expression: new String("default")
combine-host-port: true
参数说明
turbine.app-config
参数指定了需要收集监控信息的服务名,多个服务名之间用逗号分隔turbine.combine-host-port
参数设置为true
,可以让同一主机上的服务通过主机名与端口号的组合来进行区分,默认情况下会以 host 来区分不同的服务,这会使得在本地调试的时候,本机上的不同服务聚合成一个服务来统计。turbine.cluster-name-expression
参数指定了集群名称为default
,当我们服务数量非常多的时候,可以启动多个 Turbine 服务来构建不同的聚合集群,而该参数可以用来区分这些不同的聚合集群,同时该参数值可以在 Hystrix 仪表盘中用来定位不同的聚合集群,只需要在 Hystrix Stream 的 URL 中通过 cluster 参数来指定;注意:new String("default")
这个一定要用 String 来包一下,否则启动的时候会抛出异常:org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'default' cannot be found on object of type 'com.netflix.appinfo.InstanceInfo' - maybe not public or not valid?
在完成了上面的内容构建之后,我们来体验一下 Turbine 对集群的监控能力。分别启动
访问Hystrix Dashboard并对http://localhost:8773/turbine.stream聚合结果进行监控