q:什么是雪崩问题?
a:在微服务中,服务间的调用关系非常复杂,一个服务往往依赖多个其他服务。如果调用链中一个服务故障,则会引起整个调用链都无法访问,这个就是雪崩问题。
雪崩问题的解决方案一般有四种:分别为超时处理,舱壁模式(线程隔离),降级熔断,流量控制,其中流量控制可以避免因瞬间高并发流量而导致的服务器故障。
QPS:每秒请求次数
通过四种方法的描述,我们可以认为:
限流是对服务的保护,避免因瞬间高并发流量而导致服务故障,进而避免雪崩。是一种预防措施。
超时处理、线程隔离、降级熔断是在部分服务故障时,将故障控制在一定范围,避免雪崩。是一种补救措施。
目前国内比较流行的服务保护技术还是Sentinel框架,接下来,我会说明如何进行sentinel的下载与安装。
1)下载
Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址:https://sentinelguard.io/zh-cn/index.html
我们安装可以通过官网地址跳转到github,点击releases,tag来选择自己想要安装的版本。
2)运行
将jar包放到任意非中文目录,执行命令:
java -jar sentinel-dashboard-1.8.1.jar
如果要修改Sentinel的默认端口、账户、密码,可以通过下列配置:
因为本身是jar包,所以修改不了配置文件,我们可以通过java -d 的参数来进行修改,例如修改端口号为8888
java -jar sentinel-dashboard-1.8.1.jar -D server.port:8888
3)访问
访问http://localhost:8080页面,就可以看到sentinel的控制台了:
这样sentinel就下载安装好了。但是需要进行监控与流量控制,我们还需要绑定相应的微服务,所以要在有需要的服务上引入依赖,修改配置文件,进行整合。
我们在order-service中整合sentinel,并连接sentinel的控制台,步骤如下:
1)引入sentinel依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
2)配置控制台
修改application.yaml文件,添加下面内容(如果是在nacos配的,要去nacos配置中心进行修改):
server:
port: 8081
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
3)访问对应的服务的任意端点
这里我是用了一个名叫userservice的服务,我通过访问localhost:8081/user/2来触发sentinel的监控,这个时候我们访问sentinel控制台,如图:
这样我们就将我们对应的服务与Sentinel整合好了。
首先我们知道,当请求进入服务后,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源。
默认情况下,Sentinel只会监控Controller中的方法。
我们登录到Sentinel控制台,我们能看到服务点开后,左边有簇点链路,如图:
我们可以看到后面操作那一列,都是针对簇点链路的资源来设置的,因此我们可以通过点击这些按钮来设置我们想要的规则:
那如果我们想要进行流量控制,我们就可以点击资源/order/{orderId}后面的流控按钮,就可以弹出表单,表单中可以填写限流规则,如下:
这个表单的意思是限制 /order/{orderId}这个资源的单机QPS为5,即每秒只允许5次请求,超出的请求会被拦截并报错。
点击保存后,我们可以用jmeter进行测试,jmeter的基本使用在我的另一篇中有详细说明,不知道的小伙伴可以去看看。
链接: Jmeter快速入门(无介绍,纯干货,只基本使用)
运行过后,我们查看结果树,则可以看到,1s中如果有超过5次的请求,则会被拦截,相应过来的数据如图:
刚刚上述例子,只是最基本的流控,在流控规则的配置中,还有高级选项,如图:
一般默认的都是直接模式。
关联模式:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
上图说明:当/order/update资源访问量触发阈值时,就会对/order/query资源限流,避免影响/order/update资源。
使用场景:比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是优先支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。
简单来说,满足下面条件可以使用关联模式:
链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。
实例:
比如:有查询订单和创建订单两种,两种都需要对订单里面的商品进行查询,所以两种都会经过查询商品这一链路,现在我们需要针对从查询订单进入到查询商品的请求统计,并设置限流。
步骤:
上述都无需实现业务,只是创建,调用即可,所以就不截图代码部分了。
但是上述步骤都完成之后,我们会发现一个问题,之前我们说到簇点链路的时候说到过,Sentinel默认会给进入SpringMVC的所有请求(也就是Controller方法)设置同一个root资源,会导致链路模式失效。
我们需要关闭这种对SpringMVC的资源聚合,修改order-service服务的application.yml文件:
spring:
cloud:
sentinel:
# 关闭context整合
web-context-unify: false
重启服务,访问/order/query和/order/save,可以查看到sentinel的簇点链路规则中,出现了新的资源:
接下来我们添加流控规则
点击goods资源后面的流控按钮,在弹出的表单中填写下面信息:
上述规则的含义是:只统计从/order/query进入/goods的资源,QPS阈值为2,超出则被限流。而从/order/save进入/goods的资源则没有这个限制。
流控模式有哪些?
•直接:对当前资源限流
•关联:高优先级资源触发阈值,对低优先级资源限流。
•链路:阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流
在流控的高级选项中,还有一个流控效果选项:
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。
warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
默认的是快速失败
warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 maxThreshold / coldFactor,持续指定时长后,逐渐提高到maxThreshold值。而coldFactor的默认值是3
为什么要将阈值初始值设置为maxThreshold / coldFactor?是因为阈值一般是一个微服务能承担的最大QPS,但是一个服务刚刚启动时,一切资源尚未初始化(冷启动),如果直接将QPS跑到最大值,可能导致服务瞬间宕机。
例如,我设置QPS的maxThreshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10.
案例
需求:给/order/{orderId}这个资源设置限流,最大QPS为10,利用warm up效果,预热时长为5秒
用Jmeter测试后,我们观察Sentinel实时监控,可以发现:
在五s的这个区间,一直在慢慢的增加,在5s预热完成后,则是直接到了10
当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。
而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
工作原理
例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常。
那什么叫做预期等待时长呢?3
比如现在一下子来了12 个请求,因为每200ms执行一个请求,那么:
现在,第1秒同时接收到10个请求,但第2秒只有1个请求,此时QPS的曲线这样的:
如果使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑:
平滑的QPS曲线,对于服务器来说是更友好的。
案例
需求:给/order/{orderId}这个资源设置限流,最大QPS为10,利用排队的流控效果,超时时长设置为5s
1)添加流控规则
注意:超时时间单位是ms
通过Jmeter测试,得到的实时监控图是这样的:
QPS非常的平滑,一致保持在10,但是当前请求数量超过了QPS的最大值并不会直接被拒绝,而是放入队列,所以他的后续请求响应的时间越来越长,但是当队列中有超过5s还未得到响应的请求,我们会将他拒绝,所以才会有部分请求失败。
流控效果有哪些?
我们上面讲的三种限流方式,都是对一个请求的统一限流。而热点参数限流则是分别统计参数值相同的请求,也就是说,之前提到的/order/{orderId},我们可以通过传递的不同的{orderId}来进行限流。
注意:在配置的时候,因为热点参数限流对默认的SpringMVC资源无效,需要利用@SentinelResource注解标记资源。
案例需求:给/order/{orderId}这个资源添加热点参数限流,规则如下:
•默认的热点参数规则是每1秒请求量不超过2
•给102这个参数设置例外:每1秒请求量不超过4
•给103这个参数设置例外:每1秒请求量不超过10
1)标记资源
给order-service中的OrderController中的/order/{orderId}资源添加注解:
2)热点参数限流规则
这里不要点击hot后面的按钮,页面有BUG
点击左侧菜单中热点规则菜单:
填写表单:
这样就配置好了。
流量控制是预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。
比如服务A,要调用服务B与服务C。那么线程隔离就会将这两个业务,分别分配10个线程,如果服务C调用一直失败,也只会消耗十个线程,可以有效避免,因为单个服务的故障,导致所有资源被耗尽。
比如服务A,调用服务B,如果调用次数成功了4次,失败了6次,失败比例60%,超过了设置的比例,则会被断路器,熔断该业务请求。
线程隔离以及熔断降级,都是对调用方的保护,是需要在调用方发起远程调用的时候做线程隔离、熔断降级。
一般在Spring-Cloud的服务间的远程调用都是基于Feign来完成的,因此需要将Feign与Sentinel整合,在Feign里面实现线程隔离和服务熔断。(Dubbo也可以整合,但是下面主要演示Feign整合)
修改OrderService的application.yml文件,开启Feign的Sentinel功能:
feign:
sentinel:
#开启feign对sentinel的支持
enabled: true
业务失败后,不能直接报错,而应该返回用户一个友好提示或者默认结果,这个就是失败降级逻辑。
给FeignClient编写失败后的降级逻辑
①方式一:FallbackClass,无法对远程调用的异常做处理
②方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种
这里我们演示方式二的失败降级处理。
首先展示一下我的feign-api的项目结构
步骤一:在feing-api项目中定义类,实现FallbackFactory:
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
return new UserClient() {
@Override
public User findById(Long id) {
log.error("查询用户异常", throwable);
return new User();
}
};
}
}
**步骤二:**在feing-api项目中的DefaultFeignConfiguration类(配置类)中将UserClientFallbackFactory注册为一个Bean:
步骤三:在feing-api项目中的UserClient接口中使用UserClientFallbackFactory:
重启后,再访问一次订单查询业务,然后查看sentinel控制台,可以看到新的簇点链路:
我们没有整合之前,是没有其它服务的路径的,通过Feigin整合Sentinel后,我们可以将完整的调用链展示出来,哪怕是远程调用的。
Sentinel支持的雪崩解决方案:
Feign整合Sentinel的步骤:
案例需求:给 order-service服务中的UserClient的查询用户接口设置流控规则,线程数不能超过 2。然后利用jemeter测试。
1)配置隔离规则
选择feign接口后面的流控按钮:
填写表单
然后配置完成后,通过Jmeter测试:
我这里是1s内发送10次请求,我们可以发现都能成功访问,不过部分请求得到的响应是降级返回的null信息。
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
状态机包括三个状态:
断路器熔断策略有三种:慢调用、异常比例、异常数
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
案例
需求:给 UserClient的查询用户接口设置降级规则,慢调用的RT阈值为**50ms,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5**
1)设置慢调用
为了让响应时间变慢,我们可以在代码中,添加休眠
我们可以访问一下,如果订单查询orderId = 101,则远程调用查询1号用户,F12打开控制台,我们可以看到调用时间,远远超过50ms
而我们访问订单orderId = 102,则远程调用查询2号用户(并不会执行刚刚代码里加的休眠)。我们可以看到调用时间非常快。
2)设置熔断规则
下面,给feign接口设置降级规则:
填写表单
3)测试
在浏览器访问:http://localhost:8088/order/101,快速刷新5次,可以发现:
触发了熔断,请求时长缩短至5ms,快速失败了,并且走降级逻辑,返回的null
注意: 因为熔断的是链路,并不是单个id,所以不管传任何Id,哪怕响应时长不超过50ms的id过来,也是会被降级,返回null,只有过了我们设置的熔断时长之后,才能尝试断路器切换到Closed状态。
在浏览器访问:http://localhost:8088/order/102,竟然也被熔断了:
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
例如:
一个异常比例设置:
解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.4,则触发熔断。
一个异常数设置:
解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于2次,则触发熔断。
因为这两个相差不大,我们演示一下异常比例配置。
案例
需求:给 UserClient的查询用户接口设置降级规则,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5s
1)设置异常请求
首先,修改user-service中的/user/{id}这个接口的业务。手动抛出异常,以触发异常比例的熔断:
也就是说,id 为 2时,就会触发异常(对应访问orderId为102)
2)设置熔断规则
给feign接口设置降级规则:
填写表单
在5次请求中,只要异常比例超过0.4,也就是有2次以上的异常,就会触发熔断。
3)测试
在浏览器快速访问:http://localhost:8088/order/102,快速刷新5次,触发熔断:
此时,我们去访问本来应该正常的103:
上面说过,必须过了熔断时长后,断路器才会再次尝试Closed,所以现在哪怕访问正常的,也是熔断的。
授权规则可以对请求方来源做判断和控制。有白名单和黑名单两种方式。
比如:
我们允许请求从gateway(来访者)到order-service(受保护资源),不允许浏览器访问order-service(来访者),那么白名单中就要填写网关的来源名称(origin)。
Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。
public interface RequestOriginParser {
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
这个方法的作用就是从request对象中,获取请求者的origin值并返回。
默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。
因此,我们需要自定义这个接口的实现,让不同的请求,返回不同的origin。
例如order-service服务中,我们定义一个RequestOriginParser的实现类:
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 1.获取请求头
String origin = request.getHeader("origin");
// 2.非空判断
if (StringUtils.isEmpty(origin)) {
origin = "blank";
}
return origin;
}
}
这一段的代码的意思是:尝试从request-header中获取origin值。
既然获取请求origin的方式是从reques-header中获取origin值,我们必须让所有从gateway路由到微服务的请求都带上origin头。
这个需要一个GatewayFilter来实现,AddRequestHeaderGatewayFilter。
修改gateway服务中的application.yml,添加一个defaultFilter:
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=origin,gateway
routes:
# ...略
这样,从gateway路由的所有请求都会带上origin头,值为gateway。而从其它地方到达微服务的请求则没有这个头。
接下来,我们添加一个授权规则,放行origin值为gateway的请求。
配置如下:
配置好后,如果我们跳过网关,直接访问order-service服务:
这时候,通过网关访问:
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。
而如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口:
public interface BlockExceptionHandler {
/**
* 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
*/
void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}
这个方法有三个参数:
这里的BlockException包含多个不同的子类:
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
下面,我们就在order-service定义一个自定义异常处理类:
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "请求被热点参数限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级了";
} else if (e instanceof AuthorityException) {
msg = "没有权限访问";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
重启测试,在不同场景下,会返回不同的异常消息。
限流时:
授权拦截时: