微服务保护&Sentinel基本使用(未实现规则持久化)

发布于:2022-11-09 ⋅ 阅读:(13) ⋅ 点赞:(0) ⋅ 评论:(0)

一、Sentinel简介

1.雪崩问题及解决方案

1.1.雪崩问题

q:什么是雪崩问题?

a:在微服务中,服务间的调用关系非常复杂,一个服务往往依赖多个其他服务。如果调用链中一个服务故障,则会引起整个调用链都无法访问,这个就是雪崩问题。

1.2.解决方案

雪崩问题的解决方案一般有四种:分别为超时处理,舱壁模式(线程隔离),降级熔断,流量控制,其中流量控制可以避免因瞬间高并发流量而导致的服务器故障。

  1. 超时处理:设定超时时间,请求超时没有相应就会返回错误信息,不会无休止等待
  2. 舱壁模式:也称为线程隔离。我们可以限定每个业务能使用的线程数,避免耗尽整个tomcat的资源。
  3. 降级熔断:由断路器统计业务执行的异常比例,如果超过规定值,则拦截访问该业务的一切请求。
  4. 流量控制:限制业务访问的QPS,避免服务因流量的突增而故障

QPS:每秒请求次数

通过四种方法的描述,我们可以认为:

限流是对服务的保护,避免因瞬间高并发流量而导致服务故障,进而避免雪崩。是一种预防措施。

超时处理、线程隔离、降级熔断是在部分服务故障时,将故障控制在一定范围,避免雪崩。是一种补救措施。

2.Sentinel介绍与安装

目前国内比较流行的服务保护技术还是Sentinel框架,接下来,我会说明如何进行sentinel的下载与安装。

2.1.安装Sentinel

1)下载

Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址:https://sentinelguard.io/zh-cn/index.html

我们安装可以通过官网地址跳转到github,点击releases,tag来选择自己想要安装的版本。

在这里插入图片描述

2)运行

将jar包放到任意非中文目录,执行命令:

java -jar sentinel-dashboard-1.8.1.jar

如果要修改Sentinel的默认端口、账户、密码,可以通过下列配置:

  • server.port:8080 服务端口
  • sentinel.dashboard.auth.username:sentinel 默认用户名
  • sentinel.dashboard.auth.password:sentinel 默认密码

因为本身是jar包,所以修改不了配置文件,我们可以通过java -d 的参数来进行修改,例如修改端口号为8888

java -jar sentinel-dashboard-1.8.1.jar  -D  server.port:8888

3)访问

访问http://localhost:8080页面,就可以看到sentinel的控制台了:

在这里插入图片描述

这样sentinel就下载安装好了。但是需要进行监控与流量控制,我们还需要绑定相应的微服务,所以要在有需要的服务上引入依赖,修改配置文件,进行整合。

2.2.微服务整合Sentinel

我们在order-service中整合sentinel,并连接sentinel的控制台,步骤如下:

1)引入sentinel依赖

<!--引入sentinel依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId> 
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</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整合好了。

二、流量控制

1.簇点链路

首先我们知道,当请求进入服务后,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源

默认情况下,Sentinel只会监控Controller中的方法。

1.1.示例

我们登录到Sentinel控制台,我们能看到服务点开后,左边有簇点链路,如图:

在这里插入图片描述

我们可以看到后面操作那一列,都是针对簇点链路的资源来设置的,因此我们可以通过点击这些按钮来设置我们想要的规则:

  • 流控:流量控制
  • 降级:降级熔断
  • 热点:热点参数限流,是限流的一种
  • 授权:请求的权限控制

那如果我们想要进行流量控制,我们就可以点击资源/order/{orderId}后面的流控按钮,就可以弹出表单,表单中可以填写限流规则,如下:

在这里插入图片描述

这个表单的意思是限制 /order/{orderId}这个资源的单机QPS为5,即每秒只允许5次请求,超出的请求会被拦截并报错。

点击保存后,我们可以用jmeter进行测试,jmeter的基本使用在我的另一篇中有详细说明,不知道的小伙伴可以去看看。

链接: Jmeter快速入门(无介绍,纯干货,只基本使用)

运行过后,我们查看结果树,则可以看到,1s中如果有超过5次的请求,则会被拦截,相应过来的数据如图:

在这里插入图片描述

2.流控模式

刚刚上述例子,只是最基本的流控,在流控规则的配置中,还有高级选项,如图:

在这里插入图片描述

一般默认的都是直接模式。

2.1.关联模式

关联模式:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流

在这里插入图片描述

上图说明:当/order/update资源访问量触发阈值时,就会对/order/query资源限流,避免影响/order/update资源。

使用场景:比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是优先支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。

简单来说,满足下面条件可以使用关联模式:

  • 两个有竞争关系的资源
  • 一个优先级高,一个优先级低

2.2.链路模式

链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。

实例

比如:有查询订单和创建订单两种,两种都需要对订单里面的商品进行查询,所以两种都会经过查询商品这一链路,现在我们需要针对从查询订单进入到查询商品的请求统计,并设置限流。

步骤:

  1. 在OrderService中添加一个queryGoods方法
  2. 在OrderController中,创建/order/query端点,调用OrderService中的queryGoods方法
  3. 在OrderController中添加一个/order/save的端点,调用OrderService的queryGoods方法
  4. 给queryGoods设置限流规则,从/order/query进入queryGoods的方法限制QPS必须小于2

上述都无需实现业务,只是创建,调用即可,所以就不截图代码部分了。

但是上述步骤都完成之后,我们会发现一个问题,之前我们说到簇点链路的时候说到过,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的资源则没有这个限制。

2.3.总结

流控模式有哪些?

•直接:对当前资源限流

•关联:高优先级资源触发阈值,对低优先级资源限流。

•链路:阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流

3.流控效果

在流控的高级选项中,还有一个流控效果选项:

在这里插入图片描述

流控效果是指请求达到流控阈值时应该采取的措施,包括三种:

  • 快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。

  • warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。

  • 排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长

默认的是快速失败

3.1.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

在这里插入图片描述

3.2.排队等待

当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。

而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。

工作原理

例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常。

那什么叫做预期等待时长呢?3

比如现在一下子来了12 个请求,因为每200ms执行一个请求,那么:

  • 第6个请求的预期等待时长 = 200 * (6 - 1) = 1000ms
  • 第12个请求的预期等待时长 = 200 * (12-1) = 2200ms

现在,第1秒同时接收到10个请求,但第2秒只有1个请求,此时QPS的曲线这样的:

在这里插入图片描述

如果使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑:

在这里插入图片描述

平滑的QPS曲线,对于服务器来说是更友好的。

案例

需求:给/order/{orderId}这个资源设置限流,最大QPS为10,利用排队的流控效果,超时时长设置为5s

1)添加流控规则

在这里插入图片描述

注意:超时时间单位是ms

通过Jmeter测试,得到的实时监控图是这样的:

在这里插入图片描述

QPS非常的平滑,一致保持在10,但是当前请求数量超过了QPS的最大值并不会直接被拒绝,而是放入队列,所以他的后续请求响应的时间越来越长,但是当队列中有超过5s还未得到响应的请求,我们会将他拒绝,所以才会有部分请求失败。

3.3.总结

流控效果有哪些?

  • 快速失败:QPS超过阈值时,拒绝新的请求
  • warm up: QPS超过阈值时,拒绝新的请求;QPS阈值是逐渐提升的,可以避免冷启动时高并发导致服务宕机。
  • 排队等待:请求会进入队列,按照阈值允许的时间间隔依次执行请求;如果请求预期等待时长大于超时时间,直接拒绝

4.热点参数限流

我们上面讲的三种限流方式,都是对一个请求的统一限流。而热点参数限流则是分别统计参数值相同的请求,也就是说,之前提到的/order/{orderId},我们可以通过传递的不同的{orderId}来进行限流。

注意:在配置的时候,因为热点参数限流对默认的SpringMVC资源无效,需要利用@SentinelResource注解标记资源。

4.1.实例:

案例需求:给/order/{orderId}这个资源添加热点参数限流,规则如下:

默认的热点参数规则是每1秒请求量不超过2

•给102这个参数设置例外:每1秒请求量不超过4

•给103这个参数设置例外:每1秒请求量不超过10

1)标记资源

给order-service中的OrderController中的/order/{orderId}资源添加注解:

在这里插入图片描述

2)热点参数限流规则

访问该接口,可以看到我们标记的hot资源出现了:
在这里插入图片描述

这里不要点击hot后面的按钮,页面有BUG

点击左侧菜单中热点规则菜单:

在这里插入图片描述

填写表单:

在这里插入图片描述

这样就配置好了。

三、隔离和降级

流量控制是预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。

  • 线程隔离:调用者在调用服务提供者时,给每个调用的请求分配独立线程池,出现故障时,最多消耗这个线程池内资源,避免把调用者的所有资源耗尽。

比如服务A,要调用服务B与服务C。那么线程隔离就会将这两个业务,分别分配10个线程,如果服务C调用一直失败,也只会消耗十个线程,可以有效避免,因为单个服务的故障,导致所有资源被耗尽。

  • 熔断降级:是在调用方这边加入断路器,统计对服务提供者的调用,如果调用的失败比例过高(失败/调用总次数),则熔断该业务,不允许访问该服务的提供者了。

比如服务A,调用服务B,如果调用次数成功了4次,失败了6次,失败比例60%,超过了设置的比例,则会被断路器,熔断该业务请求。

线程隔离以及熔断降级,都是对调用方的保护,是需要在调用方发起远程调用的时候做线程隔离、熔断降级。

一般在Spring-Cloud的服务间的远程调用都是基于Feign来完成的,因此需要将Feign与Sentinel整合,在Feign里面实现线程隔离和服务熔断。(Dubbo也可以整合,但是下面主要演示Feign整合)

1.Feign整合Sentinel

1.1.修改调用者服务的配置文件

修改OrderService的application.yml文件,开启Feign的Sentinel功能:

    feign:
      sentinel:
        #开启feign对sentinel的支持
        enabled: true 

1.2.编写失败降级逻辑

业务失败后,不能直接报错,而应该返回用户一个友好提示或者默认结果,这个就是失败降级逻辑。

给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后,我们可以将完整的调用链展示出来,哪怕是远程调用的。

1.3.总结

Sentinel支持的雪崩解决方案:

  • 线程隔离(仓壁模式)
  • 降级熔断

Feign整合Sentinel的步骤:

  • 在application.yml中配置:feign.sentienl.enable=true
  • 给FeignClient编写FallbackFactory并注册为Bean
  • 将FallbackFactory配置到FeignClient

2.sentinel的线程隔离

案例需求:给 order-service服务中的UserClient的查询用户接口设置流控规则,线程数不能超过 2。然后利用jemeter测试。

1)配置隔离规则

选择feign接口后面的流控按钮:

在这里插入图片描述

填写表单

在这里插入图片描述

然后配置完成后,通过Jmeter测试:

在这里插入图片描述

我这里是1s内发送10次请求,我们可以发现都能成功访问,不过部分请求得到的响应是降级返回的null信息。

3.熔断降级

熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。

断路器控制熔断和放行是通过状态机来完成的:
在这里插入图片描述

状态机包括三个状态:

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
    • 请求成功:则切换到closed状态
    • 请求失败:则切换到open状态

断路器熔断策略有三种:慢调用、异常比例、异常数

3.1.慢调用

慢调用:业务的响应时长(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,竟然也被熔断了:

在这里插入图片描述

3.2.异常比例、异常数

异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。

例如:

一个异常比例设置:

在这里插入图片描述

解读:统计最近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,所以现在哪怕访问正常的,也是熔断的。

四、授权规则

授权规则可以对请求方来源做判断和控制。有白名单和黑名单两种方式。

  • 白名单:来源(origin)在白名单内的调用者允许访问
  • 黑名单:来源(origin)在黑名单内的调用者不允许访问

点击左侧菜单的授权,可以看到授权规则:
在这里插入图片描述

比如:

在这里插入图片描述

我们允许请求从gateway(来访者)到order-service(受保护资源),不允许浏览器访问order-service(来访者),那么白名单中就要填写网关的来源名称(origin)。

1.获取流控应用(origin)

1.1.RequestOriginParser接口

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值。

1.2.给网关(流控应用)添加请求头

既然获取请求origin的方式是从reques-header中获取origin值,我们必须让所有从gateway路由到微服务的请求都带上origin头。

这个需要一个GatewayFilter来实现,AddRequestHeaderGatewayFilter。

修改gateway服务中的application.yml,添加一个defaultFilter:

spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=origin,gateway
      routes:
       # ...略

这样,从gateway路由的所有请求都会带上origin头,值为gateway。而从其它地方到达微服务的请求则没有这个头。

2.配置授权规则

接下来,我们添加一个授权规则,放行origin值为gateway的请求。

点击授权
在这里插入图片描述

配置如下:

在这里插入图片描述

3.测试

配置好后,如果我们跳过网关,直接访问order-service服务:

在这里插入图片描述

这时候,通过网关访问:

在这里插入图片描述

五、自定义异常结果

默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。

1.BlockExceptionHandler接口

而如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口:

public interface BlockExceptionHandler {
    /**
     * 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
     */
    void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}

这个方法有三个参数:

  • HttpServletRequest request:request对象
  • HttpServletResponse response:response对象
  • BlockException e:被sentinel拦截时抛出的异常

这里的BlockException包含多个不同的子类:

异常 说明
FlowException 限流异常
ParamFlowException 热点参数限流的异常
DegradeException 降级异常
AuthorityException 授权规则异常
SystemBlockException 系统规则异常

2.自定义异常处理(实现BlockExceptionHandler接口)

下面,我们就在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 + "}");
    }
}

3.测试

重启测试,在不同场景下,会返回不同的异常消息。

限流时:

在这里插入图片描述

授权拦截时:

在这里插入图片描述