Sentinel:分布式系统的流量防卫兵

Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

  1. Sentinel 具有以下特征:
    1. 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
    2. 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
    3. 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
    4. 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

1 快速开始

  1. 下载https://github.com/alibaba/Sentinel/releases
  2. 运行
java -jar sentinel-dashboard-1.8.0.jar
  1. 默认在8080端口启动
    localhost:8080
    用户名:sentinel
    密码:sentinel
  2. pom.xml
<dependencies>
    <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <groupId>org.example</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
    <!--SpringCloud ailibaba sentinel -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件+actuator -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--日常通用jar包配置-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>4.6.3</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

</dependencies>
  1. application.yml
spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard地址
        port: 8719
      
management:
  endpoints:
    web:
      exposure:
        include: '*'

feign:
  sentinel:
    enabled: true # 激活Sentinel对Feign的支持
  1. 主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class MainApp8401 {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(MainApp8401.class,args);
    }
}
  1. controller
@RestController
public class FlowLimitController {
    
    

    @GetMapping("/testA")
    public String testA(){
    
    
        return "------------testA";
    }
    @GetMapping("/testB")
    public String testB(){
    
    
        return "------------testB";
    }
}
  1. 懒加载机制,需要执行一次访问
    在这里插入图片描述

2 流控规则

2.1 流量控制

  • 限流的直接表现是在执行 Entry nodeA = SphU.entry(resourceName) 的时候抛出 FlowException 异常。FlowException 是 BlockException 的子类,可以捕捉 BlockException 来自定义被限流之后的处理逻辑。
  • 一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:
    resource:资源名,即限流规则的作用对象
    count: 限流阈值
    grade: 限流阈值类型(QPS 或并发线程数)
    limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
    strategy: 调用关系限流策略
    controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)

2.1.1 基于QPS的流量控制

若使用除了直接拒绝之外的流量控制效果,则调用关系限流策略(strategy)会被忽略。

  1. 直接拒绝(默认方式)
    当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。
  2. Warm Up(预热/冷启动方式)
  • 通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
  • 默认 coldFactor 为 3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。
  1. 匀速排队
  • 严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
  • Sentinel 匀速排队等待策略是 Leaky Bucket 算法结合虚拟队列等待机制实现的。
  • 它的中心思想是,以固定的间隔时间让请求通过。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过(排队等待处理);若预期的通过时间超出最大排队时长,则直接拒接这个请求。
  • 匀速排队模式暂时不支持 QPS > 1000 的场景。

2.1.2 基于并发数的流量控制

Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置

2.1.3 基于调用关系的流量控制

  1. 根据调用方限流

    1. default:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。
    2. {some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者caller1的规则,那么当且仅当来自 caller1 对 NodeA 的请求才会触发流量控制。
    3. other:表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1 对 NodeA 的调用,都不能超过 other 这条规则定义的阈值
    • 同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default
  2. 根据调用链路入口限流:链路限流
    NodeSelectorSlot 中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root 的虚拟节点,调用链的入口都是这个虚节点的子节点。
    在这里插入图片描述

  3. 具有关系的资源流量控制:关联流量控制
    当关联资源达到阈值时,就限流自己

2.2 集群流控

2.3 网关流控

3 降级规则(Sentinel 1.8.0及以上版本)

3.1 熔断策略

  1. 慢调用比例
    1. 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。
    2. 当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
    3. 若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  2. 异常比例
    1. 当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
    2. 若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
    3. 异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  3. 异常数
    1. 当单位统计时长内的异常数目超过阈值之后会自动进行熔断。
    2. 若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
  • 开源整合模块,如 Sentinel Dubbo Adapter, Sentinel Web Servlet Filter 或 @SentinelResource 注解会自动统计业务异常,无需手动调用。

3.2 熔断降级规则说明

Field 说明 默认值
resource 资源名,即规则的作用对象
grade 熔断策略,支持慢调用比例/异常比例/异常数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow 熔断时长,单位为 s
minRequestAmount 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

4 热点参数限流

  1. 测试方法
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
                          @RequestParam(value = "p2",required = false) String p2){
    
    
    return "----------------testHotKey";
}
public String deal_testHotKey(String pi, String p2, BlockException e){
    
    
    return "-------------deal_testHotKey,/(ㄒoㄒ)/~~";
}
  1. 配置规则
    在这里插入图片描述

  2. 热点参数规则

属性 说明 默认值
resource 资源名,必填
count 限流阈值,必填
grade 限流模式 QPS 模式
durationInSec 统计窗口时间长度(单位为秒),1.6.0 版本开始支持 1s
controlBehavior 流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持 快速失败
maxQueueingTimeMs 最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持 0ms
paramIdx 热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置
paramFlowItemList 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型
clusterMode 是否是集群参数流控规则 false
clusterConfig 集群流控相关配置

5 系统规则

  1. 系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
  2. 系统规则支持以下的模式:
    1. Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
    2. CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
    3. 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
    4. 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
    5. 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

6 SentinelResource配置

6.1 按资源名称限流

  1. controller
@RestController
public class RateLimitController {
    
    

    @GetMapping("/byResource")
    @SentinelResource(value = "byResource", blockHandler = "handleException")
    public CommonResult byResource() {
    
    
        return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
    }

    public CommonResult handleException(BlockException e) {
    
    
        return new CommonResult(444, e.getClass().getCanonicalName() + "\t 服务不可用");
    }
}
  1. 配置流量规则
    在这里插入图片描述

  2. 自定义的限流处理信息
    在这里插入图片描述

6.2 按URL地址限流

  1. controller
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl() {
    
    
    return new CommonResult(200, "按URL限流测试OK", new Payment(2020L, "serial002"));
}
  1. 配置流量规则
    在这里插入图片描述

  2. 未自定义限流处理信息
    在这里插入图片描述

6.3 自定义限流处理逻辑

  1. controller
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
        blockHandlerClass = CustomerBlockHandler.class,
        blockHandler = "handlerException")
public CommonResult customerBlockHandler() {
    
    
    return new CommonResult(200, "按客户自定义", new Payment(2020L, "serial003"));
}
  1. handler
public class CustomerBlockHandler {
    
    

    public static CommonResult handlerException(BlockException e){
    
    
        return new CommonResult(4444,"按客户自定义,global handlerException---1");
    }

    public static CommonResult handlerException2(BlockException e){
    
    
        return new CommonResult(4444,"按客户自定义,global handlerException---2");
    }
}

6 服务熔断功能

6.1 Sentinel+Ribbon

  1. 建立9003微服务提供者

    1. pom.xml
     <dependencies>
       <!--SpringCloud ailibaba nacos -->
       <dependency>
           <groupId>com.alibaba.cloud</groupId>
           <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
       </dependency>
       <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
           <groupId>org.example</groupId>
           <artifactId>cloud-api-commons</artifactId>
           <version>1.0-SNAPSHOT</version>
       </dependency>
       <!-- SpringBoot整合Web组件 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-actuator</artifactId>
       </dependency>
       <!--日常通用jar包配置-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-devtools</artifactId>
           <scope>runtime</scope>
           <optional>true</optional>
       </dependency>
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <optional>true</optional>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
     </dependencies>
    
    1. application.yml
     server:
       port: 9003
    
     spring:
       application:
         name: nacos-payment-provider
       cloud:
         nacos:
           discovery:
             server-addr: localhost:8848 #配置Nacos地址
    
     management:
       endpoints:
         web:
           exposure:
             include: '*'
    
    1. 主启动类
     @SpringBootApplication
     @EnableDiscoveryClient
     public class PaymentMain9003 {
          
          
       public static void main(String[] args) {
          
          
           SpringApplication.run(PaymentMain9003.class,args);
       }
     }
    
    1. controller
     @RestController
     public class PaymentController {
          
          
    
         @Value("${server.port}")
         private String serverPort;
    
         public static HashMap<Long, Payment> hashMap = new HashMap<>();
         static {
          
          
             hashMap.put(1L,new Payment(1L,"111111111111"));
             hashMap.put(2L,new Payment(2L,"222222222222"));
             hashMap.put(3L,new Payment(3L,"333333333333"));
         }
         @GetMapping("/paymentSQL/{id}")
         public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
          
          
             Payment payment = hashMap.get(id);
             CommonResult<Payment> result = new CommonResult<>(200, "from mysql,serverPort: " + serverPort, payment);
             return result;
         }
     }
    
  2. 建立9004微服务提供者

  3. 建立84消费者

    1. pom.xml
     <dependencies>
       <!--SpringCloud ailibaba nacos -->
       <dependency>
           <groupId>com.alibaba.cloud</groupId>
           <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
       </dependency>
       <!--SpringCloud ailibaba sentinel -->
       <dependency>
           <groupId>com.alibaba.cloud</groupId>
           <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
       </dependency>
       <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
       <dependency>
           <groupId>org.example</groupId>
           <artifactId>cloud-api-commons</artifactId>
           <version>1.0-SNAPSHOT</version>
       </dependency>
       <!-- SpringBoot整合Web组件 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-actuator</artifactId>
       </dependency>
       <!--日常通用jar包配置-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-devtools</artifactId>
           <scope>runtime</scope>
           <optional>true</optional>
       </dependency>
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <optional>true</optional>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
     </dependencies>
    
    1. application.yml
     server:
       port: 84
    
     spring:
       application:
         name: nacos-order-consumer
       cloud:
         nacos:
           discovery:
             server-addr: localhost:8848
         sentinel:
           transport:
             #配置Sentinel dashboard地址
             dashboard: localhost:8080
             #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
             port: 8719
    
     #消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
     service-url:
       nacos-user-service: http://nacos-payment-provider
    
    1. 主启动类
     @SpringBootApplication
     @EnableDiscoveryClient
     public class OrderNacosMain84 {
          
          
       public static void main(String[] args) {
          
          
           SpringApplication.run(OrderNacosMain84.class,args);
       }
     }
    
    1. 配置类
     @Configuration
     public class ApplicationContextConfig
     {
          
          
         @Bean
         @LoadBalanced
         public RestTemplate getRestTemplate()
         {
          
          
             return new RestTemplate();
         }
     }
    
    1. 业务类
    • fallback只负责业务异常
    • blockHandler只负责sentinel控制台配置违规
    • exceptionsToIgnore排除属性
     @RestController
     @Slf4j
     public class CircleBreakerController {
          
          
    
       public static final String SERVICE_URL = "http://nacos-payment-provider";
    
       @Resource
       private RestTemplate restTemplate;
    
       @RequestMapping("/consumer/fallback/{id}")
       //@SentinelResource(value = "fallback") //没有配置
       //@SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback只负责业务异常
       //@SentinelResource(value = "fallback",blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
       @SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler",
               exceptionsToIgnore = {
          
          IllegalArgumentException.class})  //IllegalArgumentException异常不在走fallback方法
       public CommonResult<Payment> fallback(@PathVariable Long id)
       {
          
          
           CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);
    
           if (id == 4) {
          
          
               throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");
           }else if (result.getData() == null) {
          
          
               throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");
           }
    
           return result;
       }
       //本例是fallback
       public CommonResult handlerFallback(@PathVariable  Long id,Throwable e) {
          
          
           Payment payment = new Payment(id,"null");
           return new CommonResult<>(444,"业务异常handlerFallback,exception内容  "+e.getMessage(),payment);
       }
       //本例是blockHandler
       public CommonResult blockHandler(@PathVariable  Long id, BlockException blockException) {
          
          
           Payment payment = new Payment(id,"null");
           return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException  "+blockException.getMessage(),payment);
       }
     }
    

6.2 Sentinel+OpenFeign

  1. pom.xml
    在84消费者端添加openfeign依赖
<!--SpringCloud openfeign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. application.yml
    激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: true
  1. 主启动类
    @EnableFeignClients
  2. service接口
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
    
    
    @GetMapping("/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
  1. fallback方法,PaymentFallbackService,注意加上@Component注解
@Component
public class PaymentFallbackService implements PaymentService {
    
    
    @Override
    public CommonResult<Payment> paymentSQL(Long id) {
    
    
        return new CommonResult<>(444,"服务降级返回,------PaymentFallbackService",new Payment(id,"errorSerial"));
    }
}
  1. controller
@Resource
private PaymentService paymentService;

@GetMapping("/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
    
    
    return paymentService.paymentSQL(id);
}
  1. 服务端停止后自动降级

7 持久化

一旦我们重启应用,Sentinel规则将消失,生产环境需要将配置规则进行持久化

  1. pom.xml
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
  1. application.yml
spring: 
  cloud: 
    sentinel: 
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848
            dataId: cloudalibaba-sentinel-service
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow
  1. Nacos添加配置
[
    {
    
    
        "resource": "/rateLimit/byUrl",
        "limitApp": "default",
        "grade": 1,
        "count": 1,
        "strategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    }
]

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

  1. 启动微服务并访问资源,Sentinel增加了流控规则
    在这里插入图片描述

  2. 关闭微服务,sentinel流控规则消失

  3. 重启微服务并访问资源,sentinel流控规则重新出现并生效

猜你喜欢

转载自blog.csdn.net/qq_40857365/article/details/113196006