# Sentinel:访问限流

回顾前面笔记中的 “关于 Sentinel 的使用方式” 章节,在这里,我们在服务的 “被调方” 使用 Sentinel 整合 Spring MVC 进行流量控制。

在这里,Sentinel 借助 Spring MVC 框架的 “拦截器” 机制整合进入 Spring MVC ,“抢先” 在 Controller 执行之前进行流控(和熔断)的判断,从而决定当前请求是否被放行至 Controller 。

# 1. Sentinel 和 Spring MVC 整合

  1. 添加依赖

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    
    <!-- 其实真正起作用的是被关联引入的 sentinel-spring-webmvc-adapter 包 -->
    
  2. 添加配置(连接到 sentinel-dashboard)

    spring:
      cloud:
        sentinel:
          transport:
            dashboard: 127.0.0.1:8858
    
    # 日志设置
    logging:
      level:
        root: INFO
      pattern:
        console: "${CONSOLE_LOG_PATTERN:\
            %clr(${LOG_LEVEL_PATTERN:%5p}) \
            %clr(|){faint} \
            %clr(%-40.40logger{39}){cyan} \
            %clr(:){faint} \
            %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}}"
    
  3. 访问 sentinel-dashboard 可以看到类似如下页面:

    只需要完成上述的配置,代码不需要有任何的调整,我们就可以通过实时监控查看服务内的流量 QPS 以及平均响应时长等信息。

    警告

    只有服务接口被访问的情况下,在 sentinel 里面才可以看到监控信息。这可能会让你『等』一段时间。

    sentinel-plus-01

# 2. 几个概念和 name

  • 上下文( Context )和 context-name

    Context 代表调用链路上下文。在整个调用链路的开始处(即,前面章节所展示的 Sentinel 的执行流程的 ① 处),Sentinel 会创建上下文 Context 对象,并且为它指定一个 name 。

    在 Sentinel 中,不同的调用链路可能使用同一个上下文 Context 对象。在这里( 和 Spring MVC 整合 ),我们的调用链路都是在 sentinel_spring_web_context 中:

    sentinel-context

  • 资源(Resource)和 resource-name

    在 Sentinel 中,被 Sentinel try-catch 起来的 “真·代码”( 即,前面章节中所展示的 Sentinel 的执行流程的 ③ 处 )在 Sentinel 看来就是所谓的 “资源”,对于每一份资源,Sentinel 会为赋予一个 name(或者你手动指定)

    和 Spring MVC 整合时,Sentinel 使用的是 URI 来作为 Controller 方法的资源名( 在这里,Controller 方法就是资源 )

    sentinel-resource

# 3. 流控规则:QPS 限流

# 3.1 如何添加流控规则

在菜单左侧的 簇点链路 和流控规则都可以针对 服务接口 添加流控规则:

当我们的服务接口资源被访问的时候,就会出现在 簇点链路 列表中,我们可以针对该服务接口资源配置流程控制规则。

sentinel-02

在流控规则页面也有 新增流控规则 按钮,添加完成之后的流控规则,出现在流控规则页面列表中。

sentinel-03

# 3.2 QPS 流控

点击 新增流控规则 按钮之后,弹出如下的配置页面:

sentinel-04

  • 资源名称

    表示我们针对哪个接口资源进行流控规则配置,如:/departments/{id}

  • 针对来源

    表示针对哪一个服务访问当前接口资源的时候进行限流,default 表示不区分访问来源。

    如填写服务名称:xxx-service,表示 xxx-service 访问前接口资源的时候进行限流,其他服务访问该接口资源的时候不限流。

  • 阈值类型/单机阈值:QPS,每秒钟请求数量。上图配置表示每秒钟超过1次请求的时候进行限流。

  • 流控模式

    直接,当达到限流标准时就直接限流。

  • 流控效果:

    快速失败。很简单的说就是达到限流标准后,请求就被拦截,直接失败。(HTTP状态码:429 too many request)

  • 是否集群:

    默认情况下我们的限流策略都是针对单个服务的,sentinel 提供了集群限流的功能。

    除非你的微服务规模特别大,一般不要使用集群模式。集群模式需要各节点与 token server 交互才可以,会增加网络交互次数,一定程度上会拖慢你的服务响应时间。

上面的限流规则用一句话说:对于任何来源的请求,当超过每秒 2 次的标准之后就直接限流,访问失败抛出 BlockException 异常!

# 4. 流控规则:线程数限流

sentinel-05

  • 资源名称

    表示我们针对哪个接口资源进行流控规则配置,如:/departments/{id}

  • 针对来源

    表示针对哪一个服务访问当前接口资源的时候进行限流,default 表示不区分访问来源。

    如填写服务名称:xxxx-service ,表示 xxx-service 访问前接口资源的时候进行限流,其他服务访问该接口资源的时候不限流。

  • 阈值类型/单机阈值

    线程数。表示开启 n 个线程处理资源请求。

  • 流控模式

    直接,当所有线程都被占用时,新进来的请求就直接限流

  • 流控效果

    快速失败。很简单的说就是达到限流标准后,请求就被拦截,直接失败。(HTTP 状态码:429 too many request)

上面的限流规则用一句话说:对于任何来源的请求,department-service 服务端 /departments/{id} 资源接口的 2 个线程都被占用的时候,其他访问失败!

# 5. 关联限流

关联限流:/important 接口的重要程度要高于 /normal 接口,如果,/important 接口的访问压力很大,那么,可以『牺牲』掉 /normal 接口,全力保证 /important 接口的正常运行。

sentinel-06

上述的配置的意思是,如果 /important 的访问压力达到了每秒 1 次,那么就对 /normal 就会被限流,把 /normal 接口停掉,以保证硬件资源全力供应 /important 服务。

注意

上述是对 /normal 的配置,不需要去配置 /important 即可生效,也就是说,/important 可以没有限流规则。

# 6. 链路限流

链路限流和关联限流的思路很像,假设运营认为 /important 接口的重要程度要高于 /normal 接口,而且,它俩如果又都调用了同一个 Service 的方法,那么,我们可以『站在 Service 的方法』的角度上进行设置:如果,是 /normal 接口在调用 Service 方法,那么就进行限流,而 /important 接口的调用就不限流,或设置为更宽松一些的流控。

  1. 在 Service 的方法上使用注解 @SentinelResource

    @SentinelResource("doSomething")
    public String doSomething() {
        return "hello world";
    }
    
  2. 通过配置关闭 sentinel 的 URL 收敛功能:

    spring:
      cloud:
        sentinel:
          web-context-unify: false
    

    网上又资料显示该配置在低版本中无效,被认为是一个 bug ,不过现在高版本(spring-cloud-alibaba 2.2.2.RELEASE)中修正了这个 bug 。

    接下来,你会在 Sentinel 中看到 /important/normal 两个 URI 下都有一个 doSomething 。如下

    sentinel-07

  3. 现在你可以在流控规则中为 doSomething 添加流控规则。注意,是 doSomething ,而不是 /important/normal

    sentinel-08

  4. 分别触发 /important/normal,你会发现 /normal 有流量限制,而 /important 则没有。

# 7. 流控效果

  • 快速失败

    直接失败,抛出异常,不做任何额外的处理,是最简单、最直接、最常用的效果。

  • Warm up

    它从开始阈值到最大 QPS 阈值会有一个缓冲阶段,一开始的阈值是最大 QPS 阈值的 1/3 ,然后慢慢增长,直到达到最大阈值。

  • 排队等待

    让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排序等待。即,如果单机阈值为 N,那么没 1/N 秒有一个访问『资格』。没有『资格』时,排队等待,直到超时。

# 8. Sentinel 和 SpringMVC 整合原理

回顾一下我们之前所展示的 Sentinel 的底层执行流程:

1. 初始化上下文;
try {
  2. 熔断、流控逻辑的判断,判断当前请求是否能继续执行;
  3. 执行 “真·代码”;
} catch (BlockException e) {
  4. 上述第 2 步未能通过,会抛出 BlockException ,表示请求被拒绝
  return;
} catch (Exception e) {
  5. 业务异常。记录、统计异常信息
  throw e;
} finally {
  6. 收尾工作:曾经创建的资源该回收的回收,该清除的清除
}

之前有提到过,Sentinel 和 Spring MVC 的整合利用了 Spring MVC 的拦截器机制。

Sentinel 实现了一个名为 SentinelWebInterceptor 的拦截器,其逻辑伪代码如下:

public SentinelWebInterceptor implements HandlerInterceptor {

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            1. 初始化上下文;
            2. 熔断、流控逻辑的判断,判断当前请求是否能继续执行;
            return true; // 此时 Controller 方法会被调用。Controller 方法就是 3 。
        } catch (BlockException e) {
            4. 上述第 2 步未能通过,会抛出 BlockException ,表示请求被拒绝
            return false;
        }

    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {

    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
      if (发生了异常) {
          5. 业务异常。记录、统计异常信息
      }
      6. 收尾工作:曾经创建的资源该回收的回收,该清除的清除
    }

# 9. 自定义熔断返回信息

Sentinel 返回的默认信息是 Blocked by Sentinel (flow limiting) ,如果你对默认信息不满意,你可以自定义熔断返回信息。

Sentinel 提供了 BlockExceptionHandler 接口。当无论因何原因触发了 Sentinel 阻断用户的正常请求,Sentinel 都将『进入』到用户自定义的 BlockExceptionHandler 接口的实现类中,执行 handle 方法,并传入当前的请求、响应对象以及异常对象,并以 handle 方法的执行结果作为返回,回传给用户。

通过对 handle 方法的异常参数的判断,你可以直到当前发生了什么状况:

@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws Exception {
        String msg = null;
        if (ex instanceof FlowException) {
            msg = "限流了";
        } else if (ex instanceof DegradeException) {
            msg = "熔断了";
        } else {
            msg = "其它原因";
            // ParamFlowException   "热点参数限流";
            // SystemBlockException "系统规则(负载/...不满足要求)";
            // AuthorityException   "授权规则不通过";
        }
        // http 状态码
        response.setStatus(500);
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        response.setContentType("application/json;charset=utf-8");
        // 利用 spring mvc 默认的 json 库 jackson
        new ObjectMapper().writeValue(response.getWriter(), msg);
    }
}

需要说明的是:不止因为熔断这一个原因会导致 BlockExceptionhandlerhandle 方法的执行,因此,需要对 handle 方法的 BlockException 参数对象进行 instanceof 判断,熔断对应的异常类型正是 DegradeException