前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringCloud项目引入Sentinel做流控

SpringCloud项目引入Sentinel做流控

原创
作者头像
eeaters
修改2022-02-21 18:04:41
1.9K0
修改2022-02-21 18:04:41
举报
文章被收录于专栏:阿杰阿杰
  • sentinel简介
  • 基本原理总结
  • Sentinel使用Demo
    • 依赖
    • Demo
  • 规则测试Demo(以熔断为例)
    • 熔断规则
    • 每秒进行统计结果数据展示
    • 断路器的状态监听
    • 请求流量
    • 结果展示
  • 微服务集成
    • DashBoard
    • 项目依赖
    • 项目配置
    • 引导类
    • 效果
  • Spring中集成
    • 资源切入点
    • Web集成
    • Feign集成
  • 动态配置集成
    • 动态数据源
    • 动态数据源-JavaConfig形式增加
    • 动态数据源-配置文件形式增加
    • 动态数据源测试
  • 结束

sentinel简介

sentinel是阿里推出的流控防护组件,随着hystrix不在维护,新的项目一般会选用 resilience4j 或者 Sentinel 进行代替,由于国内很多公司使用的就是SpringCloudAlibaba作为微服务体系.

项目现在需要使用到流控组件,先对 Sentinel 进行学习

学习路线大概分了三个步骤

  1. 通过 sentinel的wiki 对sentinel的工作原理进行学习
  2. 通过 sentinel/sentinel-demo 模块了解规则的参数含义(demo很巧妙,涨了姿势)
  3. 集成到SpringBoot项目中,并测试动态规则的配置

基本原理总结

  • 规则管理
    • 利用AuthorityRuleManager/DegradeRuleManager/FlowRuleManager进行规则管理
    • 通过静态变量进行规则全局的存储
    • 允许注册监听,当规则发生变化可以通过监听进行同步调整
  • 上下文
    • 根节点是machine-root
    • 不同适应有自己的上下文,比如默认的sentinel_default_context; web的sentinel_spring_web_context,但是上下文有数量上限,最大数量为MAX_CONTEXT_NAME_SIZE=2000
    • 不同的上下文的Node互相隔离,不同的上下文有独立的统计
  • 功能槽点
    • 通过Java的SPI机制,默认按照执行顺序有 NodeSelectorSlot /ClusterBuilderSlot /LogSlot /StatisticSlot /AuthoritySlot /SystemSlot /FlowSlot/ DegradeSlot 8个, 顺序虽然没有显示指定,但是会按照读取顺序进行加载
    • 功能槽点采用责任链的形式进行执行,DefaultProcessorSlotChain类下有一个专门引导的槽点为起点,调用上面的几个功能槽点
  • 调用流程
    • SphU.entry阶段
      • 找上下文 → 找功能槽点 → 执行功能槽点的entry阶段(不同槽点根据规则进行判定)
    • BolckException捕获阶段
      • 这里不得不提一句的是 Tracer.trace(ex);必须调用,以便于close时能够感知到异常发生进行统计,没有调用则使用异常作为熔断的逻辑无法生效
    • entry.close阶段(finally块)
      • entry中持有了功能槽点链 → 执行exit(异常统计/请求耗时等在这个阶段) → 关闭上下文

Sentinel使用Demo

依赖

目的就是在基于SpringCloud下需要引入Sentinel,因此直接引入starter项目

代码语言:javascript
复制
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

Demo

代码语言:javascript
复制
 @Test
    public void test() throws InterruptedException {
        var rule = new FlowRule();
        rule.setResource("HelloWorld");
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule.setCount(10);
        FlowRuleManager.loadRules(List.of(rule));
​
        DegradeRule degradeRule = new DegradeRule();
        degradeRule.setResource("HelloWorld");
​
        DegradeRuleManager.loadRules(List.of(degradeRule));
​
        int counter = 0;
        while (true) {
            try (Entry entry = SphU.entry("HelloWorld")) {
                if (++counter % 9 == 0) {
                    throw new RuntimeException("抛出一个异常");
                }
                System.out.println("hello world!");
            } catch (BlockException e) {
                TimeUnit.MILLISECONDS.sleep(200);
                Tracer.trace(e);
                System.out.println("block!");
            } catch (Exception e) {
                System.out.println("fallback");
            }
        }
    }

规则测试Demo(以熔断为例)

对一个接口如何进行压测是比较熟悉的, 通过jmeter等工具对接口进行压测, 但是通过Sentinel的官方Demo中,我学到了新姿势

熔断规则

代码语言:javascript
复制
private void initDegradeRule() {
    DegradeRule degradeRule = new DegradeRule(DEMO_RESOURCE);   //资源
​
    degradeRule.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType())  //采用异常率作为熔断规则
            .setCount(0.5d)             //异常率达到 50% 进行熔断
            .setStatIntervalMs(30000)   //三百毫秒作为一次bucket
            .setMinRequestAmount(50)    //请求达到50个并且才会判断异常率
            .setTimeWindow(5);         //发生熔断后多久会变为半开状态进行尝试
    DegradeRuleManager.loadRules(List.of(degradeRule));
}

每秒进行统计结果数据展示

代码语言:javascript
复制
static AtomicInteger total = new AtomicInteger();
static AtomicInteger pass = new AtomicInteger();
static AtomicInteger block = new AtomicInteger();
static AtomicInteger bizException = new AtomicInteger();
​
static volatile boolean isRunning = true;
static final String DEMO_RESOURCE = "HelloWorld";
static int runningDuration = 60;
​
​
 private void tick() {
    new Thread(() -> {
        int beforeTotal = 0, beforePass = 0, beforeBlock = 0, beforeBizException = 0;
        while (isRunning) {
            ThrowableRunnable.execute(() -> TimeUnit.SECONDS.sleep(1));
            int currentTotal = total.get();
            int currentPass = pass.get();
            int currentBlock = block.get();
            int currentBizException = bizException.get();
            String format = "倒计时第 %d 秒, 总请求数-> %d 个,通过 -> %d 个 , 阻塞 -> %d 个,异常 %d 个 \n";
            System.out.printf(format, runningDuration, currentTotal - beforeTotal,
                    currentPass - beforePass,
                    currentBlock - beforeBlock,
                    currentBizException - beforeBizException);
            beforeTotal = currentTotal;
            beforeBizException = currentBizException;
            beforePass = currentPass;
            beforeBlock = currentBlock;
            if (runningDuration-- <= 0) {
                isRunning = false;
            }
        }
    }).start();
}

断路器的状态监听

断路器的规则基本都是一样的,简单描述一下:

关闭时流量通过 → 触发限流时关闭 → 达到一定时间进入半开,放一个请求进行请求的尝试 → 尝试的请求正常则断路器关闭,请求还失败会继续开启

代码语言:javascript
复制
private void registerStateChangeObserver() {
    EventObserverRegistry.getInstance()
            .addStateChangeObserver("logging", ((prevState, newState, rule, snapshotValue) -> {
//                    snapshotValue就是当前快照点的异常率
                if (newState == CircuitBreaker.State.OPEN) {
                    System.err.printf("资源: %s 断路器开启, at %s , snapshotValue: %.2f \n",
                            rule.getResource(), TimeUtil.currentTimeMillis(), snapshotValue);
                }else{
                    System.err.printf("资源: %s -> %s at %d \n", prevState.name(), newState.name(),
                            TimeUtil.currentTimeMillis());
                }
            }));
}

请求流量

代码语言:javascript
复制
final int concurrentThread = 10;
for (int i = 0; i < concurrentThread; i++) {
    new Thread(() -> {
        while (true) {
            Entry entry = null;
            try  {
                entry = SphU.entry(DEMO_RESOURCE);
                int nextInt = ThreadLocalRandom.current().nextInt(5, 10);
                ThrowableRunnable.execute(() -> TimeUnit.MICROSECONDS.sleep(nextInt));
                pass.incrementAndGet();
                int errInt = ThreadLocalRandom.current().nextInt(0, 100);
                //大概50%出现异常情况
                if (errInt > 50) {
                    throw new RuntimeException("randomException");
                }
            } catch (BlockException e) {
                block.incrementAndGet();
                int nextInt = ThreadLocalRandom.current().nextInt(5, 10);
                ThrowableRunnable.execute(() -> TimeUnit.MICROSECONDS.sleep(nextInt));
            } catch (Throwable e) {
                bizException.incrementAndGet();
                //当发生异常时,这段代码为必须执行 ; 在后面close执行的时候,会从entry中获取异常,不为空则异常累加一次
                Tracer.traceEntry(e, entry);
            }finally {
                total.incrementAndGet();
                if (entry != null) {
                    entry.exit();
                }
            }
        }
    }).start();
}
ThrowableRunnable.execute(() -> Thread.currentThread().join());

结果展示

程序每秒都会滴滴答答的展示当前的检测结果,挑选部分结果进行展示

代码语言:javascript
复制
资源: HelloWorld 断路器开启, at 1644550765275 , snapshotValue: 0.50
倒计时第 60 秒, 总请求数-> 2771 个,通过 -> 742 个 , 阻塞 -> 2039 个,异常 368 个
倒计时第 59 秒, 总请求数-> 4223 个,通过 -> 0 个 , 阻塞 -> 4222 个,异常 0 个
倒计时第 58 秒, 总请求数-> 4577 个,通过 -> 0 个 , 阻塞 -> 4578 个,异常 0 个
倒计时第 57 秒, 总请求数-> 4757 个,通过 -> 0 个 , 阻塞 -> 4756 个,异常 0 个
倒计时第 56 秒, 总请求数-> 4739 个,通过 -> 0 个 , 阻塞 -> 4740 个,异常 0 个
资源: OPEN -> HALF_OPEN at 1644550770276
资源: HelloWorld 断路器开启, at 1644550770278 , snapshotValue: 1.00
倒计时第 55 秒, 总请求数-> 4634 个,通过 -> 1 个 , 阻塞 -> 4633 个,异常 1 个
倒计时第 54 秒, 总请求数-> 3603 个,通过 -> 0 个 , 阻塞 -> 3603 个,异常 0 个
倒计时第 53 秒, 总请求数-> 4629 个,通过 -> 0 个 , 阻塞 -> 4628 个,异常 0 个
倒计时第 52 秒, 总请求数-> 4829 个,通过 -> 0 个 , 阻塞 -> 4829 个,异常 0 个
倒计时第 51 秒, 总请求数-> 4572 个,通过 -> 0 个 , 阻塞 -> 4573 个,异常 0 个
资源: OPEN -> HALF_OPEN at 1644550775278
资源: HelloWorld 断路器开启, at 1644550775280 , snapshotValue: 1.00
倒计时第 50 秒, 总请求数-> 4731 个,通过 -> 1 个 , 阻塞 -> 4729 个,异常 1 个
倒计时第 49 秒, 总请求数-> 4892 个,通过 -> 0 个 , 阻塞 -> 4892 个,异常 0 个

微服务集成

DashBoard

sentinel有自己的控制台, 下载页面

电脑的默认JDK环境是17启动失败了,这时候跑到JDK8的目录下执行命令即可 :

java -jar 目录\sentinel-dashboard-1.8.3.jar --server.port=9000

界面还是很不错的,可以通过界面直接设置一些流控规则,但是很明显这是给应急用的,没有发现持久化的规则方式当服务启动时规则肯定会丢失的; 远程配置中心和Sentinel继承肯定是要的

项目依赖

引入web /openfeign /nacos依赖

代码语言:javascript
复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
​
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
​
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
​
<!-- https://mvnrepository.com/artifact/com.alibaba.csp/sentinel-datasource-nacos -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

项目配置

代码语言:javascript
复制
spring:
  application:
    name: consumer-application
    sentinel:
      transport:
        port: 8719
        dashboard: localhost:9000
server:
  port: 10002

引导类

上一个专门写日常测试的仓库模块太多太难受,这里只是用一个模块进行测试,因此采用ApplicationBuilder来启动项目, 对应不同的配置文件

代码语言:javascript
复制
@EnableAutoConfiguration
@ComponentScan
@RestController
public class ProviderApplication {
​
    public static void main(String[] args) {
        new SpringApplicationBuilder(ProviderApplication.class)
                .run("--spring.profiles.active=provider");
    }
​
    @GetMapping("/echo")
    public ResponseEntity<String> echo(@RequestParam Integer sleepTime) throws InterruptedException {
        TimeUnit.SECONDS.sleep(sleepTime);
        return ResponseEntity.ok("成功");
    }
​
    @GetMapping("/isOk")
    public ResponseEntity<String> isOk(@RequestParam Boolean isOk) {
        if (isOk) {
            return ResponseEntity.ok("ok");
        }
        return ResponseEntity.badRequest().body("not ok");
    }
​
}

效果

当发生请求后,通过控制台可以看到资源请求的情况,也可以通过控制台对资源进行流控管理

Spring中集成

资源切入点

盲猜也是通过aop对方法进行横切,然后切面中进行增强. Sentinel目前切入的方式大致分三种:

  1. 编码形式:
代码语言:javascript
复制
- 比如demo的方式,手动通过SphU.entry进入,entry.close进行关闭注解形式:
代码语言:javascript
复制
- @SentinelResource,比如需要对业务代码某一块进行管理,那么可以在代码上加入注解产生一个Resource. 然后对方法进行拦截,拦截过程中增加流控逻辑 ; AOP类: `SentinelResourceAspect` 类三方集成
代码语言:javascript
复制
- 集成一般都在\*-starter 包, 逼格高的有一个autoconfig 包, 里面会有自动装配的配置类我们前面引入的starter-alibaba-sentinel是在一块的,这里在 META-INF/spring.factories下 看下默认引入装配类有哪些:
代码语言:javascript
复制
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration,\
com.alibaba.cloud.sentinel.SentinelWebFluxAutoConfiguration,\
com.alibaba.cloud.sentinel.endpoint.SentinelEndpointAutoConfiguration,\
com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration,\
com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration
​
org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker=\
com.alibaba.cloud.sentinel.custom.SentinelCircuitBreakerConfiguration

Web集成

核心在 SentinelWebAutoConfiguration 中

  1. @ConditionalOnProperty(name = "spring.cloud.sentinel.enabled", matchIfMissing = true)默认就会生效
  2. SentinelWebInterceptor → 通过Servlet的拦截器进行插入
  3. AbstractSentinelInterceptor#SENTINEL_SPRING_WEB_CONTEXT_NAME → web的上下文名称为 sentinel_spring_web_context
  4. SentinelWebInterceptor#getResourceName资源名称请求的uri,比如: /echo

Feign集成

核心在 SentinelFeignAutoConfiguration 中

  1. Sentinel利用Feign的扩展点, 默认是创建 ReflectiveFeign ,Sentinel搞了一个 SentinelFeign
  2. @ConditionalOnProperty(name = "feign.sentinel.enabled")默认并没有开启,所以需要配置文件标记位启用
  3. SentinelInvocationHandler#invoke →资源名为HttpMethod.toUpperCase+":"+url+path

动态配置集成

控制台上也可以增加流控规则,但是因为只能针对某一个节点并且没有持久化,一般只能简单用用,生产上还是需要将配置进行统一的管理

动态数据源

我们通过配置文件对Sentinel的行为进行调整,对应的Java类是: SentinelProperties

装配入口在 SentinelAutoConfiguration 中, SentinelDataSourceHandler 则是将配置文件进行解析产生对应的资源对象

SentinelProperties#datasource变量明显是进行数据源管理的,

DataSourcePropertiesConfiguration则是数据源的配置项,该类代码如下:

代码语言:javascript
复制
public class DataSourcePropertiesConfiguration {
    private FileDataSourceProperties file;
    private NacosDataSourceProperties nacos;
    private ZookeeperDataSourceProperties zk;
    private ApolloDataSourceProperties apollo;
    private RedisDataSourceProperties redis;
    private ConsulDataSourceProperties consul;
}

也就是上面六种形式都是支持通过配置文件形式进行配置的. 我在官网上看到还支持etcd, 可能暂时不支持直接使用配置文件,需要手动指定一下

动态数据源-JavaConfig形式增加

以file形式为例, 不使用配置文件而使用Java代码的形式配置一个基于文件的配置管理

代码语言:javascript
复制
@Configuration
public class SentinelConfig {
​
    @Bean
    public SentinelDataSourceHandler sentinelDataSourceHandler(
            DefaultListableBeanFactory beanFactory, SentinelProperties sentinelProperties,
            Environment env) {
​
        FileDataSourceProperties fileDataSourceProperties = new FileDataSourceProperties();
        fileDataSourceProperties.setFile("classpath:sentinel/degraderule.json");
        fileDataSourceProperties.setRuleType(RuleType.DEGRADE);
        DataSourcePropertiesConfiguration custom = new DataSourcePropertiesConfiguration();
        custom.setFile(fileDataSourceProperties);
​
        Map<String, DataSourcePropertiesConfiguration> datasource = sentinelProperties.getDatasource();
        datasource.put("file", custom);
​
        return new SentinelDataSourceHandler(beanFactory, sentinelProperties, env);
    }
}

动态数据源-配置文件形式增加

因为本地有nacos的exe文件,那么就以nacos为例进行了测试,

配置文件也是按照Java类的成员变量来的,对着 SentinelProperties来配置一个nacos的配置,

代码语言:javascript
复制
spring:
  application:
    name: invoke-application
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    sentinel:
      transport:
        port: 8719
        dashboard: localhost:9000
      datasource:
        nacosRule:
          nacos:
            serverAddr: 127.0.0.1:8848
            dataId: demo-sentinel
            ruleType: flow
# 启用SentinelFeign
feign:
  sentinel:
    enabled: true

动态数据源测试

之前demo中学到了一个tick滴滴答答的进行数据展示, 这里也搞一个tick进行资源的打印

代码语言:javascript
复制
  @Bean
    CommandLineRunner runner() {
        return arg -> {
            tickRule();
        };
    }
​
    private void tickRule() {
        new Thread(() -> {
            while (true) {
                System.err.println("======== ");
                List<FlowRule> rules = FlowRuleManager.getRules();
                for (FlowRule rule : rules) {
                    String resource = rule.getResource();
                    int strategy = rule.getStrategy();
                    double count = rule.getCount();
                    String format = String.format("资源:%s , 策略: %d , count: %s", resource, strategy, count);
                    System.err.println("format = " + format);
                }
                ThrowableRunnable.execute(() -> TimeUnit.SECONDS.sleep(60));
            }
​
        }).start();
​
    }

nacos上配置规则

代码语言:javascript
复制
[
  {
    "resource": "GET:http:://localhost:10000/echo",
    "count": 1,
    "grade": 1,
    "timeWindow": 1
  }
]

项目启动后,对规则调整一次,规则进行了正确的打印

代码语言:javascript
复制
========
format = 资源:GET:http:://localhost:10000/echo , 策略: 0 , count: 1.0
========
format = 资源:GET:http:://localhost:10000/echo , 策略: 0 , count: 1.0
format = 资源:GET:http:://localhost:10000/echo2 , 策略: 0 , count: 2.0
========
format = 资源:GET:http:://localhost:10000/echo , 策略: 0 , count: 1.0
format = 资源:GET:http:://localhost:10000/echo2 , 策略: 0 , count: 2.0

结束

over

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • sentinel简介
  • 基本原理总结
  • Sentinel使用Demo
    • 依赖
      • Demo
      • 规则测试Demo(以熔断为例)
        • 熔断规则
          • 每秒进行统计结果数据展示
            • 断路器的状态监听
              • 请求流量
                • 结果展示
                • 微服务集成
                  • DashBoard
                    • 项目依赖
                      • 项目配置
                        • 引导类
                          • 效果
                          • Spring中集成
                            • 资源切入点
                              • Web集成
                                • Feign集成
                                • 动态配置集成
                                  • 动态数据源
                                    • 动态数据源-JavaConfig形式增加
                                      • 动态数据源-配置文件形式增加
                                        • 动态数据源测试
                                        • 结束
                                        相关产品与服务
                                        微服务引擎 TSE
                                        微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
                                        领券
                                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档