• -------------------------------------------------------------
  • ====================================

sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)

技能 dewbay 5年前 (2019-05-08) 3221次浏览 已收录 0个评论 扫描二维码

根据官方 wiki 文档,sentinel控制台的实时监控数据,默认仅存储 5 分钟以内的数据。如需持久化,需要定制实现相关接口。

https://github.com/alibaba/Sentinel/wiki/在生产环境中使用-Sentinel-控制台 也给出了指导步骤:

1.自行扩展实现 MetricsRepository 接口;

2.注册成 Spring Bean 并在相应位置通过 @Qualifier 注解指定对应的 bean name 即可。

本文先学习官方提供的接口梳理思路,然后使用Spring Data JPA编写一个MySQL存储实现。

—————————————————————————————————————————————————————————————–

首先看接口定义:

repository.metric 包下的 MetricsRepository<T>接口

该接口定义了 4 个方法,分别用于保存和查询 sentinel 的 metric 数据。注释其实很清楚了,这里简单过一下:

save:保存单个 metric

saveAll:保存多个 metric

queryByAppAndResourceBetween:通过应用名名称、资源名称、开始时间、结束时间查询 metric 列表

listResourcesOfApp:通过应用名称查询资源列表

注:发现跟接口定义跟Spring Data JPA用法很像,即某个实体类 Xxx 对应一个 XxxRepository,方法的命令也很规范,save、queryBy…

结合控制台【实时监控】菜单的界面,大概能猜到列表页面的查询流程:

菜单属于某一个应用,这里应用名称是 sentinel-dashborad;

先通过应用名称查询应用下所有的资源,图中看到有 2 个,资源名称分别是/resource/machineResource.json、/flow/rules.json;// listResourcesOfApp 方法

再通过应用名称、资源名称、时间等查询 metric 列表用于呈现统计图表;// queryByAppAndResourceBetween 方法

在 MetricsRepository 类名上 Ctrl+H 查看类继承关系(Type Hiberarchy):

默认提供了一个用内存存储的实现类:InMemoryMetricsRepository

在 MetricsRepository 类的各个方法上,通过 Ctrl+Alt+H 查看方法调用关系(Call Hierarchy) :

sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)
sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)
sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)

可以看到,MetricsRepository 接口的

save 方法被它的实现类 InMemoryMetricsRepository 的 saveAll 调用,再往上走被 MetricFetcher 调用,用于保存 metric 数据;

queryByAppAndResourceBetween、listResourcesOfApp 被 MetricController 调用,用于查询 metric 数据;

—————————————————————————————————————————————————————————————–

OK,以上初步梳理了 MetricsRepository 接口的方法和流程,接下来我们使用MySQL数据库,实现一个 MetricsRepository 接口。

首先,参考 MetricEntity 类设计一张表 sentinel_metric 来存储监控的 metric 数据,表 ddl 如下:

sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)
-- 创建监控数据表
CREATE TABLE `sentinel_metric1` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT 'id,主键',
  `gmt_create` DATETIME COMMENT '创建时间',
  `gmt_modified` DATETIME COMMENT '修改时间',
  `app` VARCHAR(100) COMMENT '应用名称',
  `timestamp` DATETIME COMMENT '统计时间',
  `resource` VARCHAR(500) COMMENT '资源名称',
  `pass_qps` INT COMMENT '通过 qps',
  `success_qps` INT COMMENT '成功 qps',
  `block_qps` INT COMMENT '限流 qps',
  `exception_qps` INT COMMENT '发送异常的次数',
  `rt` DOUBLE COMMENT '所有 successQps 的 rt 的和',
  `_count` INT COMMENT '本次聚合的总条数',
  `resource_code` INT COMMENT '资源的 hashCode',
  INDEX app_idx(`app`) USING BTREE,
  INDEX resource_idx(`resource`) USING BTREE,
  INDEX timestamp_idx(`timestamp`) USING BTREE,
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)

注:app、resource、timestamp 在查询语句的 where 条件中用到,因此给它们建立索引提高查询速度;

     count 是MySQL的关键字,因此加上 _ 前缀。

持久层选用Spring Data JPA框架,在 pom 中引入 starter 依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
  <version>${spring.boot.version}</version>
</dependency>

在 datasource.entity 包下,新建 jpa 包,下面新建 sentinel_metric 表对应的实体类 MetricPO:

sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)
package com.taobao.csp.sentinel.dashboard.datasource.entity.jpa;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

/**
 * @author cdfive
 * @date 2018-09-14
 */
@Entity
@Table(name = "sentinel_metric")
public class MetricPO implements Serializable {

    private static final long serialVersionUID = 7200023615444172715L;

    /**id,主键*/
    @Id
    @GeneratedValue
    @Column(name = "id")
    private Long id;

    /**创建时间*/
    @Column(name = "gmt_create")
    private Date gmtCreate;

    /**修改时间*/
    @Column(name = "gmt_modified")
    private Date gmtModified;

    /**应用名称*/
    @Column(name = "app")
    private String app;

    /**统计时间*/
    @Column(name = "timestamp")
    private Date timestamp;

    /**资源名称*/
    @Column(name = "resource")
    private String resource;

    /**通过 qps*/
    @Column(name = "pass_qps")
    private Long passQps;

    /**成功 qps*/
    @Column(name = "success_qps")
    private Long successQps;

    /**限流 qps*/
    @Column(name = "block_qps")
    private Long blockQps;

    /**发送异常的次数*/
    @Column(name = "exception_qps")
    private Long exceptionQps;

    /**所有 successQps 的 rt 的和*/
    @Column(name = "rt")
    private Double rt;

    /**本次聚合的总条数*/
    @Column(name = "_count")
    private Integer count;

    /**资源的 hashCode*/
    @Column(name = "resource_code")
    private Integer resourceCode;

    // getter setter 省略
}
sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)

该类也是参考 MetricEntity 创建,加上了JPA的注解,比如@Table 指定表名,@Entity 标识为实体,@Id、@GeneratedValue 设置 id 字段为自增主键等;

在 resources 目录下的 application.properties 文件中,增加数据源和JPA(hibernate)的配置:

sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)
# datasource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=${spring.datasource.url}
spring.datasource.username=${spring.datasource.username}
spring.datasource.password=${spring.datasource.password}

# spring data jpa
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.show-sql=false
sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)

这里数据库连接(url)、用户名(username)、密码(password)用${xxx}占位符,这样可以通过 maven 的 pom.xml 添加 profile 配置不同环境(开发、测试、生产) 或 从配置中心读取参数。

接着在 InMemoryMetricsRepository 所在的 repository.metric 包下新建 JpaMetricsRepository 类,实现 MetricsRepository<MetricEntity>接口:

sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)
package com.taobao.csp.sentinel.dashboard.repository.metric;

import com.alibaba.csp.sentinel.util.StringUtil;
import com.taobao.csp.sentinel.dashboard.datasource.entity.MetricEntity;
import com.taobao.csp.sentinel.dashboard.datasource.entity.jpa.MetricPO;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author cdfive
 * @date 2018-09-17
 */
@Transactional
@Repository("jpaMetricsRepository")
public class JpaMetricsRepository implements MetricsRepository<MetricEntity> {

    @PersistenceContext
    private EntityManager em;

    @Override
    public void save(MetricEntity metric) {
        if (metric == null || StringUtil.isBlank(metric.getApp())) {
            return;
        }

        MetricPO metricPO = new MetricPO();
        BeanUtils.copyProperties(metric, metricPO);
        em.persist(metricPO);
    }

    @Override
    public void saveAll(Iterable<MetricEntity> metrics) {
        if (metrics == null) {
            return;
        }

        metrics.forEach(this::save);
    }

    @Override
    public List<MetricEntity> queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime) {
        List<MetricEntity> results = new ArrayList<MetricEntity>();
        if (StringUtil.isBlank(app)) {
            return results;
        }

        if (StringUtil.isBlank(resource)) {
            return results;
        }

        StringBuilder hql = new StringBuilder();
        hql.append("FROM MetricPO");
        hql.append(" WHERE app=:app");
        hql.append(" AND resource=:resource");
        hql.append(" AND timestamp>=:startTime");
        hql.append(" AND timestamp<=:endTime");

        Query query = em.createQuery(hql.toString());
        query.setParameter("app", app);
        query.setParameter("resource", resource);
        query.setParameter("startTime", Date.from(Instant.ofEpochMilli(startTime)));
        query.setParameter("endTime", Date.from(Instant.ofEpochMilli(endTime)));

        List<MetricPO> metricPOs = query.getResultList();
        if (CollectionUtils.isEmpty(metricPOs)) {
            return results;
        }

        for (MetricPO metricPO : metricPOs) {
            MetricEntity metricEntity = new MetricEntity();
            BeanUtils.copyProperties(metricPO, metricEntity);
            results.add(metricEntity);
        }

        return results;
    }

    @Override
    public List<String> listResourcesOfApp(String app) {
        List<String> results = new ArrayList<>();
        if (StringUtil.isBlank(app)) {
            return results;
        }

        StringBuilder hql = new StringBuilder();
        hql.append("FROM MetricPO");
        hql.append(" WHERE app=:app");
        hql.append(" AND timestamp>=:startTime");

        long startTime = System.currentTimeMillis() - 1000 * 60;
        Query query = em.createQuery(hql.toString());
        query.setParameter("app", app);
        query.setParameter("startTime", Date.from(Instant.ofEpochMilli(startTime)));

        List<MetricPO> metricPOs = query.getResultList();
        if (CollectionUtils.isEmpty(metricPOs)) {
            return results;
        }

        List<MetricEntity> metricEntities = new ArrayList<MetricEntity>();
        for (MetricPO metricPO : metricPOs) {
            MetricEntity metricEntity = new MetricEntity();
            BeanUtils.copyProperties(metricPO, metricEntity);
            metricEntities.add(metricEntity);
        }

        Map<String, MetricEntity> resourceCount = new HashMap<>(32);

        for (MetricEntity metricEntity : metricEntities) {
            String resource = metricEntity.getResource();
            if (resourceCount.containsKey(resource)) {
                MetricEntity oldEntity = resourceCount.get(resource);
                oldEntity.addPassQps(metricEntity.getPassQps());
                oldEntity.addRtAndSuccessQps(metricEntity.getRt(), metricEntity.getSuccessQps());
                oldEntity.addBlockQps(metricEntity.getBlockQps());
                oldEntity.addExceptionQps(metricEntity.getExceptionQps());
                oldEntity.addCount(1);
            } else {
                resourceCount.put(resource, MetricEntity.copyOf(metricEntity));
            }
        }

        // Order by last minute b_qps DESC.
        return resourceCount.entrySet()
                .stream()
                .sorted((o1, o2) -> {
                    MetricEntity e1 = o1.getValue();
                    MetricEntity e2 = o2.getValue();
                    int t = e2.getBlockQps().compareTo(e1.getBlockQps());
                    if (t != 0) {
                        return t;
                    }
                    return e2.getPassQps().compareTo(e1.getPassQps());
                })
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());
    }
}
sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)

参考 InMemoryMetricsRepository 类来实现,将其中用 map 存储和查询的部分改为用JPA实现:

save 方法,将 MetricEntity 转换为 MetricPO 类,调用 EntityManager 类的 persist 方法即可;

saveAll 方法,循环调用 save;

queryByAppAndResourceBetween、listResourcesOfApp 编写查询即可。

最后一步,在 MetricController、MetricFetcher 两个类,找到 metricStore 属性,在@Autowired 注解上面加上@Qualifier(“jpaMetricsRepository”)注解:

@Qualifier("jpaMetricsRepository")
@Autowired
private MetricsRepository<MetricEntity> metricStore;

至此,监控数据MySQL持久化就完成了,得益于 sentinel 良好的 Repository 接口设计,是不是很简单:)

来验证下成果:

设置 sentinel-dashboard 工程启动参数:-Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard

启动工程,打开 http://localhost:8080,查看不同的页面均显示正常,执行 sql 查询 sentinel_metric 表已有数据。

—————————————————————————————————————————————————————————————–

总结:

个人感觉 sentinel控制台默认的实现类 InMemoryMetricsRepository 挺赞的,虽然内存存储重启会清空数据,如果没有对历史数据查询的需求应用于生产环境是没问题的,

其中如何用内存存储,包括保存、查询以及排序等代码都值得学习;

对于监控数据,可能用MySQL关系数据库存储不太合适,虽然MySQL也可以通过事件或者任务定期清理;

数据定期清理、历史归档的需求,用时序数据库比如 InfluxDB 可能更适合。

—————————————————————————————————————————————————————————————–

参考:

https://github.com/alibaba/Sentinel/wiki/控制台

https://github.com/alibaba/Sentinel/wiki/在生产环境中使用-Sentinel-控制台


露水湾 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:sentinel控制台监控数据持久化【MySQL】(Spring Data JPA)
喜欢 (0)
[]
分享 (0)
关于作者:
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址