SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

通过查看@SpringCloudApplication的源码可以发现,实际上SpringCloudApplication就包括 了 @SpringBootApplication、@EnableDiscoveryClient和@EnableCircuitBreaker 3个注解,SpringCloudApplication的源码如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

这样就完成了断路器的基本配置,之前提到过,断路器的开启有触发条件,通常通过设置阈值来作为断路器的触发条件,Spring Cloud Netflix Hystrix 提供了几种常用的阈值配置 。 首先由circuitBreakerEnabled的配置项来配置是否启用断路器,默认开启,但是如果使用Spring Cloud Feign作为远程调用框架,那么这里需要额外的配置来启动Hystrix,在application.yml中添加如下配置即可。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

其实,设置阈值就是和现实情况的统计做对比,如服务失败比例、最大并发数等,一旦统计的结果超过阈值,就开启断路器。既然做统计,就会有一个维度,通过观察Hystrix的源码,我们发现在Hystrix中称这个维度为Statistical Window Buckets(统计窗口时段),即采用时间的维度,默认10s,也就是10个Bucket,通过在application.yml的配置设置

metrics.rollingStats.numBuckets的值可以改变这个窗口时段。例如,想设置统计时间是20s,那么代码如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

需要注意的是,numBuckets的配置不能动态更新,如果修改了,只能重启服务器才能生效。断路器提供了错误百分比的阈值设置,表示错误的百分比,默认是50%。也就是说,如果我们的numBuckets是20,就表示在20s内,请求的失败比例超过50%时,断路器就开启,当然可以通过配置来修改它,具体如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

断路器还提供了最大请求数的阈值设置,表示在指定的时段内,请求数量超过了阈值,则启动断路器,这项配置主要针对高并发时服务负载过大的情况下,有效地缓解服务的压力,做到负载保护,这也是断路器的一个比较核心的功能,对应的配置如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

断路器中还有个配置,称为sleepWindowInMilliseconds,源码中的注释是seconds that we will sleep before trying again after tripping the circuit(我们将会切断线路之后在重试之前休眠的秒数),意思就是断路器一旦启动,就会熔断之前的请求线路,当新的请求进来时,则会采用一定的降级策略来处理请求,但断路器并不会一直运行,在一定时间后会进入半开启状态,即会释放一部分的请求进行重试,若重试结果正常则断路器关闭,若仍然有问题,则断路器再由半开启状态进入完全开启状态。sleepWindowInMilliseconds就是用来配置断路器开启后多长时间会进入半开启状态的,即配置断路器的工作时间,具体如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

常用的配置还有很多,如设置请求的超时时间 :

execution.isolation.thread、timeoutIn Milliseconds等,这里就不一一介绍了,其他的阈值配置大家可以通过查看GitHub的Netflix Hystrix的教程wiki,或者查看HystrixCommandProperties类的源码来学习。

服务降级

====

之前讲解了开启断路器的方法和断路器启动的阈值配置,那么断路器开启后会如何处理新到来的请求呢?最常用的就是采用服务降级的方式,即提前指定好降级方法,当断路器启动时,则调用降级方法而不再调用原来的服务,以达到服务降级、保护负载的目的。

下面来看一下在Spring Cloud Netflix Hystrix中如何做到服务降级。其实很简单,如果在服务消费者端使用的是SpringCloudRibbon,那么只需在调用服务的方法上增加@HystrixCommand注解,然后指定对应熔断后调用的方法名即可,代码如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

在上述代码中,fallbackMethod="findUserNameByIdFallback"就指定了服务被熔断时的降级方法,当断路器启动后,原来调用restTemplate的方法(findUserNameById)就不会被执行,findUserNameById 方法将直接返回findUserNameByIdFallback的执行结果(ZhangGang)。

在之前介绍过,Spring Cloud Feign是对Spring Cloud Ribbon的封装,提供了更简便的远程方法调用方式,其实Spring Cloud Feign不仅封装了Ribbon,还封装了Hystrix,因为几乎在微服务架构中每个服务调用方都需要使用断路器,Spring Cloud干脆就开发了一个集成了远 程 调 用 ( Spring Cloud Ribbon ) 和 断 路 器 ( Spring Cloud Netflix Hystrix)的框架,它就是Spring Cloud Feign。

那么,在Feign中如何使用服务降级呢?配置代码如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

在以上代码中,Spring Cloud Feign可以直接定义类级别的fallback,也可以直接在Feign的接口注解@FeignClient 中 配 置FallbackFactory ,指定降级服务为UserFeignClientFallBack。

UserFeignClientFallBack的代码如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

由以上代码可以看出,fallback的类继承了FallbackFactory,实现的create方法会创建一个新的服务实现,用来代替之前的真实方法来达到降级的目的。

线程隔离

====

线程隔离又称为舱壁机制。一般为了提高船的生存能力,会将船体分为多个舱室,当船体发生事故导致进水时,只有受损的舱室会进水,其他舱室由于舱壁的隔离,并不会受到影响,从而将船体浮力的下降控制到最小。

Spring Cloud Netflix Hystrix也采用了同样的设计原理来保护微服务应用,在微服务的远程调用中,如果所有的请求都在一个线程池中,一旦有个别请求响应缓慢,这些请求可能会不断地消耗可用的资源,直至占满整个资源,其他的请求都会进入等待队列,从而拖垮整个应用。

那么,如何做到线程隔离呢?Hystrix提供了两种实现方式:线程池、信号量。

1. 线程池

线程池的做法很简单,可以将同一个请求分到同一个线程池中,使不同的请求拥有不同的线程池,这样每个请求就像有了自己的舱室,当其中一个舱室由于故障导致线程池被占满后,并不会影响其他“舱室”的请求。

Hystrix采用命令模式来完成线程池隔离线程的功能(命令模式是一种数据驱动的设计模式,也属于行为型模式)。也就是说,请求会以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适对象,并把该命令传给相应的对象,由该对象执行命令。图3.3所示为Hystrix的命令模式+线程池模式实现线程隔离的方式。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

由图3.3可知,当依赖B调用的后端服务故障时,请求可能会超时或报错,但是只会阻碍自己资源池内的请求,并不会影响依赖A和

《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》

【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享

依赖C中的请求。

Hystrix中要执行Command有4种方式:execute()、queue()、observe()和toObservable()。详细说明如下。

(1)execute()是以阻塞的方式运行Command的,在执行时会先以与queue()同样的方式获得Future对象,然后调用Future的get方法,get方法会阻塞execute的执行,直到方法运行完成。

(2)queue()是以非阻塞的方式运行Command的,调用queue方法会直接返回Future对象。需要调用者使用get方法来获取Command的返回,get是阻塞式的。

(3)observe()表示立即订阅并开始执行Command,然后返回一个Observable,当subscribe到该对象时,会重新触发流程的排序和通知。

( 4 ) toObservable() 与 observe 类 似 , 返 回 Observable , 但Command不会立即执行,必须subscribe后才能真正开始执行命令的流程。

下面来看一个简单的HelloCommand,代码如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

在上述代码中,要执行一个hello的方法,那么首先可以定义一个HelloCommand,继承HystrixCommand,再通过构造器将参数传递到Command,然后通过设置HystrixCommand GroupKey.Factory.asKey(“HelloGroup”)的方法返回HystrixCommandGroupKey,Hystrix通过HystrixCommandGroupKey来定义线程池,当HystrixCommandGroupKey相同时,则会在同一个线程池内执行,最后通过getFallback方法来定义断路后的返回,当我们在执行execute时,run方法首先被执行,然后执行异常后,getFallback将被执行,以达到优雅的降级。

2. 信号量

尽管线程池的做法能够优雅地将应用程序的依赖服务保护起来,而不会在依赖服务出错时影响到服务本身,但是通常线程池更适合于远程调用依赖服务的模式,当我们在调用一些本地依赖服务时,或者网络开销基本忽略不计、服务响应延迟极低时,就没有必要再拥有另外一个线程,线程池本身就会增加一定计算的开销,从而影响到应用程序本身的性能。

Hystrix提供了信号量作为另一种方式来实现线程隔离,信号量又称为计数器,其实就是对任何给定依赖项的并发调用数进行计数,然后限制信号量的大小,而不是使用线程池/队列大小。因此,信号量的开销是很低的,其前提是保障依赖服务能够快速地返回异常,所以通常我们在非远程的依赖服务的调用中使用信号量来做线程隔离。

那么,具体是如何使用的呢?其实相比于线程池的用法,使用信号量只需在Command的构造器中设置隔离策略为SEMAPHORE,代码如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

需要注意的是,在使用线程隔离时,无论是信号量的方式还是线程池的方式,客户端在调用依赖服务时都需要有超时的设置来防止服务被无限期阻塞,避免隔离区长期饱和。

请求合并

====

除提供基本的断路器和服务降级功能之外,Hystrix还有其他的高级功能,如请求合并和请求缓存。请求合并就是把多次请求合并为一次请求,为什么要合并请求,合并什么样的请求?带着这些问题,我们来了解一下请求合并的功能。

请求合并是在前端开发中常用的手段。例如,我们往往将一些页面的JS、CSS和图片分别合并到一个文件中,如webpack,然后只通过一次请求就可以加载这个文件;再如,一些BFF(Backend For Frontend)的框架,也都将短时间内的请求合并为一次请求进行发送,这些都是请求合并的体现,这样做可以减少与后端服务建立连接的次数,有效地减少不必要的网络消耗,提高系统性能和负载。

那么,后端的请求合并是如何做的呢?请求未合并和合并的对比具体如图3.4所示。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

由图3.4可以看出,当请求没有进行合并操作时,5次请求就会占用5个线程池中的线程,并且会与依赖服务建立5次新的连接,而当进行请求合并操作后,5次请求只会占用一个线程池的线程,并且与依赖服务建立一次连接。

Hystrix提供了请求合并的功能,采用的还是指令模式,我们通过继承HystrixCollapser类和自定义BatchCommand的方式来实现请求的合并。具体如何实现呢?假设现在有个请求是根据用户ID来查询用户,那么使用Hystrix合并请求的示例如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

由上述代码可以看出,首先需要定义一个合并查询用户的指令,并继承HystrixCollapser类,而且需要定义HystrixCollapser的泛型,具体说明如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

本例中批量查询返回值是List,单个查询返回值是User,请求参数类型是String,所以应该是继承HystrixCollapser<List, User, String>,然后通过构造器的方式设置请求参数和依赖服务到全局属性中,通过createCommand方法,返回具体批量查询的指令,通过mapResponseToRequests来定义具体返回结果和请求的映射关系。在一般情况下,返回顺序与请求的顺序相同。

批量的查询指令需要另外定义,并且继承HystrixCommand,用法与之前一样,在run方法中定义具体的实现,如调用依赖服务。这里可以看出,如果要实现请求合并,不仅客户端需要进行改造,服务端也需要提供相应的批量查询方法,代码如下。

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

写个测试来验证一下,代码如下。

SpringCloudå¾®æå¡æ¶æä¹æ­è·¯å¨ï¼å¦ä½è§£å³å¾®æå¡ä¸­çéªå´©æåºï¼

可以看到,采用queue的方式调用指令,批量查询只执行了一次,并且返回的数据也对应正确。这样便将多次GetUserById的方法合并了,减少了请求的次数。

那么,什么样的请求会被合并?是不是所有的GetUserById都会被合并?当然不是,如果所有的GetUserById请求都被合并,那么所有的请求都要等到在一起时进行合并,这显然不可能,Hystrix默认是使用一个单位的Window Bucket时间,即会合并10ms内的请求,那么可以再次验证一下,修改之前的main方法,代码如下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值