熔断限流概述
在基于Spring Cloud的微服务架构体系下,按照系统功能边界的不同划分,原先大而全的系统会被拆分为多个不同的微服务,而相应的微服务会提供一组功能关联的服务接口,并向系统中的其他微服务提供服务。在正常情况下,各个微服务之间功能上相互解耦,从软件的设计上来讲会呈现出一个比较合理的状态,但是从调用链路上来看,这种拆分实际上也是拉长了外部服务请求的调用链路。
举个例子,在创业公司的早期,考虑到研发维护成本,系统架构设计很简单,从软件结构上看,就是一个api服务面向app端,一个service端面向后台功能。以用户购物场景举例,虽然这个过程逻辑上会经历商品、下单、支付、物流、库存等复杂逻辑的处理,但是因为这些逻辑都耦合在一个系统中,所以从用户的app到后台服务,服务的调用链路并不算太长。即便在这样的情况下,还是会存在如果请求量突然剧增,服务端的业务处理线程池被塞满,整个后台系统的数据库连接资源、缓存资源全部被耗尽,从而导致整个服务不可用的情况。
而随着公司的逐步发展,业务请求量与日剧增,为了提高整个系统的吞吐量及可用性,我们采用了微服务架构的设计,将原先的系统拆分成了商品、订单、支付、物流、库存等多个微服务,而这些服务之间通过网络进行通信(以Spring Cloud来说就是通过我们前面说到的FeignClient进行服务发现后,以HTTP的方式进行网络调用),形成了一次购物请求,会经历app端调用商品微服务进行浏览,选中商品后由商品中心调用订单中心进行下单,然后订单中心调用支付系统进行付款,付款成功后,订单中心再调用物流中心进行发货,与此同时,物流中心调用库存系统进行库存减少的漫长调用链路。
这个流程看起来就有点长了,为了方便大家理解,还是来张图:
如上图所示,在系统微服务化后,虽然此时每个微服务都拥有独立的进程资源、业务线程池以及单独的数据库,整体的系统吞吐量比以前高了很多,并且每个微服务也都是集群部署。但是因为整个调用的网络链路是非常长的,如果此时发生局部网络或者部分微服服务故障的话,依然可能会导致整个微服务系统的瘫痪。
举个例子,假设此时物流服务发生了宕机,但是前面的微服务都不知道,因为整个链路调用都是同步的,所以此时订单服务调用物流微服务的时候会出现部分线程阻塞直至超时异常,同理调用物流的微服务的订单服务的那个线程也会出现阻塞,假如此时业务请求并发量非常高,因为线程阻塞时间过长,那么很快订单微服务及物流微服务的业务线程数资源就会被耗尽,此时用户App端就会出现不仅购物功能无法使用,就连商品浏览也不行了,而此时请求量继续增加,情况就会更加恶化,如果业务线程池使用的是*队列(线程池请求队列),最终还会导致系统内存溢出,此时系统要想自动恢复可能就比较困难了,糟糕的情况就是服务持续不可用,而最终可能只能采用重启整个系统的高昂成本来临时解决下,而这也还不能最终解决问题,因为重启后情况依然可能会发生(如果并没有排查及解决掉物流微服务故障原因的话)。
从上面的例子看,一个微服务的故障居然能导致整个系统的崩溃,而我们希望的情况是如果发现物流微服务持续故障的话,此时订单微服务应该是可以感知到,并根据一定的机制进行容错,即订单微服务在知道物流微服务异常的情况下,就暂时先不要把请求发送到物流微服务了,给物流微服务先限流,而在自身本地逻辑中采取一个默认容错逻辑进行熔断后立刻返回App调用端,例如,可以先将需要发送的消息缓存,待物流微服务恢复后再重新发送。这样的话,故障的物流微服务也就不会导致订单服务因为同步调用链路超时过长而出现级联故障了。
那么在Spring Cloud微服务设计中如何才能实现这样的机制呢?这里涉及到几个问题:
微服务如何定义为故障,熔断的条件是什么?也就是说订单微服务如何确定物流微服务不可用,从而可以实现熔断操作;
被定义为故障的微服务恢复后如何让熔断方感知?订单微服务何时才可以继续正常的调用物流微服务,实现故障恢复;
Spring Cloud的代码实现机制是什么样的?
以上这些问题,就是本章要讲述的如何在Spring Cloud微服务设计中实现服务熔断限流的内容了!而这一点对于并发量非常高的情况下,实现微服务的可用性是很重要的一个方面。
Spring Cloud中集成Hystrix框架
在Spring Cloud微服务设计中需要通过集成Hystrix框架来实现微服务间的熔断保护机制,Hystrix框架会通过监控微服务之间的调用情况,来决定是否启动熔断保护。那么接下来,就让我们一起来看下如何在Spring Cloud项目中通过集成Hystrix框架来实现熔断机制吧!
引入依赖
要Spring Cloud中使用Hystrix框架,需要引入Hystrix框架的starter依赖包,如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
通过引入此stater依赖包,我们就可以基于Spring Boot框架的特性,实现对Hystrix框架的开箱即用了。
注解开启熔断器
在Spring Cloud微服务中启用熔断器,需要在微服务的Application主程序上添加org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker注解,如:
@EnableDiscoveryClient
@EnableCircuitBreaker
@SpringBootApplication
@EnableFeignClients(basePackageClasses = {PaymentClient.class})
@EnableScheduling
public class Goods { public static void main(String[] args) {
SpringApplication.run(Goods.class, args);
}
}
通过这样一个简单的注解配置,此时微服务就开启了基于Hystrix的断路器功能。需要说明的是,在某个微服务中开启断路器,实现的是该微服务对其下游微服务的熔断功能,而不是该微服务对其上游调用的熔断,这一点大家不要混淆了,因为在Spring Cloud的微服务体系下,熔断的实现是基于Hystrix本地库来实现的,本质上是客户端熔断,而不是服务端的熔断。相比较于最近谈论比较多的基于Service Mesh的限流熔断功能而言,基于客户端的熔断从应用的形态上看,是与微服务本身融合在一起的,而不是独立的服务。
FeignClient开启Hystrix
在微服务中开启断路器后,并不表示就可以立刻使用了,在前面的章节中我们讲过,在Spring Cloud微服务体系中,微服务之间的通信交互需要通过使用FeignClient来进行,而默认情况下FeignClient中默认情况下是禁用Hystrix的,所以如果需要在微服务中启用Hystrix的熔断功能,则需要通过配置手动开启Hystrix功能,这样FeignClient客户端在微服务之间进行通讯调用时,才能在感知到微服务异常的情况下,将错误指标信息反馈给Hystrix框架,从而Hystrix才能根据自身逻辑对熔断器的状态进行启停(关于Hystrix的具体运行原理,我们在后面的章节中进行介绍)。
以下是我们在项目的bootstrap.yml文件中,开启FeignClient对Hystrix支持的配置:
feign:
hystrix:
enabled: true
实现FeignClient服务降级代码
Spring Cloud中微服务之间的服务调用是基于FeignClient的,在实际的工程实践中,我们一般会单独将微服务的FeignClient调用端代码进行抽离,并以SDK jar包依赖的形式进行发布。一般情况下,可以每个微服务都抽离一个FeignClient工程代码,这样更加清晰;如果觉得太过于麻烦,也可以把多个不同微服务的FeignClient客户端代码耦合在一起,所有的微服务依赖这一个SDK也可以,只是后期如果微服务的数量比较多,并且维护团队比较分散的话,这样也会导致一个很臃肿的项目出现,维护升级更加麻烦而已,大家可以根据团队的实际情况进行规划。
我们在前面讲述过基于Spring Cloud的微服务的熔断机制,实际上是基于Hystrix框架的客户端熔断机制,也就是说上游微服务在通过FeignClient调用下游微服务的时候,如果感知到下游微服务调用异常需要向上向Hystrix框架反馈异常,如果Hystrix框架计算异常指标达到了阀值就会开启熔断器。而之后FeignClient客户端针对该下游微服务的调用,就需要被Hystrix熔断后回调一个相应的本地降级处理方法,从而实现服务降级。
而FeignClient从代码的角度已经支持了这样的设计,我们在通过@FeignClient注解编写微服务的客户端调用代码时,就可以通过指定相应的Fallback类来处理服务被熔断后的降级逻辑。下面我们就以本文举例的项目示例,来编写订单微服务的FeignClient客户端SDK代码:order-client。
代码示例:
@FeignClient(value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class)
public interface OrderClient { // 查询购物订单扣费状态(内)
@RequestMapping(value = "/order/queryOrderCost", method = RequestMethod.GET)
QeuryOrderCostResVo queryOrderCost(@RequestParam(value = "orderId") String orderId) throws InternalApiException;
}
根据订单微服务中的服务接口定义,我们通过@FeignClient注解定义了一个OrderClient.class类,该类声明了微服务的接口定义,假设这里订单微服务提供了一个订单查询接口(一个微服务一般情况下会有多个服务接口,这里举一个接口只是为了好举例)。
我们可以看到在@FeignClient注解的属性中,有一个fallback属性,这个属性指定了一个服务降级的配置类OrderClientFallback.class。这样,就可以在该类中实现微服务对应方法的降级逻辑了:
public class OrderClientFallback implements OrderClient { @Override
public OrderCostDetailVo orderCost(String orderId, long userId, String busiId, String orderType, int duration,
int bikeType, String bikeNo, String countryName, int cityId, int orderCost, String currency, int strategyId,
String tradeTime) {
return new OrderCostDetailVo();
}
}
可以看到降级处理类实际上是OrderClient的一个实现类,所以在这里每个微服务的接口都会被强制要求实现相应的熔断降级代码。而具体的降级逻辑,则可以根据服务的具体情况进行编写,如这里是返回一个空的消息对象。
以上模式就是在Spring Cloud中通过FeignClient调用时,在开启Hystrix熔断功能后的基本处理套路了。接下来,我们通过具体的测试效果,来看下熔断器功能的生效情况:
1、在微服务goods中引入order微服务的FeignClient客户端SDK
<dependency>
<groupId>com.wudimanong.client</groupId>
<artifactId>order-client</artifactId>
<version>1.0.0</version>
</dependency>
2、为了观测,我们需要开启HystrixDashboard
引入HystrixDashboard依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>
在应用程序主类中开启HystrixDashboard注解:
@EnableHystrixDashboard
@EnableDiscoveryClient
@EnableCircuitBreaker
@SpringBootApplication
@EnableFeignClients
public class GoodsApplication { public static void main(String[] args) {
SpringApplication.run(GoodsApplication.class, args);
}
}
3、此时通过访问HystrixDashboard控制台就可以看到监控指标信息了
我们假设goods调用order服务正常情况下Ciruit是close状态的,如果此时断掉order服务,然后多刷几次goods调用请求,此时,我们就发现关于order服务的熔断开关被打开了。
然后我们恢复order服务,然后再多刷几次调用接口,就会发现Ciruit就会被关闭了。
通过上面的配置,我们就基本完成了Spring Cloud项目中关于Hystrix熔断器的配置了。