Loading

54-Sentinel-QuickStart

1. 概述

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

(1)Sentinel 的主要特性

(2)Sentinel 的开源生态

(3)Sentinel 两大组成部分

  • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
  • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

(4)Sentinel 两个核心概念

名称 描述
资源 它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。我们请求的 API 接口就是资源。
规则 围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。

(5)依赖

<!-- alibaba Sentinel  -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>${spring-cloud-alibaba.version}</version>
</dependency>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.0</version>
</dependency>
<!-- alibaba Sentinel 整合 Nacos 实现持久化 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <version>1.8.0</version>
</dependency>
<!-- 当 Sentinel 整合的微服务类型是 gateway -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
    <version>1.8.0</version>
</dependency>

2. 流控规则

2.1 简单说明

  • 资源名:唯一名称,默认请求路径;
  • 针对来源:Sentinel 可以针对调用者进行限流,填写微服务名,默认 default(不区分来源);
  • 阈值类型/单机阈值:
    • QPS(每秒钟的请求数量)︰当调用该 API 的 QPS 达到阈值的时候,进行限流;
    • 线程数:当调用该 API 的线程数达到阈值的时候,进行限流。
  • 是否集群:不需要集群。
  • 流控模式:
    • 直接:API 达到限流条件时,直接限流;
    • 关联:当关联的资源达到阈值时,就限流自己;
    • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)// API 级别的针对来源。
  • 流控效果:
    • 快速失败:直接失败,抛异常;
    • Warm up:根据 Code Factor(冷加载因子,默认 3)的值,从阈值/codeFactor,经过预热时长,才达到设置的 QPS 阈值;
    • 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为 QPS,否则无效。

2.2 流控模式

a. 直接

直接调用默认报错信息,技术方面 OK,但是否应该有我们自己的后续处理?例如类似有一个 fallback 的兜底方法?

b. 关联

当关联的资源达到阈值时,就限流自己(e.g. 支付接口达到阈值后限流下订单的接口)。

【效果】当关联资源 /testB 的 QPS 阈值超过 1 时,就限流 /testA 的访问地址。

在 PostMan 跑的期间去访问 /testA,不出意外 Blocked by Sentinel (flow limiting):

c. 链路

// TODO

2.3 流控效果

a. 快速失败

界面显示:Blocked by Sentinel (flow limiting)

源码参见:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController

b. Warm Up

默认 coldFactor 为 3,即请求 QPS 从 threshold/coldFactor 开始,经预热时长逐渐升至设定的 QPS 阈值。

com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController

秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统搞崩,WarmUp 就是为了保护系统,慢慢的把流量放进来,慢慢的把阈值增长到最初设定的阈值。

c. 排队等待

匀速排队,让请求以均匀的速度通过,阈值类型必须是 QPS,否则无效。

3. 降级规则

3.1 说明

除了流量控制之外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或者异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

3.2 策略

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

【注】异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。为了统计异常比例或异常数,需要通过 Tracer.trace(ex) 记录业务异常。@SentinelResource 注解会自动统计业务异常,无需手动调用。

4. 热点规则

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制;
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制。

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

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。

在 Controller#xxxHandle 方法上加了 @SentinelResource("testHotKey") 注解的资源做限制:当 QPS 超过 1次/s,立刻被限流。如若处理方法的第 1 个参数的值为 1101 则做限流 200次/s 的控制。

也即 param1 不是 "1101" 时,阈值是 1;但若值为 "1101",则阈值 200。

5. 系统规则

6. @SentinelResource

该注解处理的是 Sentinel 控制台配置的违规情况

  • value
    • 资源名称;
    • 必需项,因为需要通过 resource name 找到对应的规则,这个是必须配置的。
  • entryType
    • entry 类型;
    • 可选项,有 IN 和 OUT 两个选项,默认为 EntryType.OUT。
  • blockHandler
    • blockHandler 对应处理 BlockException 的函数名称,可选项;
    • blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。
  • blockHandlerClass
    • blockHandler 函数默认需要和原方法在同一个类中;
    • 如果希望使用其他类的函数,则需要指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • fallback
    • fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑;
    • fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。
  • fallbackClass
    • fallbackClass 的应用和 blockHandlerClass 类似,fallback 函数默认需要和原方法在同一个类中;
    • 若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • defaultFallback(since 1.6.0)
    • 如果没有配置 defaultFallback 方法,默认都会走到这里来;
    • 默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理;
    • 若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。
  • exceptionsToIgnore(since 1.6.0)
    • 用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

7. 服务熔断

7.1 &Ribbon

@Bean
@LoadBalance
public RestTemplate restTemplate() {
    return new RestTemplate();
}

7.2 &OpenFeign

# 对Feign的支持
feign:
  sentinel:
    enabled: true

8. 规则持久化

目前,Sentinel Dashboard 中添加的规则数据存储在内存,微服务停掉规则数据就消失,在生产环境下不合适。我们可以将 Sentinel 规则数据持久化到 Nacos 配置中心,让微服务从 Nacos 获取规则数据。

将限流配置规则持久化进 Nacos 保存,只要刷新 8401 某个 rest 地址,Sentinel 控制台的流控规则就能看到,只要 Nacos 里面的配置不删除,针对 8401 上 Sentinel 上的流控规则持续有效。

【避坑】升级 JDK 版本为 1.8.0_152 以上。

8.1 微服务配置

spring:
  application:
    name: alibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
      datasource:
        # 自定义的流控规则数据源名称
        flow:
          nacos:
            server-addr: ${spring.cloud.nacos.discovery.server-addr}
            data-id: ${spring.application.name}-flow-rules
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow     # 类型来自RuleType类
        degrade:
          nacos:
            server-addr: ${spring.cloud.nacos.discovery.server-addr}
            data-id: ${spring.application.name}-degrade-rules
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: degrade

8.2 Nacos 配置

Nacos Server 中添加对应规则配置集。

(1)流控规则配置集 alibaba-sentinel-service-flow-rules;

[
    {
        "resource":"findResumeOpenState",
        "limitApp":"default",
        "grade":1,
        "count":1,
        "strategy":0,
        "controlBehavior":0,
        "clusterMode":false
    }
]

所有属性来自源码 FlowRule 类:

  • resource:资源名称
  • limitApp:来源应用
  • grade:阈值类型 0 线程数 1 QPS
  • count:单机阈值
  • strategy:流控模式,0 直接 1 关联 2 链路
  • controlBehavior:流控效果,0 快速失败 1 Warm Up 2 排队等待
  • clusterMode:是否集群,true/false

(2)降级规则配置集 alibaba-sentinel-service-degrade-rules;

[
    {
        "resource":"findResumeOpenState",
        "grade":2,
        "count":1,
        "timeWindow":5
    }
]

所有属性来自源码 DegradeRule 类:

  • resource:资源名称
  • grade:降级策略 0 RT 1 异常比例 2 异常数
  • count:阈值
  • timeWindow:时间窗

(3)补充

  • Rule 源码体系结构
  • 一个资源可以同时有多个限流规则和降级规则,所以配置集中是一个 json 数组;
  • Sentinel 控制台中修改规则,仅是内存中生效,不会修改 Nacos 中的配置值,重启后恢复原来的值; Nacos 控制台中修改规则,不仅内存中生效,Nacos 中持久化规则也生效,重启后规则依然保持。

9. 实现基础限流功能

9.1 Redis 实现

Redis + Lua脚本 + AOP + 反射 + 自定义注解

(1)需要设计一个注解,声明在需要限流的接口上。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RedisLimitAnnotation {
    
    /**
     * 资源的key(唯一)
     * 作用:不同的接口,不同的流量控制
     */
    String key() default "";

    /**
     * 最多的访问限制次数
     */
    long limit() default 3;

    /**
     * 过期时间(计算窗口时间),单位秒默认30
     */
    long expire() default 30;

    /**
     * 默认提示语
     */
    String msg() default "系统繁忙/操作频繁,请稍后再试,谢谢。";
}

使用方式:

/**
 * 表示本次10秒内最多支持5次访问,到了5次后开启限流,过完本次10s后才解封放开,可以重新访问。
 */
@GetMapping("/redis/limit/test")
@RedisLimitAnnotation(key = "redisLimit", limit = 5, expire = 10, msg = "别特么点了,系统都让你点爆了!")
public String redisLimit() {
    return "正常业务返回,业务ID:" + IdUtil.fastUUID();
}

(2)新增切面类来处理增加 @RedisLimitAnnotation 注解的接口调用

@Slf4j
@Aspect
@Component
public class RedisLimitAop {

    Object result = null;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private DefaultRedisScript<Long> redisLuaScript;

    @PostConstruct
    public void init() {
        redisLuaScript = new DefaultRedisScript<>();
        redisLuaScript.setResultType(Long.class);
        redisLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
    }

    @Around("@annotation(com.atguigu.interview2.annotations.RedisLimitAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint) {
        log.info("--------- 环绕通知 ---------");

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 拿到RedisLimitAnnotation注解,如果存在则说明需要限流
        RedisLimitAnnotation redisLimitAnnotation = method.getAnnotation(RedisLimitAnnotation.class);

        if (redisLimitAnnotation != null) {
            // 获取redis的key
            String key = redisLimitAnnotation.key();
            String className = method.getDeclaringClass().getName();
            String methodName = method.getName();

            String limitKey = key + "\t" + className + "\t" + methodName;
            log.info(limitKey);

            if (null == key) {
                throw new RedisLimitException("It's danger, limitKey cannot be null.");
            }

            long limit = redisLimitAnnotation.limit();
            long expire = redisLimitAnnotation.expire();
            List<String> keys = new ArrayList<>();
            keys.add(key);

            // 执行 Lua ~
            Long count = stringRedisTemplate.execute(redisLuaScript, keys, String.valueOf(limit), String.valueOf(expire));

            log.info("Access try count is {}, key= {}", count, key);
            if (count != null && count == 0) {
                log.info("启动限流功能key: {}", key);
                // throw new RedisLimitException(redisLimitAnnotation.msg());
                return redisLimitAnnotation.msg();
            }
        }


        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }

        log.info("--------- 环绕通知 ---------");

        return result;
    }
}

关联的 rateLimiter.lua(文件放置在 resources 目录下):

-- Lua 脚本中的数组索引默认是从 1 开始的不是从 0 开始
local redisKey = KEYS[1]             -- 接口ID作为 Redis 键
local limit = tonumber(ARGV[1])      -- 限制的接口调用次数
local expire = tonumber(ARGV[2])     -- Redis 键的过期时间(秒)

local currentCount = redis.call('GET', redisKey)  -- 获取当前计数

if currentCount then
    currentCount = tonumber(currentCount)
    if currentCount >= limit then
        return 0                                  -- 超过限制,拒绝访问
    else
        redis.call('INCR', redisKey)              -- 计数加一
        return 1                                  -- 访问成功
    end
else
    redis.call('SET', redisKey, 1, 'EX', expire)  -- 第一次访问,设置计数器并设置过期时间
    return 1                                      -- 访问成功
end

9.2 令牌桶实现

使用令牌桶算法(Token Bucket Algorithm)可以实现简单的限流机制。令牌桶算法允许系统按照一个固定的速率向桶中添加令牌,然后在请求到来时,如果桶中有足够的令牌,则允许请求通过,并且从桶中消耗一个令牌;如果桶中没有足够的令牌,则拒绝请求。

使用了自定义的 TokenBucket 类结合 Spring Boot 中的 AOP 和自定义注解,实现了简单的请求限流控制。当请求到达被标记的方法时,切面会先判断 TokenBucket 中是否还有足够的令牌,如果有则允许请求通过,否则抛出限流异常或者直接返回错误信息。这样可以有效控制方法的访问频率,保护系统不受过载影响。

(1)令牌桶设计

令牌桶实现:

public class TokenBucket {

    // 桶的容量
    private final long capacity; 

    // 令牌恢复速率,单位:令牌/毫秒
    private final long refillRate; 

    // 当前桶中的令牌数量
    private AtomicLong tokens; 

    // 上次令牌恢复的时间戳
    private long lastRefillTimestamp;


    public TokenBucket(long capacity, long refillRate) {
        this.capacity = capacity;
        this.refillRate = refillRate;
        this.tokens = new AtomicLong(capacity);
        this.lastRefillTimestamp = System.currentTimeMillis();
    }

    public synchronized boolean tryConsume() {
        refillTokens();
        long currentTokens = tokens.get();
        if (currentTokens > 0) {
            tokens.decrementAndGet();
            return true;     // 令牌数足够,允许通过
        } else {
            return false;    // 令牌数不足,拒绝通过
        }
    }

    private void refillTokens() {
        long now = System.currentTimeMillis();
        long elapsedTime = now - lastRefillTimestamp;
        if (elapsedTime > 0) {
            long tokensToAdd = elapsedTime * refillRate;
            tokens.set(Math.min(capacity, tokens.get() + tokensToAdd));
            lastRefillTimestamp = now;
        }
    }
}

(2)自定义限流注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {

    // 限流的键,用于区分不同的限流策略
    String key(); 

    // 限流的阈值
    int limit(); 

}

(3)创建切面类实现限流逻辑 —— 利用 AOP 根据 RateLimit 注解中的参数来动态配置不同的限流策略

@Aspect
@Component
public class RateLimitAspect {

    private Map<String, TokenBucket> tokenBuckets = new ConcurrentHashMap<>();

    @Before("@annotation(rateLimit)")
    public void rateLimitCheck(RateLimit rateLimit) throws RateLimitException {
        String key = rateLimit.key();
        int limit = rateLimit.limit();

        TokenBucket tokenBucket = tokenBuckets.computeIfAbsent(key, k -> new TokenBucket(limit, 1)); // 每毫秒添加1个令牌
        if (!tokenBucket.tryConsume()) {
            throw new RateLimitException("请求被限流,请稍后再试。");
        }
    }
}

(4)在控制器中使用 RateLimit 注解并传入不同的参数来配置不同的限流策略

@RestController
public class MyController {

    @GetMapping("/limitedEndpoint1")
    @RateLimit(key = "endpoint1", limit = 10)
    public String limitedEndpoint1() {
        return "限流测试接口1";
    }

    @GetMapping("/limitedEndpoint2")
    @RateLimit(key = "endpoint2", limit = 20)
    public String limitedEndpoint2() {
        return "限流测试接口2";
    }
}

通过以上方法,我们可以为不同的接口配置不同的限流策略。切面类根据 RateLimit 注解中的 key 参数来区分不同的限流策略,并且可以动态创建和管理对应的 TokenBucket 实例。这样每个接口都可以根据自身的需求配置独立的限流阈值,从而灵活地控制接口的访问频率。

posted @ 2022-04-10 16:54  tree6x7  阅读(36)  评论(0编辑  收藏  举报