Sentinel

69 阅读10分钟

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

Sentinel的特征

  • 丰富的应用场景。控制突发流量在可控制的范围内,消息削峰填谷,集群流量控制,实时熔断下游不可用的应用等等。
  • 完备的实时监控。Sentinel 提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态。Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  • 完善的 SPI 扩展点。Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

下载Sentinel的控制台

Sentinel官方提供了UI控制台,方便我们对系统做限流设置。

可以到github的官网去下载: github.com/alibaba/Sen…

下载下来就是一个jar包

  1. 将其拷贝到一个你能记住的非中文目录,然后运行命令
java -jar 你的jar包名称.jar
  1. 然后访问:localhost:8080 即可看到控制台页面,默认的账户和密码都是 sentinel

这个时候控制台还没有任何信息,因为还没有和我们的服务进行整合

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

配置项默认值说明
server.port8080服务端口
sentinel.dashboard.auth.usernamesentinel默认用户名
sentinel.dashboard.auth.passwordsentinel默认密码

在原来的命令后面再加上 -D和配配置项的名称和值即可

java -Dserver.port=8090 -jar 你的jar包名称.jar

微服务整合Sentinel

在我们的微服务中整合Sentinel,并且连接Sentinel的控制台,步骤如下:

  1. 引入sentinel依赖:
<dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
      <version>2023.0.1.0</version>
</dependency>
  1. 配置控制台地址:
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
  1. 重启微服务,访问微服务的任意端点,触发sentinel监控

image.png

先访问以下任意接口,再来Sentinel的控制台刷新以下,就可以看到具体的一些信息了

image.png


限流规则

在说限流规则之前,我们先来说一下什么是簇点链路

簇点链路

簇点链路: 就是项目内的调用链路,链路中被监控的每个接口就是一个资源。默认情况下 sentinel 会监控 SpringMVC 的每一个端点(Endpoint),因此 SpringMVC 的每一个端点(Endpoint)就是调用链路中的一个资源。

端点可以理解成 Controller 中的方法

在我们的项目中,请求进来会调用 Controller 的方法,Controller 又调用 Service,Service 又调用用 Mapper,这样就形成了一个调用链路。

image.png

流控、熔断都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则

流控规则入门案例

要求:给 /order/hello 这个资源设置流控规则,QPS 不能超过 200.然后利用 jemeter 测试。

点击流控按钮就会有这个设置流控规则的窗口,我这里设置了 QPS 的单机阈值为 200,然后点击新增即可

image.png

这里可以看到,被拒绝的 QPS 数量

image.png

限流的高级选项

image.png

点击流控规则窗口中的高级选项,会多出来两个配置:流控模式、流控效果

  • 流控模式

    • 直接: 统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式(A触发阈值,对A做限流)
    • 关联: 统计当前资源相关的另一个资源,触发阈值时,对当前资源限流(A触发阈值,对B做限流)
    • 链路: 统计从指点链路访问到本资源的请求,触发阈值时,对指定链路限流(A触发阈值,对请求来源做限流)

    使用说明

    关联模式:

    当 /order/test 资源访问量触发阈值时,就会对 /order/hello 资源限流,避免影响到 /order/test

    当两个资源有竞争关系,一个优先级高,一个优先级低,可以选择关联模式

image.png

链路模式:

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

如果只希望统计 /test2 进入到当前资源的请求,可以这样配置

image.png

这里介绍一个注解@SentinelResource("hello") 这个注解可以标记一个资源,括号中的 hello 就是这个资源的名字,因为 Sentinel 默认加载的资源是Contoller中的方法,所以当有 Service 获取其他的方法需要做限流是,可以使用这个注解。

@SentinelResource("hello")
public String sayHello(String name) {
     System.out.println("hello " + name);
     return "调用了sayHello方法";
}

Sentinel默认会将Controller中的方法做context整合,导致链路模式的流控失效,需要修改application.yml,添加配置:

spring:
    sentinel:
      web-context-unify: false
  1. 流控效果

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

  • 快速失败: 达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。收默认处理方式。
  • warm up: 预热模式,对超出阈值的请求同样是拒绝并抛出异常。但是这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。

warm up模式也叫做预热模式,是应对冷启动的一种方案。请求阈值的初始值是 threshold/coldFactor,持续指定时长后逐渐提高到threshold(最大阈值)值。而coldFactory的默认值是3.

例如:我设置的 QPS 的 threshold 为 10 ,预热时间为 5 秒,那么初始阈值就是 10/3,也就是 3 ,然后在 5 秒后逐渐增长到 10

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

当请求超过 QPS 阈值时,快速失败和 warm up 会拒绝新的请求并抛出异常。而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔一次执行。后来的请求必须等待前面执行完成,如果请求预期等待的时间超出最大时长,则会被拒绝。

热点参数限流

之前的限流是统计访问某个资源的所有请求,判断是否超过 QPS 阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过 QPS 阈值。

比如说现在有多个请求来访问 /goods/{id} 资源,id会有不同的值 ,QPS 统计的时候就会分开统计了,

参数值QPS
id = 13
id = 21

配置示例:

image.png

image.png

在热点参数限流的高级选项中,可以对部分参数设置例外的配置:

注意: 热点参数资源对默认的 SpringMVC 资源无效需要加 @SentinelResource("hello") 注解

隔离和降级

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

不管是线程隔离还是熔断降级,都是对**客户端(调用方)**的保护。

  1. 线程隔离

线程隔离有两种方式实现:

  • 线程池隔离

加入现在有个服务A,它依赖于服务C和服务D,现在来了一个请求,这个请求需要分别调用服务C和服务D,那么就会分别给服务C和服务D创建线程池,发起调用的时候,分别从这两个线程池中取线程发起调用,这样两个服务就隔离开了。把故障隔离在一个范围内。

  • 信号量隔离(默认)

通过控制对共享资源的访问数量,限制了同时访问该资源的线程数量。不同的线程可以共享同一个资源,但是同时访问该资源的线程数量受到信号量的限制。

并发线程数:就是该资源能使用的线程数的最大值。也就是通过限制线程数数量,实现舱壁模式。超出的线程会被拒绝。

image.png

  1. 熔断降级

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

断路器的工作流程如下所示:

这里面有两个比较关键的属性需要我们来配置:熔断时间和失败阈值(熔断策略)

image.png

熔断策略

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

  • 慢调用: 业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。例如:

解读:

RT 超过500ms的调用是慢调用,统计最近10000ms内的请求,如果请求超过10次,并且慢调用比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。

image.png

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

解读: 统计最近 1000ms 内的请求,如果请求量超过10次,并且异常比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态。放行一次请求做测试。

image.png

统计最近 1000ms 内的请求,如果请求量超过10次,并且异常数不低于2,则触发熔断,熔断时长为5秒。然后进入half-open状态。放行一次请求做测试。

image.png

授权规则

image.png

授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。

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

是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的:

image.png

这个方法的作用就是从request对象中,获取请求者的origin值并返回。 默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。 因此,我们需要自定义这个接口的实现,让不同的请求,返回不同的origin。

@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;
    }
}

自定义异常

默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。如果要自定义异常时的返回结果,需要实现 BlockExceptionHandler接口

BlockException 子类

image.png

@Component
// 可以写自己的一个异常处理的逻辑,根据不同的情况抛出不同的异常
public class BlockExceptionHandlerImpl implements BlockExceptionHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, 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 + "}");
    }
}