【限流与Sentinel超详细分析】

Sentinel

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来保障微服务的稳定性。

1 Sentinel 基本概念

资源 (Resource):

  • 资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码;
  • 只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,服务名称作为资源名来标示资源。

规则 (Rule):围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。

2 Sentinel核心功能

2.1 流量控制

任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求进行整型,如下图所示

在这里插入图片描述

流量控制主要有如下三个角度:

  • 限流指标,例如QPS,并发线程数等指标
  • 限流策略,例如直接限流(根据自己的指标来限流,关联限流(根据有关联关系的其他资源来限流),调用链路限流(根据指定路调用链路涌入的的流量来限流)
  • 限流效果,触发限流以后的请求该如何处理, 例如快速失败(fast fail),慢启动(warm up), 排队等待

2.2 熔断降级

除了流量控制以外,对调用链路中的不稳定资源进行处理, 保护整个调用链路, 也是 Sentinel 的作用之一。

2.2.1 熔断

现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。

在服务内调用其它服务时, 比如基于 RestTemplate 进行调用时, 是同步式的调用, 发起调用的服务在被调用的服务返回前, 不能去做别的事情;

如果下游服务出现故障, 迟迟无法返回, 那么上游服务就被阻塞在调用下游服务的位置

随着新的请求不断到来, 将有越来越多的上游服务线程被阻塞在调用故障服务的位置, 进而使上游服务没有线程可用, 最终使上游服务也发生故障,

以此类推, 导致整个调用链路中的服务都不可用, 造成雪崩;

预防雪崩问题有以下几个方法:

  1. 超时时间: 设定超时时间, 请求超过一定时间没有响应, 就立即返回一个错误状态, 不会无休止等待;

    缺点是仍然会有请求到达故障, 并且数量没有限制, 服务器的线程被阻塞直到超时时间才被释放;

  2. 线程隔离: 遵循仓壁模式, 对当前服务的不同API, 设置其所能占用的并发线程数量的上限, 这样, 即使当前 API 内调用了某个故障的下游服务, 也不会耗尽当前服务的所有线程资源;

    sentinel中, 配置限流规则时, 设置最大线程数, 就有线程隔离的作用;
    缺点是仍然有一定数量的请求可以到达故障服务;

  3. 熔断机制: 根据响应时间, 错误响应比例等依据, 对故障业务进行熔断, 拦截访问该业务的请求; 对这些请求, 也不能置之不理, 比如我可以对这些请求进行降级处理;

2.2.2 降级

服务调用无法正常完成时, 例如被限流, 超时, 或者出现异常, 或者服务被熔断等后, 最好是我不能把请求扔在那不管, 对到来的请求, 可以执行一个替代的简化的处理逻辑, 这就是降级;

2.3 熔断和降级辨析

目的上来说, 熔断和降级, 都是为了提高系统的可用性, 都是为了防止系统崩溃;

熔断的侧重点是防止服务雪崩, 阻止对故障服务的调用, 防止因为调用故障服务导致当前服务资源耗尽,保护上游服务的稳定性。

降级侧重点是在服务出现问题时或者 QPS 过大时取消问题服务或者边缘服务的完整业务, 转而提供基本的服务。比如服务被熔断时, 就可以使用降级; 比如电商平台大促期间, 就可以对边缘业务进行降级处理;

熔断的时候, 可以配合降级机制, 阻止对故障服务调用的同时, 提供保底的基本的服务; 但降级一般不会触发熔断机制;

3 Sentinel的使用

Sentinel 可以分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合 Dashboard 可以取得最好的效果。

使用 Dashboard 只需要下载 jar 包Releases · alibaba/Sentinel (github.com), 并通过命令行启动; 网页访问时, 默认用户名和密码都是 sentinel

java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.3.jar

使用核心库, 需要导包

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

在服务中进行配置

spring:
  cloud:
    sentinel:
      transport:
        # 指定操作面板的地址
        dashboard: localhost:8080

3.1 定义资源

定义资源的方式有多种,其中比较常用是注解方式定义,框架自适配定义。

  • 框架自适配

    为了减少开发的复杂程度,Sentinel 对大部分的主流框架,例如 Web Servlet、Dubbo、Spring Cloud等都做了适配

    例如使用SpringMVC时, 一个请求的应用内路径就会被自动识别为资源;

  • 注解方式定义资源

    Sentinel 支持通过 @SentinelResource 注解定义资源并配置 blockHandlerfallback 函数来进行降级处理。示例:

    // 使用注解声明资源时, 一定要指定blockHandler, 并blockHandler一定要是public的
    // 否则触发限流时, 直接服务器内部错误500; 而自动识别的资源, 会自动输出
    // Blocked by Sentinel (flow limiting)
    @SentinelResource(blockHandler = "blockHandlerForGetUser")
    public User getUserById(String id) {
    	//......
    }
    
    // blockHandler 函数,原方法调用被限流/降级/系统保护的时候调用
    public User blockHandlerForGetUser(String id, BlockException ex) {
        return new User("admin");
    }
    

3.2 定义规则

Sentinel 的所有规则都可以在内存中动态地查询及修改,修改之后立即生效; 规则可以分为流控规则, 熔断规则, 热点规则, 授权规则等;

推荐使用 Dashboard 去定义规则;

4 流控规则

顾名思义, 用于流量控制的规则; 同一个资源可以对应多条限流规则。Sentinel 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。

阈值类型

主要有两种统计类型,一种是统计线程数,另外一种则是统计 QPS; 其中线程数、QPS 值,都是 Sentinel 实时统计获取的。

  • 线程数限流用于保护服务器线程数不被耗尽, 可以防止雪崩。
  • 基于QPS的限流,直接限制同时处理请求的数量。当 QPS 超过某个阈值的时候,则采取措施进行流量控制;

流控模式

直接限流

即最直接的方式,根据被访问资源本身的流量,决定是否要限流;

关联流量限流

当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。

比如不同的 Sentinel Resource 对数据库同一个字段进行写操作,如果放任这两个 Sentinel Resource 争抢数据库资源,则争抢本身带来的开销会降低整体的吞吐量。

可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说

  • read_dbwrite_db 这两个资源分别代表数据库读写;
  • 我们可以给 read_db 设置限流规则来达到写优先的目的:设置限流策略为关联流量限流, 同时设置关联资源为write_db
  • 这样当写库操作过于频繁时,读数据的请求会被限流。

链路限流

在高版本 Sentinel中, 已经不支持链路限流;

sentinel中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root 的虚拟节点,调用链的入口都是这个虚节点的子节点。

                  machine-root
                    /       \
                   /         \
             Entrance1     Entrance2
                /             \
               /               \
      DefaultNode(nodeA)   DefaultNode(nodeA)

上图中来自入口 Entrance1Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 限流策略为 链路限流,同时设置 访问入口为 Entrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而对来自 Entrance2 的调用漠不关心。

流控效果

只有在阈值类型为QPS的时候, 才能设置流控效果;

快速失败

该方式是默认的流量控制方式,当QPS超过阈值后,新的请求就会被立即拒绝,拒绝方式为直接抛出 FlowException, FlowExceptionBlockException 的子类;

慢启动

系统启动时, 不能上来就把流量拉到系统稳定运行时能承受的最大QPS, 多种原因:

  • 这时候各级缓存还没有产生, 所以启动时的平均响应时间要大于稳定后的平均响应时间, 所以启动的时候能承受的 QPS 要更小;

  • 刚启动时, 所有请求到来需要先建立TCP连接, 比较耗时, 稳定运行后只有部分请求 ( 新的客户端发来的请求 ) 需要建立连接;

通过慢启动,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。

如果将流控效果设置为慢启动, 则实际进行流控时, 实际的阈值是逐渐提高的, 一开始是设定值的 1/3, 最后才变为设定值;

流量达到阈值后新的请求还是快速失败的逻辑处理;

排队等待方式

根据设置的QPS计算出两个请求处理的最小间隔时间, 例如 QPS = 10, 则 间隔时间 = 100ms;

当请求到达时,如果当前请求速率未超过设置QPS,请求立即通过。

如果当前请求速率超过阈值,请求将进入等待队列,每经过间隔时间处理队列中的一个请求。

当队列非空时, 新到来的请求肯定超过了流量阈值(因为就是按阈值的速度在处理队列中的请求), 进入队列;

如果请求入队前, 根据队列大小计算预期等待时间, 如果超过设定的最大等待时间,请求将直接被拒绝。

热点规则

热点参数限流会统计资源方法传入的参数,并根据配置进行限流,热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

可以对某个位置上的参数进行控制

对 id 参数进行控制, 不论 id 的值是多少, 只要是携带 id 的请求, 就会参与限流;

@GetMapping("testSen")
@SentinelResource(value = "testSen", blockHandler = "testSenBlockHandler")
String testSen(@RequestParam(value = "id", required = false) Integer id){
    return "test Sen SUCCESS";
}

对固定位置上的参数的某个固定值进行限流, 也叫参数例外项

例如 一秒内, 最多有 2 个 id 值为 2022001 的请求通过;

5 熔断规则

熔断时, 新请求来到直接抛 DegradeException

限流时, 抛FlowException, 二者都继承自BlockException

熔断策略

Sentinel中, 有三种熔断触发策略,分别是慢调用比例 (SLOW_REQUEST_RATIO),异常比例 (ERROR_RATIO),异常数 (ERROR_COUNT)触发;

慢调用比例 (SLOW_REQUEST_RATIO):

  • 选择以慢调用比例作为阈值,需要设置最大 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。需要设置统计时长, 比例阈值, 最小请求数, 熔断时长;

  • 当统计时长内请求数目大于设置的最小请求数,并且慢调用的比例大于最大 RT,则接下来的熔断时长内请求会自动被熔断。

  • 经过熔断时长后熔断器会进入半开状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

异常比例 (ERROR_RATIO):

  • 当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。

  • 经过熔断时长后熔断器会进入半开状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

  • 异常比率的阈值范围是 0 ~ 1, 代表百分比。

异常数 (ERROR_COUNT):

  • 当统计时长内总请求数目大于最小请求数, 并且异常请求的数目超过阈值之后, 会自动进行熔断。

  • 经过熔断时长后熔断器会进入半开状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

熔断效果

熔断时, 如果指定了降级逻辑 fallback, 就会执行降级的逻辑;

如果没有指定降级逻辑, 请求来到时就直接抛出降级异常, 将被 BlockHandler 处理, 如果有自定义的, 就用自定义的, 没有就用公用的 BlockHandler, 显示Blocked by Sentinel (flow limiting)

public class TestService {
    // 对应的 `handleException` 函数需要位于 `ExceptionUtil` 类中,
    // 并且必须为 public static 函数.
    @SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
    public void test() {
        System.out.println("Test");
    }

    // fallback方法位于同类中, 必须为public 
    @SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
    public String hello(long s) {
        return String.format("Hello at %d", s);
    }
    
    // Fallback 函数,必须为public, 函数签名与原函数一致或加一个 Throwable 类型的参数.
    public String helloFallback(long s) {
        return String.format("Halooooo %d", s);
    }

    // Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
    public String exceptionHandler(long s, BlockException ex) {
        // Do some log here.
        ex.printStackTrace();
        return "Oops, error occurred at " + s;
    }
}
value

资源名称,必需项(不能为空)

blockHandler / blockHandlerClass:

blockHandler 指定的函数, 将处理当前资源的 BlockException;

blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException

blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意这时, 对应的函数必需为 static 函数,否则无法解析。

blockHandler 和 fallback 本质上, 都是一种降级逻辑, 早期 fallback 函数只针对降级异常(DegradeException)进行处理, 而这个异常只在发生熔断的时候抛出; 相当于 fallback 专门针对熔断提供降级服务;

后来, fallback 支持所有类型的异常, 这时, 它和 BlockHandler 的区别在于, BlockHanlder 只对限流, 熔断提供降级服务, 而 fallback 还可以对业务异常提供降级服务

fallback / fallbackClass:

fallback 函数名称,可选项,用于提供降级处理逻辑。

在被熔断, 或者被限流, 或者出现业务异常的时候, 都会交由 fallback 提供降级服务;

fallback 函数可以针对所有类型的异常, 包括BlockException进行处理。

fallback 函数签名和位置要求:

  • 返回值类型必须与原函数返回值类型一致;

  • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。

  • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

注:1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException)进行处理;

如果只配置了 fallback, 那么限流, 熔断, 抛出业务异常时, 均进入 fallback 进行处理;

如果 blockHandler 和 fallback同时配置, 发生熔断和限流的时候, 都会由 fallback 处理;

如果两个都没配置, 发生限流和熔断时, 由 Sentinel 提供的默认BlockHandler 处理; 显示Blocked by Sentinel (flow limiting), 熔断的时候也显示 flow limiting;

与OpenFeign整合

Sentinel 适配了 Feign 组件。如果想使用,需要引入 spring-cloud-starter-alibaba-sentinelspring-cloud-starter-openfeign的依赖;

并且在使用 FeignClient 的服务的配置文件中设置 feign.sentinel.enabled = true

与Feign整合后, 可以指定降级类或者降级类工厂, BlockException 和 其它业务异常, 都会在降级类中处理;

方式一:

编写 OpenFeignClient 接口, 提供一个实现类, 实现类中重写方法, 该方法实现降级后的策略, 向容器注册该实现类;

在 @FeignClient 注解中, 指定 降级实现类;

@FeignClient(name = "order-service", fallback = OrderServiceFallback.class)
public interface OrderFeignClient {
    @GetMapping("order/getOrder")
    String getOrder(@RequestParam("order_id") Integer id);
}

@Configuration
class FeignConfiguration {
    @Bean
    public OrderServiceFallback orderServiceFallback() {
        return new OrderServiceFallback();
    }
}

class OrderServiceFallback implements OrderFeignClient {
    @Override
    public String getOrder(@RequestParam("order_id") Integer id) {
        return "order-service fallback";
    }
}

需要注意的是:

FeignClient 对应的接口的资源名为:method:protocol://requesturl,例如GET:http://account-service/account/test

方式二

@FeignClient(value = "stock-service", fallbackFactory = StockFallbackFactory.class)
public interface StockClient {
    @GetMapping("/stock/test")
    public Stock test(){
        //......
    }
}

public class StockFallbackFactory implements FallbackFactory<StockClient> {
    @Override
    public StockClient create(Throwable throwable) {
        return new StockClient() {
            @Override
            public Stock test() {
                // 可以获取异常信息, 可以处理异常; 降级时抛出的异常类型是DegradeException
                throwable.printStackTrace();
                return new Stock();
            }
        }
    };
}

@Configuration
public FallbackConfig{
    @Bean
	public StockFallbackFactory stockClientFallbackFactory(){
    	return new StockFallbackFactory();
	}
}

6 Gateway规则

本质是做成了 Gateway 应用的热点规则;

配置Gateway

Sentinel 从 1.6.0 版本开始,提供了 Spring Cloud Gateway 的适配模块,可提供两种资源维度的限流:

  • route 维度:在 Spring 配置路由条目时,资源名为 routeId
  • 自定义 API 维度,用户可以用 Sentinel 提供的 API 来定义一些 API 分组

网关应用引入依赖

<!--加入nacos的依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

<!--添加Sentinel的依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>
<!-- sentinel整合gateway-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<!-- sentinel-nacos持久化 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

编写bootstrap.yaml

server:
  port: 7770

spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        file-extension: yaml

其余配置

spring:
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

    gateway:
      routes:
        - id: duolai-order
          uri: lb://duolai-order
          predicates:
            - Path=/shopping/order/**,/shopping/cancelOrder
        - id: duolai-user
          uri: lb://duolai-user
          predicates:
            - Path=/user/**, /shopping/addresses
    # sentinel配置
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
        port: 9719
        clientIp: 127.0.0.1
      datasource:
        # 从nacos获取路由条目对应的流控规则
        gateway-flow-rules:
          nacos:
            server-addr: 127.0.0.1:8848
            dataId: ${spring.application.name}-flow-rules
            groupId: SENTINEL_GATEWAY_GROUP
            data-type: json
            rule-type: gw-flow
        #  从nacos获取api分组流控规则
        gateway-api-rules:
          nacos:
            server-addr: 127.0.0.1:8848
            dataId: ${spring.application.name}-api-groups
            groupId: SENTINEL_GATEWAY_GROUP
            data-type: json
            rule-type: gw-api-group

然后就是在nacos对应dataId的配置文件中,添加流控规则即可,具体流控规则,可以在dashboard中添加并获取

获取网关流控规则

GET http://ip:端口/gateway/getRules

获取api分组

GET http://ip:端口/gateway/getApiDefinitions

Sentinel配置

路由ID模式

API分组模式, 对匹配到同一个API分组的请求, 进行流量控制;

7 自定义BlockExceptionHandler

自定义BlockExceptinHandler, 只对SpringMVC默认的路径资源有效; 对 @SentinelResource 注解定义的资源无效;

抛出的 ExceptionHandler 类型异常将由自定义的BlockExceptionHandler处理;

@Component
public class MyBlockHandler implements BlockExceptionHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().println(e.toString());
    }
}

8 规则持久化

Sentinel还做了和Nacos的适配,我们只需要把我们的规则配置存储在Nacos中,就可以实现Sentinel规则的持久化保存;

之后 Sentinel 会自动从 Nacos 读取规则配置,并且当 Nacos 中配置内容发生变更的时候,Sentinel 也会实时感知到规则的变化,从而让规则生效。

但是 Sentinel 中修改添加新的规则, 不会自动推送到 Nacos, 如果想实现, 需要修改 Sentinel dashboard 的源码;

为了实现Sentinel和Nacos的整合,我们首先需要在项目中导入依赖

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

并且在配置文件中添加如下依赖

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
      datasource:
        # ds1 是第一个数据源的名字,我们可以定义多个数据源
        ds1:
          nacos:
            # nacos地址
            server-addr: localhost:8848
            # 配置的 dataId
            dataId: sentinel-rules
            # 配置的 group
            groupId: SENTINEL_GROUP
            # 配置的 namespace
            namespace: 41a9d584-4dbd-480b-aadd-d53c1700ecb4
            # 配置的数据类型
            data-type: json
            # 规则类型
            rule-type: flow  #flow、degrade、param-flow、gw-flow        

然后,我们在nacos中去配置好,对应的规则即可。

那么如何通过json字符串去定义规则呢?我们可以在 Dashboard 中配置规则, 然后通过给定的API直接获取规则的 JSON 格式;

GET http://ip:port/getRules?type=xxx
  • ip是你进行限流的那个服务所在的ip地址
  • port是你的服务与dashboard通信的端口号
  • type指查询的规则类型,这里可以取flow,degrade等,分别代表流控规则和降级规则。

其实请求地址 Sentinel 已经自动生成

在这里插入图片描述

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值