前言

我们的项目为了方便移植,所以选择了阿里云来进行部署,脱离的公司自己的技术能力平台。项目中使用sentinel做 限流,单原本的sentinel只有基于的内存存储的单机限流攻击,无法满足线上软件的要求。我们需要在sentinel的基础上,改造dashboard完成如下能力。

  1. 接入Sentinel-Dashboard提供更灵活的限流配置管理和更直观的查看系统资源的入口。
  2. 接入nacos 提供持久化的限流配置存储能力。
  3. 接入token-server,提供集群限流的能力。
  4. Sentinel-Dashboard 接入sqllite,持久化度量展示。

本文则会详细展开来讲解每一项的改动过程。

sentinel 初识

在Sentinel改造之前,我们来简单看一下sentinel官方提供了什么的基础能力。
sentinel官网

在sentinel中,限流的基本单位就是资源。用户可以自己将应用程序中的任何内容定义为一个资源,并围绕这个资源实时设定相关的限流规则。从而实现系统的限流保护,熔断降级等高可用的保障能力。下面是官方提供的一个简单例子。


private static void initFlowRules(){
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule();
    //指定资源
    rule.setResource("HelloWorld");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    //设置qps的限流值
    rule.setCount(20);
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}

public static void main(String[] args) {
    
    // 配置规则.
    initFlowRules();

    //定义Hello World资源
    try (Entry entry = SphU.entry("HelloWorld")) {
            // 被保护的逻辑
            System.out.println("hello world");
    } catch (BlockException ex) {
            // 处理被流控的逻辑
        System.out.println("blocked!");
    }
}

至此,我们就简单的实现了使用sentinel做一个单机限流的一个代码实例,只不过这个限流的规则是通过硬编码的方式存储在应用启动的内存当中的。当应用启动以后将无法实时的查看资源的使用情况和动态调整资源的限流规则。为此Sentinel提供了一个dashbaord 启动控制台,可以实时监控各个资源的运行情况,并且可以实时地修改限流规则。

接入dashboard

sentinel控制台官网

首先我们将sentinel-dashboard下载到本地。先在github上找到sentinel的源代码,地址:https://github.com/alibaba/Sentinel。并单独打开sentinel-dashboard工程,

image.png

可以看到sentinel-dashboard是一个包含了前端和后端资源的springBoot工程,我们直接运行DashboardApplication就可以直接启动sentinel的dashboard。

image.png

我们把使用到限流的业务工程称为sentienl客户端, sentinel-dashboard启动后,我们先把dashboard放在一边,来看一下sentinel客户端如何接入dashboard.

sentienl的客户端和dashboard之间通过 sentinel-transport-simple-http 模块通信,sentinel客户端每秒讲资源的实时情况通过http汇总到dashboard,并监听来自于dashboard的规则修改指令。因此客户端需要引入如下依赖。


<dependency>  
    <groupId>com.alibaba.csp</groupId>  
    <artifactId>sentinel-core</artifactId>  
    <version>1.8.6</version>  
</dependency>  
<dependency>  
    <groupId>com.alibaba.csp</groupId>  
    <artifactId>sentinel-transport-simple-http</artifactId>  
    <version>1.8.6</version>  
</dependency>

客户端启动时,需要额外增加Sentinel相关的参数

-Dproject.name=client-biz-test # 指定当前客户端的名称
-Dcsp.sentinel.dashboard.server=127.0.0.1:8080 # 指定远程的dashoard的地址。

sentinel的资源创建是一个懒加载的过程,在客户端启动后,一定要先触发一次请求。否则在dashboard讲查看不到任何的客户端信息。触发一次后,再打开dashboard地址,通过默认的账号和密码(sentinel:sentinel)就可以查看到当前资源的信息了。

image

但目前来说,所有的规则配置信息都是保存着客户端内存中,如果客户端重启,所有的配置信息将全部丢失。如果想要在线上生成环境更好的使用sentinel就需要进行深入的改造工程。

开始sentinel改造之旅

接入nacos

改造的第一站,就是要实现 规则配置的持久化:将规则存储在第三方的配置中心(比如nacos或zookeeper),我在项目中使用的是nacos,本文也讲着重讲解如何使用nacos来存储规则。

sentienl的原始模式与push模式

上面的通过dashboard将配置写入到客户端内存中的这种方式就是原始模式。
而我们的目标:从nacos中实时感知规则配置的变更的这种模式就是push模式。

image.png

image.png

在push模式中控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。

sentinel的客户端改造

参考官方网站的指引:动态规则扩展

首先我们先完成 sentinel的客户端改造。只需要两步:

首先引入依赖

<dependency>  
    <groupId>com.alibaba.csp</groupId>  
    <artifactId>sentinel-core</artifactId>  
    <version>1.8.6</version>  
</dependency>  
<dependency>  
    <groupId>com.alibaba.csp</groupId>  
    <artifactId>sentinel-datasource-nacos</artifactId>  
    <version>1.8.6</version>  
</dependency>  
<dependency>  
    <groupId>com.alibaba.csp</groupId>  
    <artifactId>sentinel-transport-simple-http</artifactId>  
    <version>1.8.6</version>  
</dependency>

然后注册nacos的数据源


@Component
@Slf4j
public class SentinelNacosConfig {

    @Resource
    private Environment environment;

    private static final String PROJECT_NAME = "project.name";

    @PostConstruct
    public void sentinelConfigChange(String configJson) {

        //todo 这里填入你自己的nacos信息
        String remoteAddress = '';
        String groupId = '';
        String namespace = '';

        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, remoteAddress);
        properties.put(PropertyKeyConst.NAMESPACE, namespace);

        //读取jvm参数中的 -Dproject.name 每一个工程生成一个dataId
        String projectName = MoreObjects.firstNonNull(environment.getProperty(PROJECT_NAME), "default");
        String dataId = projectName + "-flow-rules";
        //注册数据源头
        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(properties, groupId, dataId,
                source -> JsonUtils.readObject(source, new TypeReference<>() {
                }));
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }

客户端启动时,仍然需要额外增加Sentinel相关的参数再启动。

-Dproject.name=client-biz-test # 指定当前客户端的名称
-Dcsp.sentinel.dashboard.server=127.0.0.1:8080 # 指定远程的dashoard的地址。

sentinel的dashboard改造

dashbaord的改造就相对来说复杂一些。这里我会尽量讲清楚。

打开我们之前下载的dashbord源代码。sentinel团队其实已经给我们预留好了接入nacos的代码。dashboard中

  • 通过DynamicRuleProvider来从第三方读取规则信息,默认的实现是通过http从sentinel客户端中任意一台节点来读取规则信息。(具体实现类:flowRuleDefaultProvider)
  • 通过DynamicRulePublisher来将页面变更的规则信息推送到第三方中。默认的实现是通过http推送给所有的sentinel客户端节点。(具体实现类:flowRuleDefaultPublisher)

image.png

而我们要做的事情是

  1. pom中启用nacos模块
    image.png
    删除这个score
  2. 替换DynamicRuleProvider和DynamicRulePublisher的默认实现。只需要将源码中test目录下的nacos实现并覆盖rule工程下即可。

image.png

在NacosConfig中填入你自己的nacos的配置信息


@Bean  
public ConfigService nacosConfigService() throws Exception {  
    Properties properties = new Properties();  
    properties.put(PropertyKeyConst.SERVER_ADDR, "");  
    properties.put(PropertyKeyConst.NAMESPACE, "");  
    return ConfigFactory.createConfigService(properties);  
}
  1. 将之前所有使用flowRuleDefaultProvider和flowRuleDefaultPublisher的地方改成使用flowRuleNacosProvider和flowRuleNacosPublisher。

    其实只有FlowControllerV2中有使用。FlowController是流控规则Controller入口,Sentinel Dashboard的流控规则下的所有操作,都会调用Sentinel-Dashboard源码中的FlowController类,这个类中包含流控规则本地化的CRUD操作。

    此时你会发现代码中既有 FlowControllerV1类,又有FlowControllerV2类,这里其实是一个历史原因。

    一开始只有FlowController类,该类的代码和现在看到的FlowControllerV1是相同的:http从sentinel客户端中任意一台节点来读取规则信息。再将变更通过http推送给所有的sentinel客户端节点。从 Sentinel 1.4.0 开始,官方抽取出了接口用于向远程配置中心推送规则以及拉取规则。即DynamicRuleProvider和DynamicRulePublisher。

    但即使是最新的版本,前端默认的情况在仍然是请求 FlowControllerV1 的接口的(主要是为了历史兼容)。因此我们需要继续修改前端代码。

  2. 修改前端代码:Sentinel Dashboard前端sidebar.html页面入口。在目录resources/app/scripts/directives/sidebar找到sidebar.html,里面有关于V1版本的请求入口:

    <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>

 对应的JS 请求是在 app.js 文件下,搜索关键字 dashboard.flowV1 ,可以看到下方会有一个类似的但请求FlowControllerV2 的 js代码。因此我们只需要将sidebar.html中的关于v1的版本请求改掉即可。

image.png

dashboard改造完成,启动dashboard,尝试调整一下限流配置观察整改改造链路是否完成。在nacos页面中可以查看具体的配置项目是否已经创建成功。

image.png

更详细的内容,可以查看官方文档 Sentinel 控制台

集群流控

参考sentinel官方文档 集群流量控制

即使经过上面的改造我们仍然只是解决单机限流的配置持久化和及时的信息查询能力。所有的限流仍然是单节点有效。在实际的高并发场景下,由于业务集群需要能够动态的扩容缩容,所以我们无法通过使用 单机限流 * 节点数 来实现一个集群限流的能力。我们仍然是需要一个真正的集群限流能力。如果想实现集群流控,就避免不了需要有一个单点服务做统计。在Sentinel中,集群流控中共有两种身份:

  • Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
  • Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。

若Token Sever 宕机,则会使用Token Client的单机限流进行均摊。

Sentinel 集群限流服务端有两种启动方式:

  • 独立模式(Alone),即作为独立的 token server 进程启动,独立部署,隔离性好,但是需要额外的部署操作。独立模式适合作为 Global Rate Limiter 给集群提供流控服务。

image

  • 嵌入模式(Embedded),即作为内置的 token server 与服务在同一进程中启动。在此模式下,集群中各个实例都是对等的,token server 和 client 可以随时进行转变,因此无需单独部署,灵活性比较好。但是隔离性不佳,需要限制 token server 的总 QPS,防止影响应用本身。嵌入模式适合某个应用集群内部的流控。

image

在生产环境中,我们更希望是有一个单独的节点来作为的集群流控的token-server角色。因此我们需要使用独立模式。

指定Token Client角色

回到sentinel的客户端改造 这个章节,我们需要指定我们目前的Sentinel客户端为Token Client角色(我在实际改造过程中没有主动指定身份,结果整个集群怎么都不通,然后卡了大半天)

在 SentinelNacosConfig 中 增加一行代码

@Component
@Slf4j
public class SentinelNacosConfig {

    @Resource
    private Environment environment;

    private static final String PROJECT_NAME = "project.name";

    @NacosConfigListener(dataId = "sentinel_nacos_config")
    public void sentinelConfigChange(String configJson) {
        //全部省略

        // 指定当前身份为 Token Client
        ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);
    }
}

单独搭建 Token-Server 工程

初始化一个简单的springBoot工程。

  1. 引入Sentinel相关的依赖

    <dependency>  
     <groupId>com.alibaba.csp</groupId>  
     <artifactId>sentinel-datasource-nacos</artifactId>  
     <version>1.8.6</version>  
    </dependency>  
    <dependency>  
     <groupId>com.alibaba.csp</groupId>  
     <artifactId>sentinel-transport-simple-http</artifactId>  
     <version>1.8.6</version>  
    </dependency>
  2. 并添加如下文件用于配置nacos并启动token-server

@Slf4j  
@Component  
public class SentinelServer {  
  
    private String groupId = "SENTINEL_GROUP";  
  
    private String dataIdSuffix = " -flow-rules";  
  
    private static final String namespaceSetDataId = "token-server-namespace-set";  
  
    @PostConstruct  
    public void init() throws Exception {  
  
        Properties properties = new Properties();  
  
        //填入你的自己的nacos配置,  
        properties.put(PropertyKeyConst.SERVER_ADDR, "");  
        properties.put(PropertyKeyConst.NAMESPACE, "");  
  
        initPropertySupplier(properties);  
        initNamespaceSetProperty(properties);  
        runTokenServer();  
    }  
  
  
    private void initPropertySupplier(Properties properties) {  
        // 与sentinel client 共同读取同一套限流规则配置 ,因此groupId和dataId的生成规则要和 sentinel-client 以及 sentinel-dashboard保持一致  
        ClusterFlowRuleManager.setPropertySupplier(namespace -> {  
            ReadableDataSource<String, List<FlowRule>> ds = new NacosDataSource<>(properties, groupId,  
                    namespace + dataIdSuffix,  
                    source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {  
                    }));  
            return ds.getProperty();  
        });  
    }  
  
    // 配置命名空间  
    // 集群限流服务端服务的作用域(命名空间),可以设置为自己服务的应用名。  
    // 集群限流 client 在连接到 token server 后会上报自己的应用名(默认为 project.name 配置的应用名),token server 会根据上报的应用名来统计连接数。  
    private void initNamespaceSetProperty(Properties properties) {  
        // Server namespace set (scope) data source.  
        ReadableDataSource<String, Set<String>> namespaceDs = new NacosDataSource<>(properties, groupId,  
                namespaceSetDataId, source -> JSON.parseObject(source, new TypeReference<Set<String>>() {  
        }));  
        ClusterServerConfigManager.registerNamespaceSetProperty(namespaceDs.getProperty());  
    }  
  
    //启动token-server  
    private void runTokenServer() throws Exception {  
          
        ClusterTokenServer tokenServer = new SentinelDefaultTokenServer();  
          
        ServerTransportConfig serverTransportConfig = new ServerTransportConfig();  
        serverTransportConfig.setPort(11111);  
        ClusterServerConfigManager.loadGlobalTransportConfig(serverTransportConfig);  
        // Start the server.  
        tokenServer.start();  
  
    }  
  
}
  1. 启动部署Token-server
    -Dproject.name=client-biz-test # 指定当前客户端的名称
    -Dcsp.sentinel.dashboard.server=127.0.0.1:8080 # 指定远程的dashoard的地址。
  2. 分配client
    正常启动以后,需要在dashboard中分配client和token-server之间的关系。dashboar集群限流选择项中可以调整,这里不再赘述。

dashboard 监控数据持久化

Dashboard 中MetricsRepository 是用来存储和查询所有的监控入口的入口,默认的实现方式是InMemoryMetricsRepository,dashboard从client收集上来的监控数据只会在内存中存储5分钟的时间。有时候我们查询历史的qps情况就需要我们自己来实现一个中MetricsRepository。

对于日志类的数据,一般来说InfluxDB这种时序数据库是再合适不过的。使用InfluxDB那么就需要额外的启动一个InfluxDB的进程。但我这里希望dashboard能足够的精简独立,而且经过上面的改造dashboard完全可以单节点部署(dashbaord挂了也没关系,所有的规则配置都在nacos中),所以我选择了嵌入式sqllite来作为持久化的存储,直接将数据存储到

如何在springBoot中引入sqllite的依赖,我这里就不赘述了,我们完全可以让chatgpt帮我们写一个具体的例子来。

但有一点需要考虑的是,为了防止sqlite占用的空间无限增大,我们需要定时来清理7天之前的数据。


@Component
@Slf4j
public class SqlLiteMetricsRepository implements MetricsRepository<MetricEntity> {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void initDb() {

        String createTableSql = "CREATE TABLE IF NOT EXISTS MetricEntity (\n" +
                "    id INTEGER,\n" +
                "    gmtCreate TIMESTAMP,\n" +
                "    gmtModified TIMESTAMP,\n" +
                "    app TEXT,\n" +
                "    timestamp TIMESTAMP,\n" +
                "    resource TEXT,\n" +
                "    passQps INTEGER,\n" +
                "    successQps INTEGER,\n" +
                "    blockQps INTEGER,\n" +
                "    exceptionQps INTEGER,\n" +
                "    rt REAL,\n" +
                "    count INTEGER,\n" +
                "    resourceCode INTEGER\n" +
                ");\n" +
                "CREATE INDEX IF NOT EXISTS idx_app_resource_timestamp ON MetricEntity (app, resource, timestamp); ";
        jdbcTemplate.execute(createTableSql);
    }

    @Override
    public void save(MetricEntity metricEntity) {
        String sql = "INSERT INTO MetricEntity (id, gmtCreate, gmtModified, app, timestamp, resource, passQps, successQps, blockQps, exceptionQps, rt, count, resourceCode) " +
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
        //此处省略操作sqllite相关的代码
    }

    @Override
    public void saveAll(Iterable<MetricEntity> metrics) {
        String sql = "INSERT INTO MetricEntity (id, gmtCreate, gmtModified, app, timestamp, resource, passQps, successQps, blockQps, exceptionQps, rt, count, resourceCode) " +
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
        //此处省略操作sqllite相关的代码
    }


    @Override
    public List<MetricEntity> queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime) {
        String sql = "SELECT * FROM MetricEntity WHERE app = ? AND resource = ? AND `timestamp` BETWEEN ? AND ?";
        //此处省略操作sqllite相关的代码
        return metricEntityList;
    }

    @Override
    public List<String> listResourcesOfApp(String app) {
        String sql = "SELECT * FROM MetricEntity WHERE app = ?  ";
        //此处省略操作sqllite相关的代码
    }

    //滚动删除7天前的数据
    @Scheduled(fixedRate = 86400000) // Every 24 hours
    public void cleanupOldData() {
        Instant cutoff = Instant.now().minusSeconds(7 * 24 * 60 * 60); // 7 days ago
        String sql = "DELETE FROM MetricEntity WHERE timestamp < ?";
        jdbcTemplate.update(sql, cutoff);
    }

}

然后将所有使用到MetricsRepository的地方,指明装配sqlLiteMetricsRepository。
总共有两个文件用到了 MetricsRepository

  • MetricController:提供前端查询用的接口
  • MetricFetcher:获取Sentinel-client信息的入口。

改造前端
我们需要给前端页面增加 开始时间和结束时间的查询范围入参。让前端页面能够选择查询的范围。

相关的代码文件在metric.html和metric.js中。可能大部分的后端开发对前端的相关知识并不是很清楚,改起来比较费劲,我们可以着前端同事帮忙修改前端页面,增加这个逻辑。也可以像我一样从github中找一份以及改造过的代码,然后将对应的metric.html、metric.js文件直接覆盖过去。再进行调试,通过调试反馈的问题,不断的复制copy相关的代码。参考的github :https://github.com/shiyindaxiaojie/Sentinel


jian
282 声望210 粉丝