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

  • 相关阅读:
    jquery实现选项卡(两句即可实现)
    常用特效积累
    jquery学习笔记
    idong常用js总结
    织梦添加幻灯片的方法
    LeetCode "Copy List with Random Pointer"
    LeetCode "Remove Nth Node From End of List"
    LeetCode "Sqrt(x)"
    LeetCode "Construct Binary Tree from Inorder and Postorder Traversal"
    LeetCode "Construct Binary Tree from Preorder and Inorder Traversal"
  • 原文地址:https://www.cnblogs.com/cdfive2018/p/9838577.html
Copyright © 2011-2022 走看看