zoukankan      html  css  js  c++  java
  • sentinel控制台监控数据持久化【MySQL】

    根据官方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) :

    可以看到,MetricsRepository接口的

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

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

    -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

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

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

    -- 创建监控数据表
    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;

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

    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省略
    }

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

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

    # 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

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

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

    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());
        }
    }

    参考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-控制台

  • 相关阅读:
    HashMap(HashSet)的实现
    C/C++ 安全编码 —— 指针与内存
    AlexNet神经网络结构
    查看,修改动态组成员/通讯组
    刷新已禁用用户邮箱状态
    监控DAG状态
    AD诊断命令
    PowerShell管理Exchange
    TCP连接(Time_Wait、Close_Wait)说明
    IIS配置(安装IIS、.Net、更改IIS Log目录位置)
  • 原文地址:https://www.cnblogs.com/cdfive2018/p/9838577.html
Copyright © 2011-2022 走看看