Sentinel从入门到应用

image-20220905095626011

面向分布式、多语言异构化服务架构的流量治理组件

官方文档:https://sentinelguard.io/zh-cn/

一、基本介绍

1.1 Sentinel简介

官方介绍:随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。

Sentinel 的开源生态:

image-20220905101129721

Sentinel具有以下特征:

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

Sentinel分为两个部分:

  • **核心库(Java 客户端)**不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。

  • **控制台(Dashboard)**基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

    控制台支持实时监控

Sentinel 中的基本概念:

  • 资源

    资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。

    只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。

  • 规则

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

1.2 限流与熔断降级

服务限流 :当系统资源不够,不足以应对大量请求,对系统按照预设的规则进行流量限制或功能限制

服务熔断:当调用目标服务的请求和调用大量超时或失败,服务调用方为避免造成长时间的阻塞造成影响其他服务,后续对该服务接口的调用不再经过进行请求,直接执行本地的默认方法

服务降级:为了保证核心业务在大量请求下能正常运行,根据实际业务情况及流量,对部分服务降低优先级,有策略的不处理或用简单的方式处理

1.3 Sentinel 功能与设计

Sentinel定位是分布式系统的流量防卫兵。目前互联网应用基本上都使用微服务,微服务的稳定性是一个很重要的问题,而限流、熔断降级是微服务保持稳定的一个重要的手段。

  • 流量控制

    流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。**我们需要根据系统的处理能力对流量进行控制。**Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:

    image-20220905132838176

    流量控制有以下几个角度:

    • 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
    • 运行指标,例如 QPS、线程池、系统负载等;
    • 控制的效果,例如直接限流、冷启动、排队等。

    Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。

  • 熔断降级

    除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。

    Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。

    熔断降级设计理念

    在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。

    Hystrix 通过线程池的方式,来对依赖(在我们的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。

    Sentinel 对这个问题采取了两种手段:

    • 通过并发线程数进行限制

    和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。

    • 通过响应时间对资源进行降级

    除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。

  • 系统负载保护

    Sentinel 同时提供系统维度的自适应保护能力。防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。

    针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。

二、基本使用

2. 1 Hello World

引入依赖:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.5</version>
</dependency>
 public static void main(String[] args) {
        // 配置规则.
        initFlowRules();
        int x = 10000;
        while (x > 0) {
            // 1.5.0 版本开始可以直接利用 try-with-resources 特性
            try (Entry entry = SphU.entry("HelloWorld")) {
                // 被保护的逻辑
                System.out.println("hello world");
            } catch (BlockException ex) {
                // 处理被流控的逻辑
                System.out.println("限流限流限流");
            }
            x--;
        }
    }
    private static void initFlowRules(){
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        rule.setResource("HelloWorld");
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // Set limit QPS to 20.
        rule.setCount(20);
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }

输出结果:

image-20220907133143159

2.2 控制台

image-20220914154955715

  1. 从官网下载控制台jar包

  2. 运行jar包

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

    参数说明:

    # 指定控制台的端口为8480
    -Dserver.port=8480 
    # 指定要被哪个控制台监控(这里指定的是自己监控自己)
    -Dcsp.sentinel.dashboard.server=localhost:8480 
    # 指定实例名称(名称会在控制台左侧以菜单显示)
    -Dproject.name=sentinel-dashboard 
    # 设置登录的帐号为:sentinel 
    -Dsentinel.dashboard.auth.username=sentinel 
    # 设置登录的密码为:123456
    -Dsentinel.dashboard.auth.password=123456 
    
    

    如果使用docker部署:

    # 指定docker容器使用宿主机的网络(你也可以映射8858,8719这两个端口)
    --network=host    
    # 开启登录认证,在application.yml中你需要配置对应的用户名和密码
    auth.enabled="true"
    # 指定Sentinel控制台用户名,默认Sentinel
    sentinel.dashboard.auth.username=admin
    # 指定Sentinel控制台密码,默认Sentinel
    sentinel.dashboard.auth.password=admin
    # 用于指定 Spring Boot服务端session的过期时间,如7200表示7200 秒;60m表示60分钟,默认为30分钟;
    server.servlet.session.timeout=7200
    
    
  3. 访问http://localhost:8080,默认登录的用户名和密码都是sentinel

登录后页面:

image-20220914160011883

2.2 快速入门

Sentinel适配了常见主流框架,包括Dubbo、Spring Boot、Spring WebFlux、gRPC、Zuul、Spring Cloud Gateway、RocketMQ、Web Servlet,对于需要限流的资源,支持用原生Java的try-catch 接入或者使用注解。

下面是一个结合控制台的限流实例:

1️⃣ 引入依赖:

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

2️⃣ 配置文件中指定控制台地址:

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719

默认与控制台通信的端口为 8719

3️⃣ 创建一个测试用的Controller

    @GetMapping("getList")
    public List<User> getList(){
        return userService.getUserList();
    }

启动项目,可以在dashboard中机器列表中看到我们的服务,以及健康状态。

4️⃣ 用postman 连续发送请求观察实时监控

image-20220914163117374

5️⃣ 配置限流策略

image-20220914163430750

6️⃣ 使用postman再次进行测试

image-20220914163727124

如果不是接口资源可以通过@SentinelResource来指定资源。

三、@SentinelResource详解

3.1 定义资源

Sentinel需要先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。

Sentinel提供多种定义资源的方式,分别是:

  • 主流框架的默认适配
  • 抛出异常的方式定义资源
  • 返回布尔值方式定义资源
  • 注解方式定义资源
  • 异步调用支持

想要详细了解每种方式可以自行查阅官网

3.2 @SentinelResource 注解

Sentinel提供了@SentinelResource注解用于定义资源,并提供可选的异常回退和Block回退。

异常回退指的是:@SentinelResource注解标注的方法发生Java异常时的回退处理;

Block回退指的是:当@SentinelResource资源访问不符合Sentinel控制台定义的规则时的回退(默认返回Blocked by Sentinel (flow limiting))。

属性说明必填与否使用要求
value用于指定资源的名称必填-
entryTypeentry 类型可选项(默认为 EntryType.OUT)-
blockHandler服务限流后会抛出 BlockException 异常,而 blockHandler 则是用来指定一个函数来处理 BlockException 异常的。 简单点说,该属性用于指定服务限流后的后续处理逻辑。可选项blockHandler 函数访问范围需要是 public返回类型需要与原方法相匹配;参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException;blockHandler 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 blockHandler 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
blockHandlerClass若 blockHandler 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。可选项不能单独使用,必须与 blockHandler 属性配合使用;该属性指定的类中的 blockHandler 函数必须为 static 函数,否则无法解析。
fallback用于在抛出异常(包括 BlockException)时,提供 fallback 处理逻辑。 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。可选项返回值类型必须与原函数返回值类型一致方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常fallback 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
fallbackClass若 fallback 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。可选项不能单独使用,必须与 fallback 或 defaultFallback 属性配合使用该属性指定的类中的 fallback 函数必须为 static 函数,否则无法解析。
defaultFallback默认的 fallback 函数名称,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。 默认 fallback 函数可以针对所以类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。可选项返回值类型必须与原函数返回值类型一致方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
exceptionsToIgnore用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。可选项-

注意:

  • 在 Sentinel 1.6.0 之前,fallback 函数只针对降级异常(DegradeException)进行处理,不能处理业务异常。
  • 注解方式埋点不支持 private 方法。
  • 特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandlerfallbackdefaultFallback,则被限流降级时会将 BlockException 直接抛出

3.3 代码示例

@RestController
public class BuyController {

    @GetMapping("buy/{name}/{count}")
    @SentinelResource(value = "buy", fallback = "buyFallback", blockHandler = "buyBlock")
    public String buy(@PathVariable String name, @PathVariable Integer count) {
        if (count >= 20) {
            throw new IllegalArgumentException("购买数量过多");
        }
        if ("car".equalsIgnoreCase(name)) {
            throw new NullPointerException("已售罄");
        }
        return "购买了"+count+"个"+name;
    }

    // 异常回退
    public String buyFallback(@PathVariable String name, @PathVariable Integer count, Throwable throwable) {
        return String.format("【进入fallback方法】购买%d份%s失败,%s", count, name, throwable.getMessage());
    }

    // sentinel回退
    public String buyBlock(@PathVariable String name, @PathVariable Integer count, BlockException e) {
        return String.format("【进入blockHandler方法】购买%d份%s失败,当前购买人数过多,请稍后再试", count, name);
    }
}

在控制台配置流控规则后再进行测试发现:走了对应的回退方法。

在当前类中编写回退方法会使得代码变得冗余耦合度高,我们可以将回退方法抽取出来到指定类中。

public class BuyFallback {
    // 异常回退
    public static String buyFallback(@PathVariable String name, @PathVariable Integer count, Throwable throwable) {
        return String.format("【进入fallback方法】购买%d份%s失败,%s", count, name, throwable.getMessage());
    }
}

public class BuyBlockHandler {
    
    // sentinel回退
    public static String buyBlock(@PathVariable String name, @PathVariable Integer count, BlockException e) {
        return String.format("【进入blockHandler方法】购买%d份%s失败,当前购买人数过多,请稍后再试", count, name);
    }
}

然后注解中的值可以简化为:

@SentinelResource(value = "buy",
            fallback = "buyFallback",
            fallbackClass = BuyFallBack.class,
            blockHandler = "buyBlock",
            blockHandlerClass = BuyBlockHandler.class
    )

fallbackClassblockHandlerClass指定回退方法所在的类。

此外我们也可以当遇到某个类型的异常时,不进行回退。比如

    @SentinelResource(value = "buy",
            fallback = "buyFallback",
            fallbackClass = BuyFallBack.class,
            blockHandler = "buyBlock",
            blockHandlerClass = BuyBlockHandler.class,
            exceptionsToIgnore = NullPointerException.class
    )

exceptionsToIgnore指定,当遇到空指针异常时,不回退。

四、Sentinel的流控熔断降级规则

4.1 服务降级

  1. 什么是服务降级

    服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略的不处理或者用一种简单的方式进行处理,从而**释放服务器资源的资源以保证核心业务的正常高效运行。**说白了,就是尽可能的把系统资源让给优先级高的服务。

  2. 为什么需要服务降级

    服务器的资源是有限的,而请求是无限的。在用户使用即并发高峰期,会影响整体服务的性能,严重的话会导致宕机,以至于某些重要服务不可用。故高峰期为了保证核心功能服务的可用性,就需要对某些服务降级处理。可以理解为舍小保大

  3. 应用场景

    多用于微服务架构中,一般当整个微服务架构整体的负载超出了预设的上限阈值(和服务器的配置性能有关系),或者即将到来的流量预计会超过预设的阈值时(比如双11、6.18等活动或者秒杀活动)

  4. 服务降级策略

    • 拒绝服务
    • 关闭服务

4.2 服务熔断

1️⃣什么是服务熔断?

应对微服务雪崩效应的一种链路保护机制,类似股市、保险丝,可看作降级的特殊情况

2️⃣为什么需要服务熔断?

​ 微服务之间的数据交互是通过远程调用来完成的。服务A调用服务,服务B调用服务c,某一时间链路上对服务C的调用响应时间过长或者服务C不可用,随着时间的增长,对服务C的调用也越来越多,导致请求的堆积,然后服务C崩溃了,但是链路调用还在,对服务B的调用也在持续增多,然后服务B崩溃,随之A也崩溃,导致雪崩效应

服务熔断是应对雪崩效应的一种微服务链路保护机制。例如在高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。同样,在微服务架构中,熔断机制也是起着类似的作用。当调用链路的某个微服务不可用或者响应时间太长时,会进行服务熔断,不再有该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路

服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用

在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。

4.3 服务降级与熔断的区别

  1. 触发原因不一样,服务熔断由链路上某个服务引起的,服务降级是从整体的负载考虑
  2. 管理目标层次不一样,服务熔断是一个框架层次的处理,服务降级是业务层次的处理
  3. 实现方式不一样,服务熔断一般是自我熔断恢复,服务降级相当于人工控制
  4. 触发原因不同,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑

4.4 流量控制

Sentinel除了提供服务的熔断与降级以外,还提供了流量控制来保证服务的高可用流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

流量控制有以下几个角度:

  • 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
  • 运行指标,例如 QPS、线程池、系统负载等;
  • 控制的效果,例如直接限流、冷启动、排队等。

Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果

4.5 流控规则

FlowSlot 会根据预设的规则,结合前面 NodeSelectorSlotClusterBuilderSlotStatisticSlot 统计出来的实时信息进行流量控制。

限流的直接表现是在执行 Entry nodeA = SphU.entry(resourceName) 的时候抛出 FlowException 异常。FlowExceptionBlockException 的子类,您可以捕捉 BlockException 来自定义被限流之后的处理逻辑。

同一个资源可以创建多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

  • resource:资源名,即限流规则的作用对象
  • count: 限流阈值
  • grade: 限流阈值类型(QPS 或并发线程数)
  • limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
  • strategy: 调用关系限流策略
  • controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)

结合控制台来看:

image-20220908223503999

参数解释:

  • 资源名:Sentinel定义的资源名称,通常在代码中通过@SentinelResource进行埋点,名称唯一

  • 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,指定对哪个微服务进行限流 ,默认default(不区分来源,全部限制)

    • default :表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流(默认)
    • {some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者caller1的规则,那么当且仅当来自 caller1NodeA 的请求才会触发流量控制。
    • other:表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1NodeA 的调用,都不能超过 other 这条规则定义的阈值。
  • 阈值类型:流量控制主要有两种统计类型,一种是统计并发线程数,另外一种则是统计 QPS

    • QPS:当 QPS 超过某个阈值的时候,则采取措施进行流量控制

    • 并发线程数控制:当调用该接口的线程数超过阈值时,进行限流

      **并发数控制用于保护业务线程池不被慢调用耗尽。**例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置。

  • 单机阈值

    • 基于QPS:QPS的阈值,超过该阈值会进行流控
    • 基于线程数:线程的阈值,超过该阈值拒绝请求
  • 流控效果:当 QPS 超过某个阈值的时候,则采取措施进行流量控制,流量控制的效果包括以下几种:直接拒绝(快速失败)、Warm Up匀速排队(排队等待)

    • 快速失败:该方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException

    • Warm Up:即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮

    • 排队等待:该方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法

      该方式的作用如下图所示:

      image

      这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求

      注意:匀速排队模式暂时不支持 QPS > 1000 的场景。

  • 流控模式

    • 直接:接口达到限流条件时,直接限流

    • 关联:当关联的资源达到阈值时,就限流自己

      屏幕截图 2022-09-08 233907

      当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_dbwrite_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategyRuleConstant.STRATEGY_RELATE 同时设置 refResourcewrite_db。这样当写库操作过于频繁时,读数据的请求会被限流

    • 只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就可以限流)[api级别的针对来源]

      屏幕截图 2022-09-08 233320

      Sentinel链路流控失效请参考Issues Sentinel 链路流控模式失效 #1213

4.6 熔断降级规则

慢调用比例 (SLOW_REQUEST_RATIO):

选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

屏幕截图 2022-09-08 235141

  1. 异常比例 (ERROR_RATIO)

    当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

  2. 异常数 (ERROR_COUNT)

    当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。

@SentinelResource 注解会自动统计业务异常,无需手动调用。其他统计异常方式请参考官网:熔断降级

五、规则持久化与动态规则

Sentinel 的理念是开发者只需要关注资源的定义,当资源定义成功后可以动态增加各种流控降级规则。Sentinel 提供两种方式修改规则:

  • 通过 API 直接修改 (loadRules)、或者在代码中写死
  • 通过 DataSource 适配不同数据源修改

由于手动通过API定义规则是一种硬编码的方式,不够灵活,肯定不能应用于生产环境。

所以要引入DataSource,规则设置可以存储在数据源中,通过更新数据源中存储的规则,推送到Sentinel规则中心,客户端就可以实时获取最新的规则,根据最新的规则进行限流、降级。

推送模式说明优点缺点
原始模式API 将规则推送至客户端并直接更新到内存中,扩展写数据源简单,无任何依赖不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境
Pull 模式扩展写数据源, 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等简单,无任何依赖;规则持久化不保证一致性;实时性不保证,拉取过于频繁也可能会有性能问题。
Push 模式扩展读数据源,规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。生产环境下一般采用 push 模式的数据源。规则持久化;一致性;快速引入第三方依赖

3.2 拉模式

实现拉模式的数据源最简单的方式是继承 AutoRefreshDataSource 抽象类,然后实现 readSource() 方法,在该方法里从指定数据源读取字符串格式的配置数据。比如 基于文件的数据源

由此看出这是一个双向读写的过程,我们既可以在应用本地直接修改文件来更新规则,也可以通过 Sentinel 控制台推送规则。下图为控制台推送规则的流程图。

img

3.3 推模式

拉模式实时性不能保证,推模式就解决了这个问题。除此之外还可以持久化,也就是数据保存在数据源中,即使重启也不会丢失之前的配置,这也解决了原始模式存在内存中不能持久化的问题。

Remote push rules to config center

可以和Sentinel配合使用的数据源有很多种,比如ZooKeeper,Nacos,Apollo等等。这里演示使用Nacos的方式。

1️⃣ 准备Nacos环境

引入依赖:

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

在配置文件中配置数据源:

spring:
  application:
    name: wz-service
  cloud:
    sentinel:
      transport:
        #配置 Sentinel dashboard 地址
        dashboard: 192.168.199.128:8858
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719
      datasource:
        ds1:
          nacos:
            server-addr: 192.168.199.128:8848 #Nacos地址
            dataId: ${spring.application.name}-flow-sentinel #dataID
            groupId: DEFAULT_GROUP
            data-type: json # 配置文件的格式
            # 注意网关流控规则对应 gw-flow
            rule-type: flow # 表示流控规则,可配置规则:flow,degrade,authority,system,param-flow,gw-flow,gw-api-group
        ds2:
          nacos:
            server-addr: 192.168.199.128:8848 #Nacos地址
            dataId: ${spring.application.name}-degrade-sentinel
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: degrade #表示降级规则,可配置规则:flow,degrade,authority,system,param-flow,gw-flow,gw-api-group

在Nacos中添加对应的配置文件:

# 流控配置文件
[
    {
        "resource": "buy",
        "limitApp": "default",
        "grade": 1,
        "count": 2,
        "strategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    }
]
# 降级配置文件
[
  {
    "resource": "getList",
    "count": 20.0,
    "grade": 0,
    "passCount": 0,
    "timeWindow": 10
  }
]

启动服务,然后先访问一次刚才配置的资源(Sentinel懒加载机制),然后在控制台观察添加的规则是否生效

注意,在Sentinel控制台中添加的规则并不会同步到Nacos中,应用一旦重启,在控制台添加的规则依然会失效

在Nacos控制台对Sentinel规则的更改会同步到Sentinel控制以及应用程序中

六、整合Gateway

6.1 Gateway Adapter Common

Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。

sentinel-api-gateway-common-arch

Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和自定义 API 的实体和管理逻辑:

  • GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
  • ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/**/baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。

其中网关限流规则 GatewayFlowRule 的字段解释如下:

  • resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。

  • resourceMode:规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是 route。

  • grade:限流指标维度,同限流规则的 grade 字段。

  • count:限流阈值

  • intervalSec:统计时间窗口,单位是秒,默认是 1 秒。

  • controlBehavior:流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。

  • burst:应对突发请求时额外允许的请求数目。

  • maxQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。

  • paramItem :参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段:

    • parseStrategy:从请求中提取参数的策略,目前支持提取来源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数(PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。
    • fieldName:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称。
    • pattern:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流控;若为空则统计该请求属性的所有值。(1.6.2 版本开始支持)
    • matchStrategy:参数值的匹配策略,目前支持精确匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)和正则匹配(PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本开始支持)

    这些参数在集成Nacos注册动态规则源动态推送的时候会用到,也可以通过Sentinel控制台添加,缺点就是服务重启所有规则都会丢失

6.2 案例

1️⃣使用Spring Cloud Alibab能够很方便的集成Sentinel,首先需要导入Sentinel相关的依赖

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

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
            <version>1.8.3</version>
        </dependency>click to copyerrorsuccess

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

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

2️⃣添加Sentinel配置文件

package cuit.epoch.pymjl.config;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import javax.annotation.PostConstruct;

import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author Pymjl
 * @version 1.0
 * @date 2022/9/15 14:15
 **/
@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    /**
     * 这里ServerCodecConfigurer会报错:“Could not autowire. No beans of 'ServerCodecConfigurer' type found.”
     * 不影响使用,可以忽略他
     * sentinel拦截包括了视图、静态资源等,需要配置viewResolvers以及拦截之后的异常,我们也可以自定义抛出异常的提示
     *
     * @param viewResolversProvider 视图解析器供应器
     * @param serverCodecConfigurer 服务器编解码器配置
     */
    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 由于sentinel的工作原理其实借助于全局的filter进行请求拦截并计算出是否进行限流、熔断等操作的,增加SentinelGateWayFilter配置
     *
     * @return {@code SentinelGatewayBlockExceptionHandler}
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    @PostConstruct
    public void doInit() {
        initCustomizedApis();
    }

    /**
     * 自定义异常提示:当发生限流异常时,会返回定义的提示信息。
     */
    private void initCustomizedApis() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                // 自定义异常处理
                HashMap<String, String> map = new HashMap<>();
                map.put("code", "10099");
                map.put("message", "服务器忙,请稍后再试!");
                return ServerResponse.status(HttpStatus.NOT_ACCEPTABLE)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromValue(map));
            }
        };
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }
}click to copyerrorsuccess

3️⃣除此之外,用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组,然后Sentinel会针对这个分组进行限流,示例如下

    @PostConstruct
    public void doInit() {
        initCustomizedApis();
    }

    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        ApiDefinition api1 = new ApiDefinition("some_customized_api")
            .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                add(new ApiPathPredicateItem().setPattern("/ahas"));
                add(new ApiPathPredicateItem().setPattern("/product/**")
                    .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
            }});
        ApiDefinition api2 = new ApiDefinition("another_customized_api")
            .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                add(new ApiPathPredicateItem().setPattern("/**")
                    .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
            }});
        definitions.add(api1);
        definitions.add(api2);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }click to copyerrorsuccess

注意:

  • Sentinel 网关流控默认的粒度是 route 维度以及自定义 API 分组维度,默认不支持 URL 粒度。若通过 Spring Cloud Alibaba 接入,请将 spring.cloud.sentinel.filter.enabled 配置项置为 false(若在网关流控控制台上看到了 URL 资源,就是此配置项没有置为 false)。
  • 若使用 Spring Cloud Alibaba Sentinel 数据源模块,需要注意网关流控规则数据源类型是 gw-flow,若将网关流控规则数据源指定为 flow 则不生效。

3️⃣编写配置文件

bootstrap.yaml 中编写对应的配置

spring:
  application:
    name: gateway-service
  cloud:
    loadbalancer:
      nacos:
        enabled: true
    nacos:
      discovery:
        server-addr: 192.168.199.128:8848 #Nacos地址
      config:
        server-addr: 192.168.199.128:8848 #Nacos地址
        file-extension: yaml #这里我们获取的yaml格式的配置
    gateway:
      discovery:
        locator:
          enabled: true
    sentinel:
      filter:
        enabled: false
      transport:
        #配置 Sentinel dashboard 地址
        dashboard: 192.168.199.128:8858
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719click to copyerrorsuccess

在Nacos中对应的远程配置文件

spring:
  cloud:
    gateway:
    # 全局的过滤器,跨域配置
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin, RETAIN_UNIQUE
      globalcors:
        cors-configurations:
          '[/**]':
            allowedHeaders: '*'
            allowedMethods: '*'
            allowedOrigins: '*'
      # 路由      
      routes:
        - id: user_servcie_get
          uri: lb://user-service
          predicates:
            - Path=/user/get/{id}
        - id: user_service_test
          uri: lb://user-service
          predicates:
            - Path=/user/test
click to copyerrorsuccess

4️⃣测试

先启动userservice和网关,然后访问一次对应的接口,观察Sentinel控制台

image-20220915152217162

我们给user_service_test添加对应的流控规则

image-20220915152256985

然后再来访问测试

image-20220915152317106

如图所示,流控生效

七、实战经验

原文作者:Sentinel实战 - 掘金 (juejin.cn)

7.1 单机限流阈值配多少?

这个不能“拍脑袋”,配太高了可能会引发故障,配太低了又担心过早“误杀”请求。还是得根据容量规划和水位设定来配置,而且前提是监控告警灵敏。给出两种比较实用的方式:

  1. 参考单机容量规划的思路,在软负载中调整某个节点的流量权重和比例直到逼近极限为止。记录极限状态下的QPS,按照单机房70%的水位设定标准,你就可以推算出该资源的单机限流阈值了;
  2. 你可以周期性观察监控系统的流量图,得到线上真实的峰值QPS,如果该周期内峰值时段应用系统和业务都是健康状态的,那么你可以假设该峰值QPS就是理论水位。这种方式是可能造成资源浪费的,因为峰值时段可能并未达到系统承载极限,适合流量周期性比较规律的业务;

7.2 你真的需要集群限流吗?

图片.png

其实大多数场景下你并不需要使用集群限流,单机限流就足够了。仔细思考其实只有几种情况下可能需要使用到集群限流:

  1. 当想要配置单机QPS限制 < 1 时单机模式是无法满足的,只能使用集群限流模式来限制集群的总QPS。比如有个性能极差的接口单机最多只能扛住0.5 QPS,部署了10台机器那么需要将集群最大容量是5 QPS,当然这个例子有点极端。再比如我们希望某个客户调用某个接口总的最大QPS为10,但是实际我们部署了20台机器,这种情况是真实存在的;
  2. 上图中单机限流阈值是10 QPS,部署了3个节点,理论上集群的总QPS可以达到30,但是实际上由于流量不均匀导致集群总QPS还没有达到30就已经触发限流了。很多人会说这不合理,但我认为需要按真实情况来分析。如果这个 “10QPS”是根据容量规划的系统承载能力推算出来的阈值(或者说该接口请求如果超过10 QPS就可能会导致系统崩溃),那这个限流的结果就是让人满意的。如果这个“10 QPS”只是业务层面的限制,即便某个节点的QPS超过10了也不会导致什么问题,其实我们本质上是想限制整个集群总的QPS,那么这个限流的结果就不是合理的,并没有达到最佳效果;

所以,实际取决于你的限流是为了实现 “ 过载保护 ” ,还是实现业务层的限制。还有一点需要说明的是:集群限流并无法解决流量不均匀的问题,限流组件并不能帮助你重新分配或者调度流量。集群限流只是能让流量不均匀场景下整体限流的效果更好。实际使用建议是:集群限流 (实现业务层限制)+ 单机限流(系统兜底,防止被打爆)

7.3 既然网关层已经限流了,那应用层还需要限流吗?

需要的,双重保护是很有必要。同理,上游的聚合服务配置了限流,下游的基础服务也是需要配置限流的,试想下如果只配置了上游的限流,如果上游发起大量重试岂不是依旧可能压垮下游的基础服务?而且这种情况,我们在配置限流阈值时也需要特别注意,比如上游的A,B两个服务都依赖了下游Y服务,A,B分别配置的100 QPS,那么Y服务至少得配置为200 QPS,要不然有部分请求额外的经过透传和处理但最终又被拒绝,不仅是浪费资源,严重了还可能导致数据不一致等问题。

所以,最好是根据全链路总体的容量规划来配置(木桶短板原理),越早拦截越好,每一层都要配置限流。

7.4 热点参数限流功能实用吗?

功能挺实用的,可以防止热点数据(比如:热门店铺、黑马商品)占用并消耗过多的系统资源,导致对其他数据的请求处理受到严重影响。

还有一种需求,如果你做C端的产品,你想限制某用户访问某接口的最大QPS,或者你是做B端的SAAS产品,你想限制某租户访问某接口的最大QPS…热点参数默认不是为了满足这类需求而设计的,你需要自行扩展SLOT去实现类似的限制需求。当然,热点参数限流中的paramFlowItemList(参数例外项,可以实现指定某个客户ID=1的大客户访问某资源的最大QPS为100),这在某种程度上是可以实现这种特殊需求的。对于这种需求还有一种解决办法:我们在代码中定义resouceName时就直接给它赋予对应的业务数据标识(例如:queryAmount#{租户Id}),然后根据resouceName去控制台单独配置。

7.5 为什么还整出个系统自适应保护啊?

这个其实也是一种兜底的做法。当真实流量超过限流阈值一部分时,开销基本可以忽略,当真实流量远超限流阈值N倍时,尤其是像双十一大促、春晚红包、12306购票这种巨大流量的场景下,那么限流拒绝请求的开销就不能忽略了,这种情况在阿里内部称为“系统被摸死”,这种场景下自适应限流可以做好兜底。

7.6 黑白名单限制需要配吗?

如果你想根据请求来源做限制(仅放行指定上游系统过来的请求),那么该功能非常有用的。Sentinel中内置了“簇点链路监控”功能,有点类似调用链监控但目的不一样。

7.7 自动熔断降级有啥使用建议?

配置自动熔断降级前,首先我们需要识别出可能出现不稳定的服务,然后判断其是否可降级。降级处理通常是快速失败,当然我们业可以自定义降级处理结果(Fallback),例如:尝试包装返回默认结果(兜底降级),返回上一次请求的缓存结果(时效性降级),包装返回处理失败的提示结果等。

对弱依赖和次要功能的降级通常是人工推送开关来完成的,而Sentinel的熔断降级主要是在 调用端 自动判断并执行的,Sentinel基于规则中配置的时间窗口内的平均响应时间、错误比例、错误数等统计指标来执行自动熔断降级。

举个例子:我们系统同时支持“余额支付”和“银行卡支付”,这两个功能对应的接口默认在相同应用的同一线程池中,任何一方出现RT抖动和大量超时都可能请求积压并导致线程池被耗尽。假设从业务角度来看“余额支付”的比例更高,保障的优先级也更高。那么我们可以在检查到 “银行卡支付”接口(依赖第三方,不稳定)中RT持续上升或者发生大量异常时对其执行“自动熔断降级”(前提是不能导致数据不一致等影响业务流程的问题),这样优先保证“余额支付”的功能可以继续正常使用。


参考文章:

  1. 官方文档
  2. Spring Cloud Gateway集成Sentinel流控 (pymjl.com)
  3. Sentinel实战 - 掘金 (juejin.cn)
  4. Spring Cloud Alibaba Sentinel @SentinelResource | MrBird
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

结构化思维wz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值