1、前言
在前边,我们已经学习了三层的限流的各种用法,我们知道限流啊,可以降低服务的负载,从而避免服务,因为过高并发而出现故障。
服务不出现故障,自然就不会把故障传递给其他服务,从而避免了级联失败啊。
所以啊,限流其实是对服务故障的一种预防措施,但是一旦服务已经出现了故障。它就很容易把这些故障传递给其他依赖于它的服务,那这样很容易就产生了雪崩了。
所以我们就需要用线程隔离,还有降低熔断这些手段啊,去避免接连失败,避免雪崩。
所以今天这篇博客啊,我们就来看一下sentinel如何去实现线程隔离和降级熔断。
2、隔离和降级
2.1 FeignClient整合Sentinel
那我们先来看一下 FeignClient整合sentinel。为什么要用 FeignClient整合sentinel呢?
这个跟隔离和降级的原理啊,是有关系的,所以我们先来回顾一下隔离和降级的原理。
比方说呢,这是我们之前讲到过的几个微服务。
那服务之间有相互依赖的关系,比如说服务a啊,它的里边有一个业务。
依赖于服务c。那服务c出现了故障,服务a内的业务去调用服务c时必然会被阻塞,那这个业务被阻塞了以后,资源就得不到释放。
那随着这样的请求越来越多,最终啊,服务a内部的资源一定会被耗尽,那整个服务a等于也就出现故障了。
那按照这样一种方式去发展,最终整个集群是不是就挂了?这就是雪崩问题。
那线程隔离的做法比较简单啊。其实就是给每个业务划分独立的线程池,或者是限定每个业务所能使用的线程数量。
比方说呀,我给业务一限定十个线程,那这样来当有请求访问业务,业务一最多使用这十个线程池。
再比如给业务二也限定十个,那这样来有人访问业务二时,最多使用我这里边的十个线程。
那现在服务c出现了故障,那么业务二被阻塞了。
再有更多请求进来也都是会被阻塞,资源得不到释放。
但是啊,它最多占用我十个线程的资源,那再要呢,我就不给了,那再有更多请求进来,我就会拒绝这些请求。
那这样是不是就把这种故障隔离到了一定范围内了,所以呢,它叫做。线程隔离。
而熔断降级的方式呢?就跟它有一定差别了。
熔断降级会去统计故障服务的比例。比方说现在啊。
服务a访问服务d的时候啊,成功的只有一个故障的呢,有两个显然故障比例过高。这个时候断路器就会熔断业务。
再有,想要请求服务地的业务来的时候啊,就会快速的失败啊,不让它访问了,直接拒绝,那这样呢,就不会导致资源耗尽了。因为你压根就没机会去访问了嘛。
这是线程隔离和熔断它的一个原理,那在这呢,我们会发现啊。
无论是线程隔离还是熔断,其实都是对服务调用者的一种保护,避免服务的调用者被故障服务给拖垮。
对吧,那我服务a依赖于服务c呀,你挂了,结果把我拖垮了,这不行,所以我要保护这个调用者。
那要保护服务的调用者是不是应该在微服务发起远程调用的时候去做隔离或者熔断呀。
那我们里边做远程调用都是基于谁来实现的?是不是Feign 呢?
所以我们要想实现隔离和降级啊,最好的办法就是基于Feign去整合Sentinel去做隔离和降级,那怎么去实现呢?
第一步呢,你只需要去修改服务调用者的application.yml文件,然后添加这么一个配置。
就是开启分对于sentinel的支持,只要你做了这个配置啊,我们的sentinel就会自动的去监控Feign客户端,把它变成一个链路中的资源。
那这样一来你就可以给它配置
限流的规则,
隔离的规则,
降级规则等等,是不是就很方便了?
而第二步呢,我们还要给Feign编写一个失败后的降级逻辑,那这是什么意思呢?
在以前,我们无论是限流也好,还是服务被拒绝也好,其实都是直接抛个异常到前端,那这种方式啊,其实不够友好。
你想啊,人家来调用你服务a,服务a那边的业务啊又去调了服务d,结果是因为服务d出了故障。你咔抛个异常到前端,那前端会以为你这出了什么事呢,给用户的感受就不够好,那比较好的一种处理方案是什么呢?
第一种办法,我们可以给用户返回一个友好提示,告诉他说啊诶,这里出了些什么事。 第二种办法,你也可以啊。查询失败的时候返回一些默认的结果给前端。
比方说我现在是去查一个广告信息,那查失败了,我可不可以返回一个默认的广告的前端呢?
或者用户去搜一个东西,没搜到我是不是可以返回一些默认的商品给他看呀?推荐的商品那这些呢?
其实就是一种兜底的方案啊。那也就是降级的逻辑了。
所以呢,给FeignClient编写失败后的降级逻辑就是给它想办法写一个备用方案。
比方说啊,我们来看一下我们的业务,打开Idea。我们这里查订单,
那订单再去查询的过程中又去调用了查用户服务。
其实这不就是微服务的远程调用了吗?
而调用的过程中是不是用了这个userClient呀?
它是什么呀?
它就是Feign的客户端。
那这个调用失败了,你是不是可以写一些兜底的方案? 比方说,返回一些友好提示啊,或者返回一个呃空的,一个用户信息呀诶,这些都可以啊。
那因此我们解下来就要给userClient去编写这个失败的降级逻辑。
那该怎么编写呢?这里有两种方式啊,
- FallbackClass,无法对远程调用的异常做处理。
- FallbackFactory,可以对远程调用的异常做处理。(我们选择这种)
这两种方式本身没有太大的差别啊,只不过FallbackClass它没有办法去编写逻辑的过程中处理异常,但是FallbackFactory却可以。
因此呢,我们一般都是使用FallbackFactory。
那怎么去做呢?首先啊,我们需要在feing-api项目中定义类。
当然这个类你是随便定义的。
但你要去实现。FallbackFactory接口,并且在泛型里去指定你是给哪个Feign的客户端编写的,那我们刚才看到是不是userClient?
代码 :
代码语言:javascript复制
代码语言:javascript复制package com.jie.feign.client.fallback;
import com.jie.feign.client.UserClient;
import com.jie.feign.entity.User;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
/**
* UserClient的fallback factory,用于创建UserClient的fallback实例
*/
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
/**
* 创建一个新的UserClient实例作为服务降级操作的响应结果
* @param throwable 服务调用过程中出现的异常
* @return 新的UserClient实例
*/
@Override
public UserClient create(Throwable throwable) {
// 返回一个新的UserClient实例
return new UserClient() {
// 重写UserClient接口的findById方法
@Override
public User findById(Long id) {
// 记录异常信息到日志中
log.error("查询用户异常", throwable);
// 返回一个默认的User对象
return new User();
}
};
}
}
你就是在这编写降级的逻辑或者讲备用的方案,你可以在这记日志啊,可以在这里啊,返回友好提示啊。或者返回一个默认的用户信息啊,都是可以的,所以你看我们这里呢,其实就是记录了个日志。
返回了一个什么啊。空的用户。这就是一个失败降级的业务逻辑了。
写好这个工厂以后第二步呢,我们还要把这个工厂注册成一个spring的Bean因为只有注册成了Bean别人才能用。
那么第三步呢,就是在UserClient上面使用UserClientFallbackFactory,通过@FeignClient 这个注解里的 fallbackFactory ,去指定这个FallbackFactory。
我们所编写的Fallback逻辑啊,自然就生效了啊。
如果启动不了,可以检查一下启动类。
我们去重启一下项目,重启完成啊,我们打开浏览器啊,在这里呢,我们去刷新一下。
看一下触点链路啊。
现在什么都没有,我们去访问一下。
再过来诶,来了啊,我们可以看到啊,在我们查订单的链路下边。
就会有我们Feign客户端的簇点链路啊,所以现在我们已经实现了Feign与Sentinel的整合了,你可以给它加任意的这些规则。
2.2 线程隔离
刚才,我们已经实现了Feign与Sentinel的整合了,所以接下来啊,我们就要利用Sentinel去完成线程隔离和降级熔断了。
2.2.1.线程隔离的实现方式
那我们先看一下线程隔离啊,之前我们其实已经聊到过线程隔离呢,有两种实验方式。
分别呢是基于线程池隔离,还有基于信号量隔离。
那Sentinel啊,默认用的就是信号量这种方式啊,那它们两个有什么差别呢?
我们通过一个案例啊,来看一下,假设说我现在有四个服务I, A,B,C啊。
服务I里面的一些业务啊,它依赖于服务AB和c。比方说现在来了一个请求啊。
那这个请求的业务它依赖于服务a和服务b,如果说我们现在用的是线程池隔离。
那么他就会给这个业务所依赖的每个服务都创建一个线程池,你不是依赖于AB吗?那我就给你创建俩线程池,一个是a的线程池,一个是b的。
然后你这个请求来了以后啊,我不会去使用你这个请求本身的这个线程,我会让你去从这俩池子里。分别取一个线程。
然后用这个线程去调用Feign的客户端,发起远程调用。那这样呢,我们就把两个服务给隔离了。
你想啊,现在服务a,比如说它出现了故障。那么,最多就是把这个池子里的线程给用完吧,现在如果还有新的请求,还想访问这个服务a,结果这个池子满了,那新的请求还能进来吗?
不能就会被我拒绝,那这样一来,它是不是就不会把我们服务 I 里边的资源给耗尽了,它就把故障隔离在这个范围内了。所以这是线程池模式啊。
而如果你采用的是信号量方式啊,那就简单多了,你比如说现在来了一个请求,你想要访问服务c。
那么信号量啊,它不会去创建独立线程,而会去使用你原始的这个处理请求的线程,而让你直接去调用Feign的客户端去调用服务c。
那它怎么去做隔离呢?它会在你这个请求进入时做一个判断,它维持了一个计数器啊。然后判断什么呢?
判断一下计数器现在还有没有,比方说计数器总量是十。
每进入一个请求啊,这个计数器。就会减一就等于你从这里边取了一个信号嘛,然后呢,你就可以去访问了,那你想当你这个业务啊,有十个来访问时,十个信号都被取完了,再有新请求,是不是也进不来了?
再来新请求就会被拒绝。那这样一来,等于是利用计数器限制了最终的这个线程的数量吧。
对它也能起到一个故障隔离的作用。当然了呃,如果你的业务处理完,信号肯定还要还回去。
这是线程池隔离啊,与信号量隔离,两者的一些实现上的差别啊。那他们到底什么场景下该用哪个呢?有什么优缺点呢?好,我们来做一个简单的对比啊。
首先呢,我们来看一下线程池隔离啊。
那它的一个优势是支持主动超时和异步调用。那什么叫主动超时呢?
我们知道线程池模式啊,会给每一个远程调用。创建独立线程。因此,在这个请求啊,远程调用的请求发出以后啊,其实我们是可以。通过线程池来控制它的比方说,我看你这个请求耗时有点久了,我可以立即终止这个线程。
线程一旦被终止,那你这个业务是不是也就终止了?所以这种就叫主动超时,看你时间长了,咔给你干掉,不让你玩了。
而所谓的异步调用呢?每一次调用我都是一个独立的线程,我不是使用原来的处理tomcat请求的那个线程,那这样一来我都是独立线程了。
不同的调用采用不同的线程池了,那我之间是不是相互独立的?是不是异步的?对吧?它是支持这种异步调用的啊。
当然了,它也有一定的缺点啊。因为你使用了独立线程,带来这些优势,但同时你每一次调用都有独立线程,线程越多将来。
额外的开销是不是也越大呀?别的不说,你CPU的上下文切换是不是也会比较耗时啊?所以啊,这是它所带来的一些缺点。
那因此呢,我们的线程池池隔离呀,它的一个使用场景啊,就是低扇出的场景。那什么叫扇出呢?
比方说啊,现在请求到我这个服务了,我这个服务依赖于n个其他的服务。那么,这个就叫扇出,就是从我这来了,来了一个咔我删出去了好几个嘛,是不是有点扩散的那个感觉?这叫扇出。
那如果我依赖的服务越多,那我这个扇出是不是也就越高,而扇出越高,那调用的越多,我需要开启的线程也越多,我的消耗是不是也就越大呀?所以呢,它不适用于。高扇出的场景。
好,那这是它的一些缺点和优点了啊。
那信号量呢,其实就是跟线程池啊,做了一个弥补。
那它的一个优点呢?就是轻量级没有额外开销,你想嘛,它就是个计数器嘛,它不需要开启线程,是不是开销非常的小啊?
当然了,那它也有一些缺点,那正是因为它没有开启独立线程,请求进来了以后,
它只是判断一下信号量。啊,你这个计数器到底诶有没有到零?是否允许你访问?如果允许,一旦放行,那这个请求发出了以后啊。就不归你管了,你这个信号量就控制不了了啊,你不管人家掉的是五秒还是十秒,你想中间把它停掉。不好意思,停不掉,你只能依赖于你这个Feign本身的超时时间。
所以呢,它不能做主动超时啊啊,当然更不用提异步调用了,你就没有独立线程,怎么做异步调用?
当然,这是它的一个缺点啊,那因此呢,它比较适用于啊,高频高删除的这种场景。
为什么呢?因为你不管调用多少服务。它都不用开启独立线程嘛,消耗比较低嘛。
那像我们的网关啊,我们的Gateway,它其实就是一个高删除的场景啊,你想啊,请求进入网关以后,网关又不处理它,其实是个代理嘛,它把请求路由到其他为服务。
那将来你的微服务有成百上千,是不是删出就非常多了?那这个时候呢,我们都会采用信号量模式啊,我们的网关基本上都是采用这种模式。
这也是sentinel为什么选择信号量模式的原因。
2.2.2.sentinel的线程隔离
那我们接下来就来看一下Sentinel如何去实现信号量隔离。
以前我们选的是QPS嘛。啊,每秒并发数,而一旦你勾选线程数,后面给一个阈值,其实啊,就是在指定那个信号量的最大值。比如说我这只是五,那也就是说当前这个请求它的信号量最大值为五,也就是最多只能使用五个线程。
那下边呢?我们通过一个案例啊,去演示一下啊。
案例:
案例需求:给 order-service服务中的UserClient的查询用户接口设置流控规则,线程数不能超过 2。
我们来给这个Feign 调用。做一个线程隔离.
配完了以后呢,我们就可以去做测试。
一次发生10个请求,有较大概率并发线程数超过2,而超出的请求会走之前定义的失败降级逻辑。
发现虽然结果都是通过了,不过部分请求得到的响应是降级返回的null信息。
2.3 熔断降级
我们就来看一下sentinel如何来去实现熔断。那熔断降级呢,它其实就是用一个断路器去统计服务,调用时的一个异常比例。
如果说在做服务调用的时候啊,异常的比例过高,触发了预值就会熔断该服务。
拦截访问该服务的一切请求,那这样呢,就会把这个故障的服务隔离开了,不会让它影响到我们正常的服务。
这就像古代啊,这个武侠人士是吧?这手被毒蛇咬了,赶紧收起刀落啊,把这个手砍掉。那脚要是中毒了呢,咔把脚砍掉,防止这个毒扩散到全身啊。
所以壮士断腕啊,就是一种自我保护。当然,你把手砍了,不算本事啊,你要是能接回来才算本事。
所以呢,服务熔断,很好做,将来服务如果恢复了,你是不是还应该去恢复对该服务的访问呢?那这个我们的断路器怎么去做呢?
好,它是由内部的一个状态机来实现的啊,那么这个状态机呢?包含三个状态。分别是
- closeed
- open
- half open
绿色代表走,那在这种状态下啊,断路器。不会拦截任何请求,你随便,不管你这个请求是正常的还是异常的,都可以访问。
但是啊,我们的断路器此时会去统计你这个调用的异常比例,如果统计过程中发现异常的比例过高。
达到了预值,它就会从close状态切换到open状态,红色代表停止。那么这个时候它就会拦截进入该服务的一些请求了。
也就相当于是中断了,但是啊,你不能一直是熔断状态吧,那万一这个服务它又恢复了呢,那因此啊,我们这个熔断的状态啊,它会有一个持续的时间。当这个时间结束的时候啊,它会从open状态切换到half-open半开状态啊。
那么这个状态呀,他会放行一次请求。然后根据这次请求的结果来判断,接下来该干嘛,比如说我放弃了一次请求,结果发现这个请求依然是失败的。
那不好意思,我会再次进入open状态拦截一切请求进入熔断,当然同样是持续一段时间啊,然后再进入Half-open,那如果我放行的这个请求,它执行完了,发现是成功的,那么我就会从哈弗open切换到closed状态。
那这个时候啊,就等于我们的断路器又开始放行了,那大家随便,然后它又开始做数据统计了。
那么这三个状态啊,就可以按照这样一种方式进行一个切换,因此呢,我们不仅能够熔断,还能恢复哦,就是靠这个来实现的。
那在这里面啊,比较关键的两个东西,第一就是熔断的持续时间。这个将来肯定由我们去配置,对吧?第二呢,是失败的预值,什么情况下你要去熔断呢?
而这个达成熔断的条件啊,在Sentinel里边就叫做熔断的策略。
我们知道断路器要想从closed进入open的状态。需要判断服务有没有触发熔断的条件,而熔断条件的判断啊,就是依据熔断策略来完成的。
那在Sentinel里边呢?熔断的策略有三种啊:
- 慢调用
- 异常比例
- 异常数
2.3.1 慢调用
那我们先来看一下慢调用啊。慢调用顾名思义,就是看你的响应的时间,响应时间,简称为rt啊,如果说你这个响应哦,你这个远程调用啊,响应时间过长。
超过了指定时间,那么你这个调用啊,就是慢调用,请求很慢,很明显会占用额外的资源,会拖慢我们整个服务嘛,对不对?所以呢,如果你的慢调用比例达到了预值,也就是说你的这个服务我每次调用你都很慢。
超过了一定的预值,那我就会触发熔断,我认为你这个服务有问题,我不想再调用你了。这就是慢调用,那下边呢是一个配置的示例啊。
资源名叫test策略呢,选的是慢调用比例,然后配了一个rt啊,这就是响应时间,意思就是超过500毫秒的响应都算是慢调用了啊。然后比例域值是0.5,意思就是如果慢调用的比例啊,超过了0.5达到一半以上,那么就触发域值了。
那接下来还有一个熔断时长 5,也就是说我一旦熔断啊,这个熔断时间持续五秒,五秒后进入half-open。
这里还有一个最小请求数,还有一个统计时长,那这两个代表的含义是什么呢?
就是说我会统计最近十秒内的至少十次请求。那在十次里边,如果超过500毫秒的这种慢调用比例达到了一半以上啊,那也就是达到了五个。那么我就触发熔断,而熔断时间啊是五秒钟。
那下边呢,我们就通过一个案例来实践一下啊。
案例
需求:给 UserClient的查询用户接口设置降级规则,慢调用的RT阈值为50ms,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5
那我们呢,就去给这个userClient的查询用户的接口啊,设置一个降级规则。这个配置是不是就很容易了,但是这里有一个问题啊。
那我们怎么样才能触发慢调用呢?响应时间是不是要达到50毫秒以上啊?但是我们本机调用时间不可能这么长,对不对?
所以为了能够触发这个慢调用啊,等一会我们需要去修改一下业务代码。故意的让它休眠一下啊,从而超过这个响应的时间。
1 设置慢调用
修改user-service中的/user/{id}这个接口的业务。通过休眠模拟一个延迟时间:
重启一下。
2 设置熔断规则
下面,给feign接口设置降级规则:
规则:
超过50ms的请求都会被认为是慢请求。
3 测试
在浏览器访问:http://localhost:8088/order/101,快速刷新5次,可以发现:
触发了熔断,快速失败了,并且走降级逻辑,返回的null。
2.3.2 异常比例、异常数
异常比例顾名思义,它不是去看你调用的快或慢,而是看你有没有抛异常。
只要你抛异常了,就算是有问题了,然后呢?异常比例也就是说只要抛异常的请求次数。超过了一定的比例,那就会触发熔断。
你比如说这里配了比例是0.4,然后呢,请求数量是十,统计时长是一,也就是在一秒钟内我统计十次请求,如果十次请求里有四次都抛了异常。那么就触发了我的熔断了,熔断时长五秒钟.
所以这个其实比慢调用那个要简单一些吧。但是这个是按异常比例来做熔断。
这里还有一个叫异常数,其实我讲到这,大家应该能猜测到了吧,那它是按照什么去做熔断?
它不是按比例,它是按固定的数值。
比如说我这配个二,就是说。我去统计一秒钟内十次请求,只要十次理由,两次抛异常,就算是达到预值了,所以一个是按比例的,一个是按固定值的。所以这俩其实是差不多的吧啊。
所以呢,这两个我们合到一起来说了啊,那下边我们通过一个案例啊来演示一下。
案例
需求:给 UserClient的查询用户接口设置降级规则,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5s。
1 设置异常请求
首先,修改user-service中的/user/{id}这个接口的业务。手动抛出异常,以触发异常比例的熔断:
2 设置熔断规则
下面,给feign接口设置降级规则:
规则:
在5次请求中,只要异常比例超过0.4,也就是有2次以上的异常,就会触发熔断。
3 测试
在浏览器快速访问:http://localhost:8088/order/102,快速刷新5次,触发熔断: