SentinelDashboard-Nacos动态数据源-SpringCloudGateway

友情提示:要下载源码进行改造,本文基于Sentinel-1.8.2。

以下内容摘自个人的技术文档,相关资料主要来自SpringCloudAlibaba和Github-Sentinel。正文是部分节选内容,仅供参考。

4.1.2. 网关流控

网关限流规则,是针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。

与普通的应用流控相同,网关流控触发后同样会返回这样的结果。

{
    "code":429,
    "message":"Sentinel block exception",
    "route":"/"
}

4.1.2.1. 基础结构

前端代码

  • 前端页面:webapp/resources/app/views/gateway/flow.html
  • js文件:webapp/resources/app/scripts/controllers/gateway/flow.js

后台代码

  • Package:com.alibaba.csp.sentinel.dashboard.controller.gateway
  • Class:GatewayFlowRuleController
    • GET /gateway/api/list.json - 查询流控规则
    • POST /gateway/api/new.json - 新增流控规则
    • PUT /gateway/api/save.json - 修改流控规则
    • DELETE /gateway/api/delete.json - 删除流控规则

4.1.2.2. 查询流控规则

接口入参

参数名 类型 规则约束 说明
app String 字符串非空 gateway网关ApplicationName,对应网关配置文件中spring.application.name属性的值
ip String 字符串非空 网关服务器IP
port Integer 非空 网关服务器端口,对应网关配置文件中sentinel.transport.port的值

内部逻辑

SentinelDashboard-Nacos动态数据源-SpringCloudGateway

4.1.2.3. 新增流控规则

接口入参

参数名 类型 规则约束 说明
app String 字符串非空 应用名称
ip String 字符串非空 应用IP
port Integer 应用端口号
resource String 字符串非空 流控针对的资源
resourceMode Integer 非空,RESOURCE_MODE_ROUTE_ID=0;RESOURCE_MODE_CUSTOM_API_NAME=1 资源模式
grade Integer 非空,0为线程数,1为qps 限流指标维度,同限流规则的 grade 字段
count Double 大于等于0 限流阈值
interval Long 大于0 统计间隔,默认是 1
intervalUnit Integer 秒、分、时、天 统计间隔单位,默认是秒
controlBehavior Integer 非空,0-快速失败 2-匀速排队 流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
burst Integer 快速失败时必填,大于等于0 应对突发请求时额外允许的请求数目
maxQueueingTimeoutMs Integer 匀速排队时必填,大于等于0 匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效
paramItem GPFIV 参数限流配置

GPFIV:GatewayParamFlowItemVo若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。

参数名 类型 规则约束 说明
parseStrategy Integer 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie 从请求中提取参数的策略
fieldName String 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称
pattern String 参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流控;若为空则统计该请求属性的所有值。(1.6.2 版本开始支持)
matchStrategy Integer 精确匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)、正则匹配(PARAM_MATCH_STRATEGY_REGEX 参数值的匹配策略(1.6.2 版本开始支持)

内部逻辑
SentinelDashboard-Nacos动态数据源-SpringCloudGateway

4.1.2.4. 修改流控规则

接口入参

除了用id替代了部分参数外,其他参数都与新增接口的入参相同

参数名 类型 规则约束 说明
id Long 非空 规则id
grade Integer 非空,0为线程数,1为qps 限流指标维度,同限流规则的 grade 字段
count Double 大于等于0 限流阈值
interval Long 大于0 统计间隔,默认是 1
intervalUnit Integer 秒、分、时、天 统计间隔单位,默认是秒
controlBehavior Integer 非空,0-快速失败 2-匀速排队 流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
burst Integer 快速失败时必填,大于等于0 应对突发请求时额外允许的请求数目
maxQueueingTimeoutMs Integer 匀速排队时必填,大于等于0 匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效
paramItem GPFIV 参数限流配置

内部逻辑(与新增相同)

4.1.2.5. 删除流控规则

接口入参

参数名 类型 规则约束 说明
id Long 非空 规则id

内部逻辑(大致与新增、修改相同,不再赘述)

5. 动态数据源

5.1. 前置知识

5.1.1. 什么是动态数据源

开源框架中(至少截止1.8.2版本),所有的规则都是存储在内存中(JVM中以Map形式存在)。但是在生产环境中,我们通常希望有一个固定的配置文件或者数据源来完成这些规则的持久化及使用,这就是动态数据源。

本项目是Sentinel Dashboard,而不是Sentinel。由于Dashboard提供了比较完善的规则管理功能,所以我们期望通过Dashboard来进行规则配置,而不是通过Nacos这样的配置中心对文本进行修改。那么,为了满足这样的需求,就需要把动态数据源集成入Dashboard中,从而将用户在Dashboard中配置的规则持久到动态数据源中。

Sentinel(也包括Dashboard)提供了两种动态数据源方案:

  • 拉模式,客户端(集成Sentinel的应用)主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;

  • 推模式,规则中心统一推送,客户端(集成Sentinel的应用)通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。
    SentinelDashboard-Nacos动态数据源-SpringCloudGateway

Sentinel 目前支持以下数据源扩展:

5.1.2. 拉模式拓展

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

5.1.3. 推模式拓展

实现推模式的数据源最简单的方式是继承 AbstractDataSource 抽象类,在其构造方法中添加监听器,并实现 readSource() 从指定数据源读取字符串格式的配置数据。比如 基于 Nacos 的数据源

控制台通常需要做一些改造来直接推送应用维度的规则到配置中心。功能示例可以参考 AHAS Sentinel 控制台的规则推送功能。改造指南可以参考 在生产环境中使用 Sentinel 控制台

5.1.4. 注册数据源

本小节内容是单独使用Sentinel时需要做的步骤,如果使用SpringCloudAlibaba集成Sentinel,则不需要。

通常需要调用以下方法将数据源注册至指定的规则管理器中:

ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, parser);
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());

若不希望手动注册数据源,可以借助 Sentinel 的 InitFunc SPI 扩展接口。只需要实现自己的 InitFunc 接口,在 init 方法中编写注册数据源的逻辑。比如:

package com.test.init;

public class DataSourceInitFunc implements InitFunc {

    @Override
    public void init() throws Exception {
        final String remoteAddress = "localhost";
        final String groupId = "Sentinel:Demo";
        final String dataId = "com.alibaba.csp.sentinel.demo.flow.rule";

        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId,
            source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }
}

接着将对应的类名添加到位于资源目录(通常是 resource 目录)下的 META-INF/services 目录下的 com.alibaba.csp.sentinel.init.InitFunc 文件中,比如:

com.test.init.DataSourceInitFunc

这样,当初次访问任意资源的时候,Sentinel 就可以自动去注册对应的数据源了。

5.1.5. 官方拓展示例

Sentinel官方在源码中提供了针对应用流控规则的动态数据源拓展示例。Sentinel开源版提供了基于内存的规则引擎,为了使用数据源进行持久化,有必要对其进行改造。

官方针对Nacos作为动态数据源的情况写了专门的样本代码,该样本针对的是流控规则。

回顾一下流控规则的后台代码:

  • Package:com.alibaba.csp.sentinel.dashboard.controller
  • Class:FlowControllerV1
    • GET /v1/flow/rules - 查询流控规则
    • POST /v1/flow/rule - 新增流控规则
    • PUT /v1/flow/save.json - 修改流控规则
    • DELETE /v1/flow/delete.json - 删除流控规则

样本代码则是:

  • Package:com.alibaba.csp.sentinel.dashboard.controller.v2
  • Class:FlowControllerV2
    • GET /v2/flow/rules - 查询流控规则
    • POST /v2/flow/rule - 新增流控规则
    • PUT /v2/flow/save.json - 修改流控规则
    • DELETE /v2/flow/delete.json - 删除流控规则

Sentinel官方在dashboard的test包中提供了nacos的关键样例(test/com.alibaba.csp.sentinel.dashboard.rule.nacos),也包括了Apollo和Zookeeper。以下主要介绍基于Nacos作为动态数据源如何操作。

5.1.5.1. 拷贝样例类

位于test/com.alibaba.csp.sentinel.dashboard.rule.nacos下,有4个类:

  • FlowRuleNacosProvider
  • FlowRuleNacosPublisher
  • NacosConfig
  • NacosConfigUtil

将它们拷贝到SentinelDashboard中。由于NacosConfig中填写的Nacos服务地址是localhost,我们要改为ip:port的格式,通常我们会将它定义个配置项,挪到配置文件中定义或者用启动参数指定。

5.1.5.2. 添加数据源依赖

由于官方将数据源依赖设置为了test范围,我们需要将其开放使用,只要将pom.xml中下面的标签注释掉即可。

		<!-- for Nacos rule publisher sample -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
            <scope>test</scope>
        </dependency>

5.1.5.3. 修改前端路由

默认前端访问的后端接口上面提到了,是这些:

  • GET /v1/flow/rules - 查询流控规则
  • POST /v1/flow/rule - 新增流控规则
  • PUT /v1/flow/save.json - 修改流控规则
  • DELETE /v1/flow/delete.json - 删除流控规则

现在要改为/v2/……,所以要把前端改造一下。虽然前端是Angular开发的,但对于全栈来说,不用担心看看就知道是怎么回事。

Angular和Vue差不多,需要用node.js进行编译打包之类的操作,所以这里需要安装node.js,建议使用11.15.0,否则可能会遇到一些奇奇怪怪的问题。如果希望同时管理多个node版本,可以参考这篇博客

node相关命令:

# 安装包
npm -install

# 运行(开发)
npm start

# 打包(生产)
npm run build

前端工程的结构大概是这样的(只摘录必要的结构,其他的已省略):

  • webapp/resources/

    • app

      • scripts
        • controllers
          • flow_v1.js ------------------------------ 原始流控前端js
          • flow_v2.js ------------------------------ 动态数据源前端js
        • services
          • flow_service_v1.js ------------------- 原始流控后端API定义
          • flow_service_v2.js ------------------- 动态数据源后端API定义
        • directives
          • sidebar
            • sidebar.html --------------------- 定义路由
        • app.js ------------------------------------------ 定义全局的控件,是核心配置
      • views
        • flow_v1.html --------------------------------- 原始流控规则配置页面
        • flow_v2.html --------------------------------- 动态数据源流控规则配置页面
    • dist ---------------------------------------------------------- 存放生产环境编译打包后的文件

    • tmp ---------------------------------------------------------- 存放开发环境编译打包后的文件

    • gulpfile.js -------------------------------------------------- service 文件定义的地方(后续扩展新的代码要用)

此处有两个关键,app.js和sidebar.html,在app.js中定义了两个state,是我们可以直接用的,内容如下:

      .state('dashboard.flowV1', {
        templateUrl: 'app/views/flow_v1.html',
        url: '/flow/:app',
        controller: 'FlowControllerV1',
        resolve: {
          loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) {
            return $ocLazyLoad.load({
              name: 'sentinelDashboardApp',
              files: [
                'app/scripts/controllers/flow_v1.js',
              ]
            });
          }]
        }
      })

      .state('dashboard.flow', {
          templateUrl: 'app/views/flow_v2.html',
          url: '/v2/flow/:app',
          controller: 'FlowControllerV2',
          resolve: {
              loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) {
                  return $ocLazyLoad.load({
                      name: 'sentinelDashboardApp',
                      files: [
                          'app/scripts/controllers/flow_v2.js',
                      ]
                  });
              }]
          }
      })

对应的,控制台左侧的导航中的连接,则是在sidebar.html中定义:

<!-- 原始流控规则导航 -->
<li ui-sref-active="active" ng-if="!entry.isGateway">
	<a ui-sref="dashboard.flowV1({app: entry.app})">
	<i class="glyphicon glyphicon-filter"></i>&nbsp;&nbsp;流控规则</a>
</li>
<!-- 改成动态数据源流控规则导航 -->
<li ui-sref-active="active" ng-if="!entry.isGateway">
	<a ui-sref="dashboard.flow({app: entry.app})">
	<i class="glyphicon glyphicon-filter"></i>&nbsp;&nbsp;流控规则</a>

很显然标签中的dashboard.flowV1就是app.js中state所定义的组件。

前端改好后,可以直接启动或者打包后启动来调试,看看是否成功。

5.2. 基于SpringCloudGateway

SpringCloudGateway由于Sentinel官方已经做好了适配,所以集成起来还是比较简单。

Gateway相关的代码基本都在gateway包或者目录下,很好找。

此处为了保留原始代码,基本上都是新建类、页面和js等进行扩展,不在原有代码上直接改,以下仅针对流控过程扩展供参考。

5.2.1. 新增Publisher和Provider

根据原始的GatewayFlowRuleController,可以知道,接口内使用的实体对象都是GatewayFlowRuleEntity,所以直接为它提供转换器并编写PublisherProvider即可。

Provider

import com.alibaba.csp.sentinel.dashboard.config.NacosConfigUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component("gatewayFlowRuleNacosProvider")
public class GatewayFlowRuleNacosProvider implements DynamicRuleProvider<List<GatewayFlowRuleEntity>> {
    @Autowired
    private ConfigService configService;
    // 规则对象的转换器,获取到的数据根据使用的数据类型的不同,需要用不同的转换器转化后使用
    @Autowired
    private Converter<String, List<GatewayFlowRuleEntity>> converter;

    @Override
    public List<GatewayFlowRuleEntity> getRules(String appName) throws Exception {
        String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
                NacosConfigUtil.GROUP_ID, 3000);
        if (StringUtil.isEmpty(rules)) {
            return new ArrayList<>();
        }
        return converter.convert(rules);
    }
}

Publisher

import com.alibaba.csp.sentinel.dashboard.config.NacosConfigUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * <h3>Sentinel动态数据源的网关流控规则发布器</h3>
 *
 * <p>把通过控制台配置好的规则发布到动态数据源(nacos)中,当前使用的是push模式,
 * 规则被推送至nacos后,网关将会通过监听器自动获取最新的规则,并更新到网关的本地缓
 * 存中。
 * </p>
 */
@Component("gatewayFlowRuleNacosPublisher")
public class GatewayFlowRuleNacosPublisher implements DynamicRulePublisher<List<GatewayFlowRuleEntity>> {
    // 数据源的配置服务
    @Autowired
    private ConfigService configService;
    /**
     * <p>数据通信的转换器。
     * <p>在config包下的NacosConfig类中声明的Spring Bean对象。
     * <p>负责将实体对象转换为json格式的字符串</p>
     */
    @Autowired
    private Converter<List<GatewayFlowRuleEntity>, String> converter;

    @Override
    public void publish(String app, List<GatewayFlowRuleEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");
        if (rules == null) {
            return;
        }
        /*
         * 将规则发布到动态数据源作持久化,第一个参数是app+后缀,此处用的是-flow-rules的后缀;
         * 第二个参数是nacos分组id,这个用默认提供的sentinel预留项即可;最后一个参数是数据转换
         * 器,要将对象转换成统一的格式后,网络传输到nacos。
         */
        configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
                NacosConfigUtil.GROUP_ID, converter.convert(rules));
    }
}

5.2.2. 添加Converter

添加一个Converter<List< GatewayFlowRuleEntity>, String>类型的转换器,可以直接在前面拷贝的NacosConfig中添加:

@Bean
public Converter<List<GatewayFlowRuleEntity>, String> gatewayFlowRuleEntityEncoder() {
	return JSON::toJSONString;
}

@Bean
public Converter<String, List<GatewayFlowRuleEntity>> gatewayFlowRuleEntityDecoder() {
	return s -> JSON.parseArray(s, GatewayFlowRuleEntity.class);
}

5.2.3. GatewayFlowRuleControllerV2

  1. 新增一个GatewayFlowRuleControllerV2,内容可以直接拷贝GatewayFlowRuleController

  2. 将原来的接口地址改一下:/gateway/flow -> /v2/gateway/flow

  3. 去掉原来的sentinelApiClient

    @Autowired
    private SentinelApiClient sentinelApiClient;
    
  4. 加入Provider和Publisher

    @Autowired
    @Qualifier("gatewayFlowRuleNacosProvider")
    private DynamicRuleProvider<List<GatewayFlowRuleEntity>> ruleProvider;
    @Autowired
    @Qualifier("gatewayFlowRuleNacosPublisher")
    private DynamicRulePublisher<List<GatewayFlowRuleEntity>> rulePublisher;
    
  5. 修改publishRules()方法

    private boolean publishRules(String app, String ip, Integer port) {
    	List<GatewayFlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
        return sentinelApiClient.modifyGatewayFlowRules(app, ip, port, rules);
    }
    // 改为
    
        /**
         * <h3>发布规则统一逻辑</h3>
         *
         * <p>规则都是存在本地内存中的,先从内存中获取所有当前要发布规则应用的规则,是一个List</p>
         * <p>将全量的规则以一定的格式发布到数据源中,进行统一更新</p>
         *
         * @param app 应用名称
         * @param ip 应用IP
         * @param port 应用端口
         * @throws Exception 远程发布,会发生异常,要进行异常处理
         */
        private void publishRules(String app, String ip, Integer port) throws Exception {
            List<GatewayFlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
            rulePublisher.publish(app, rules);
        }
    
  6. 修改controller中的逻辑,主要针对读取规则和保存规则两块:

    • queryFlowRules

      List<GatewayFlowRuleEntity> rules = sentinelApiClient.fetchGatewayFlowRules(app, ip, port).get();
      // 改为
      List<GatewayFlowRuleEntity> rules = ruleProvider.getRules(app);
      
    • addFlowRule

      try {
      	entity = repository.save(entity);
      } catch (Throwable throwable) {
      	logger.error("add gateway flow rule error:", throwable);
      	return Result.ofThrowable(-1, throwable);
      }
      
      if (!publishRules(app, ip, port)) {
      	logger.warn("publish gateway flow rules fail after add");
      }
      // 改为
      try {
          entity = repository.save(entity);
          publishRules(entity.getApp(), entity.getIp(), entity.getPort());
      } catch (Throwable throwable) {
          logger.error("add gateway flow rule error:", throwable);
          return Result.ofThrowable(-1, throwable);
      }
      
    • updateFlowRule(与addFlowRule类似)

    • deleteFlowRule(与addFlowRule类似)

5.2.4. 前端改造

  1. app.js中定义新的gateway state

    // 新增的state
    	.state('dashboard.gatewayFlowV2', {
              templateUrl: 'app/views/gateway/flow_v2.html',
              url: '/gateway/flow/:app',
              controller: 'GatewayFlowCtlV2',
              resolve: {
                  loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) {
                      return $ocLazyLoad.load({
                          name: 'sentinelDashboardApp',
                          files: [
                              'app/scripts/controllers/gateway/flow_v2.js',
                          ]
                      });
                  }]
              }
          })
    
    // 这是原来的,在最底部
          .state('dashboard.gatewayFlow', {
              templateUrl: 'app/views/gateway/flow.html',
              url: '/gateway/flow/:app',
              controller: 'GatewayFlowCtl',
              resolve: {
                  loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) {
                      return $ocLazyLoad.load({
                          name: 'sentinelDashboardApp',
                          files: [
                              'app/scripts/controllers/gateway/flow.js',
                          ]
                      });
                  }]
              }
          });
    
  2. 改造sidebar.html,添加新导航到新的controller,旧的注释掉

    <li ui-sref-active="active" ng-if="entry.isGateway">
    	<a ui-sref="dashboard.gatewayFlowV2({app: entry.app})">
    	<i class="glyphicon glyphicon-filter"></i>&nbsp;&nbsp;网关流控规则</a>
    </li>
    
  3. 新增一个网关流控页面,views/gateway/flow_v2.html

  4. 拷贝controllers/gateway/flow.js -> controllers/gateway/flow_v2.js,只改带V2的地方即可

    app.controller('GatewayFlowCtlV2', ['$scope', '$stateParams', 'GatewayFlowServiceV2', 'GatewayApiService', 'ngDialog', 'MachineService',
      function ($scope, $stateParams, GatewayFlowServiceV2, GatewayApiService, ngDialog, MachineService) {
    
  5. 拷贝services/gateway/flow_service_v1.js -> services/gateway/flow_service_v2.js

    app.service('FlowServiceV2', ['$http', function ($http) {
    

    FlowService改为 FlowServiceV2,然后把所有的请求路径加上 /v2 前缀

  6. gulpfile.js中添加FlowServiceV2的定义:

    const JS_APP = [
    	……,
    	'app/scripts/services/gateway/flow_service_v2.js',
    ]
    
  7. 执行npm run build,清空浏览器缓存后测试即可。

上一篇:服务网关知识


下一篇:springCloudGateway-使用记录