Spring Cloud Alibaba入门之Sentinel Dashboard实时监控数据的持久化

Sentinel Dashboard实时监控数据的持久化

1、实时监控-简介

  请参考《秒级监控日志》《实时监控》官方文档。

2、监控数据持久化思路

  请参考《sentinel控制台监控数据持久化【MySQL】》

  在Sentinel Dashboard中,实时数据的接口在MetricController类中定义,在该类中又通过注入MetricsRepository< MetricEntity>实现类(只提供了基于内存存储的实现类InMemoryMetricsRepository)来保存实时的监控数据,我们把基于内存的存储换成基于MySQL的存储是不是就可以实现了实时数据的持久化呢?答案是肯定的。我们这里选择使用基于Mybatis的数据持久化,我们根据InMemoryMetricsRepository实现类改造基于MySql的存储即可。

3、创建对应的表结构

  首先,需要根据com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity类设计一张表sentinel_metric来存储监控的metric数据。在MetricEntity类中,除了定义了需要的属性字段外,还提供了一些addXXX()方法,方便进行属性的累加运行等。如下所示:

public class MetricEntity {
    private Long id;
    private Date gmtCreate;
    private Date gmtModified;
    private String app;
    /**
     * 监控信息的时间戳
     */
    private Date timestamp;
    private String resource;
    private Long passQps;
    private Long successQps;
    private Long blockQps;
    private Long exceptionQps;

    /**
     * summary rt of all success exit qps.
     */
    private double rt;

    /**
     * 本次聚合的总条数
     */
    private int count;

    private int resourceCode;

	//省略了其他方法
}

  根据实体类,设计SQL如下:

CREATE TABLE `sentinel_metric` (
`id`  int(11) NOT NULL AUTO_INCREMENT COMMENT 'id,主键' ,
`gmt_create`  datetime NULL DEFAULT NULL COMMENT '创建时间' ,
`gmt_modified`  datetime NULL DEFAULT NULL COMMENT '修改时间' ,
`app`  varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '应用名称' ,
`timestamp`  datetime NULL DEFAULT NULL COMMENT '统计时间' ,
`resource`  varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源名称' ,
`pass_qps`  int(11) NULL DEFAULT NULL COMMENT '通过qps' ,
`success_qps`  int(11) NULL DEFAULT NULL COMMENT '成功qps' ,
`block_qps`  int(11) NULL DEFAULT NULL COMMENT '限流qps' ,
`exception_qps`  int(11) NULL DEFAULT NULL COMMENT '发送异常的次数' ,
`rt`  double NULL DEFAULT NULL COMMENT '所有successQps的rt的和' ,
`_count`  int(11) NULL DEFAULT NULL COMMENT '本次聚合的总条数' ,
`resource_code`  int(11) NULL DEFAULT NULL COMMENT '资源的hashCode' ,
PRIMARY KEY (`id`),
INDEX `app_idx` (`app`) USING BTREE ,
INDEX `resource_idx` (`resource`) USING BTREE ,
INDEX `timestamp_idx` (`timestamp`) USING BTREE 
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
AUTO_INCREMENT=157
ROW_FORMAT=DYNAMIC
;

4、修改pom文件

  首先,需要修改pom文件,引入需要的依赖,如下所示:

<!-- 添加基于MySQL数据库持久化 start -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>6.0.6</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.1</version>
</dependency>
<!-- 添加基于MySQL数据库持久化 end-->

5、修改application.properties,添加DataSource配置

#datasource 配置
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/sentinel?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username = root
spring.datasource.password = 123456

6、定义MetricEntityMapper接口

  我们采用了基于Mybatis的数据持久化,所以首先需要实现一个操作sentinel_metric数据表的Mapper类。具体实现如下:

@Component
@Mapper
public interface MetricEntityMapper {

    @InsertProvider(type = Provider.class,method = "save")
    public void save(@Param("metric") MetricEntity metric);

    @InsertProvider(type = Provider.class,method = "batchSave")

    public void saveAll(List<MetricEntity> list) ;

    @SelectProvider(type = Provider.class, method = "queryList")
    public List<MetricEntity> queryList(@Param("app")String app, @Param("resource")String resource, @Param("startTime")long startTime, @Param("endTime")long endTime);



    class Provider{
        //新增
        public String save(MetricEntity metric) throws IllegalAccessException {
            SQL sql = new SQL(){
   
   {
                INSERT_INTO("sentinel_metric");
                Field[] fields = metric.getClass().getDeclaredFields();
                for(int i=0;i<fields.length;i++){
                    String cName = fields[i].getName();
                    if(fields[i].get(metric) != null){
                        VALUES(cName,"#{" + cName + "}");
                    }
                }
            }};
            return sql.toString();
        }
        //批量新增
        public String batchSave(Map param){
            List<MetricEntity> list = (List<MetricEntity>) param.get("list");
            StringBuilder sb = new StringBuilder();
            StringBuilder keyFields = new StringBuilder();
            StringBuilder valueFields = new StringBuilder();
            if(list != null && list.size() > 0){//解析实体类属性
                MetricEntity metric = list.get(0);
                Field[] fields = metric.getClass().getDeclaredFields();
                for(int i=0;i<fields.length;i++){
                    keyFields.append(StringUtils.formatJavaFieldToDb(fields[i].getName()));
                    valueFields.append("#'{'list[{0}]." + fields[i].getName() + "}");
                    if(i < fields.length - 1){//非最后一个
                        keyFields.append(",");
                        valueFields.append(",");
                    }
                }
            }

            sb.append("INSERT INTO sentinel_metric (" +
                    keyFields.toString() +
                    ") VALUES ");
            MessageFormat mf = new MessageFormat(
                    "(" +
                            valueFields.toString() +
                            ")"
            );
            for (int i = 0; i < list.size(); i++) {
                sb.append(mf.format(new Object[] {i}));
                if (i < list.size() - 1)
                    sb.append(",");
            }
            return sb.toString();
        }
        //查询1
        public String queryList(Map map){
            String app = (String) map.get("app");
            String resource = (String) map.get("resource");
            long startTime = (long) map.get("startTime");
            long endTime = (long) map.get("endTime");

            StringBuffer sql = new StringBuffer("select " +
                    "id," +
                    "gmt_create AS gmtCreate," +
                    "gmt_modified AS gmtModified," +
                    "app," +
                    "timestamp," +
                    "resource," +
                    "pass_qps AS passQps," +
                    "success_qps AS successQps," +
                    "block_qps AS blockQps," +
                    "exception_qps AS exceptionQps," +
                    "rt," +
                    "count," +
                    "resource_code AS resourceCode" +
                    " from sentinel_metric where 1=1 ");

            if(!StringUtil.isEmpty(app)){
                sql.append(" AND ").append(" app = '" + app + "' ");
            }
            if(!StringUtil.isEmpty(resource)){
                sql.append(" AND ").append(" resource = '" + resource + "' ");
            }
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Calendar cal = Calendar.getInstance();
            if(endTime > 0){
                cal.setTimeInMillis(endTime);
                sql.append(" AND ").append(" timestamp <= '" +sdf.format(cal.getTime()) + "' ");
            }
            if(startTime > 0){
                cal.setTimeInMillis(startTime);
                sql.append(" AND ").append(" timestamp >= '" + sdf.format(cal.getTime()) + "' ");
            }
            sql.append(" ORDER BY timestamp DESC");
            return sql.toString();
        }

    }
}

7、实现JdbcDaoMetricsRepository类

  参考InMemoryMetricsRepository类,实现自定义的JdbcDaoMetricsRepository类,其中需要注入前面实现的MetricEntityMapper的类(实际注入的是Mybatis生成的代理对象),并通过MetricEntityMapper来操作数据库,进行数据的添加和查询等。具体实现如下:

@Component
public class JdbcDaoMetricsRepository implements MetricsRepository<MetricEntity> {

    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    @Autowired
    private MetricEntityMapper metricEntityMapper;

    @Override
    public void save(MetricEntity metric){
        if (metric == null || StringUtil.isBlank(metric.getApp())) {
            return;
        }
        readWriteLock.writeLock().lock();
        try{
            metricEntityMapper.save(metric);
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }

    @Override
    public void saveAll(Iterable<MetricEntity> metrics){
        if (metrics == null) {
            return;
        }
        readWriteLock.writeLock().lock();
        try {
           // metrics.forEach(this::save);
            List<MetricEntity> list = new ArrayList<>();
            metrics.forEach(single ->list.add(single));
            metricEntityMapper.saveAll(list);
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    @Override
    public List<MetricEntity> queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime){
        List<MetricEntity> results = new ArrayList<>();
        if (StringUtil.isBlank(app) || StringUtil.isBlank(resource)) {
            return results;
        }
        readWriteLock.readLock().lock();
        try{
            List<MetricEntity> list = metricEntityMapper.queryList(app,resource,startTime,endTime);
            return list;
        }finally {
            readWriteLock.readLock().unlock();
        }
    }

    @Override
    public List<String> listResourcesOfApp(String app){
        List<String> results = new ArrayList<>();
        if (StringUtil.isBlank(app)) {
            return results;
        }
        readWriteLock.readLock().lock();
        try{
            //final long minTimeMs = System.currentTimeMillis() - 1000 * 60;
            Map<String, MetricEntity> resourceCount = new ConcurrentHashMap<>(32);
            List<MetricEntity> list = metricEntityMapper.queryList(app,null,-1,-1);
            for(MetricEntity metric : list){
                if(resourceCount.containsKey(metric.getResource())){
                    MetricEntity oldEntity = resourceCount.get(metric.getResource());
                    oldEntity.addPassQps(metric.getPassQps());
                    oldEntity.addRtAndSuccessQps(metric.getRt(), metric.getSuccessQps());
                    oldEntity.addBlockQps(metric.getBlockQps());
                    oldEntity.addExceptionQps(metric.getExceptionQps());
                    oldEntity.addCount(1);
                }else{
                    resourceCount.put(metric.getResource(), MetricEntity.copyOf(metric));
                }
            }
            // 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(Entry::getKey)
                    .collect(Collectors.toList());
        }finally {
            readWriteLock.readLock().unlock();
        }
    }

}

  在原来的InMemoryMetricsRepository实现中,listResourcesOfApp()方法,默认查询了最近五分钟有访问数据的资源,这里为了测试方便,取消掉了这个时间限制。

8、修改MetricFetcher、MetricController两个类的MetricsRepository注入

  原来默认注入的是InMemoryMetricsRepository对象,现在需要添加注解@Qualifier,实现jdbcDaoMetricsRepository的注入,具体如下:

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

只有一个MetricsRepository实现类,自动注入,添加JdbcDaoMetricsRepository后,不使用@Qualifier表示,会报错

9、修改MetricController具体方法

  修改queryTopResourceMetric方法,使得可以查询历史监控数据。这里主要修改了对searchKey参数的使用,当参数以#开始和结尾时,我们默认把其中的部分作为时间参数处理,并查询从这个时间开始五分钟内的监控数据。

  该方法主要是前端页面用来获取实时数据的API。主要逻辑如下:首先进行参数处理,然后通过metricStore的listResourcesOfApp()方法查询到所有需要展示的资源列表(默认显示全部),如果资源列表为空,直接返回NULL,否则进行处理;首先根据查询的资源数量和分页数据,计算需要监控的资源列表(可以直接放到数据库查询中进行),最后在把需要监控的资源列表,遍历,分别查询出指定时间区间内的实时监控数据并构造返回数据,返回给前端进行展示。

@ResponseBody
@RequestMapping("/queryTopResourceMetric.json")
public Result<?> queryTopResourceMetric(final String app,
                                        Integer pageIndex,
                                        Integer pageSize,
                                        Boolean desc,
                                        Long startTime, Long endTime, String searchKey) throws ParseException {
    if (StringUtil.isEmpty(app)) {
        return Result.ofFail(-1, "app can't be null or empty");
    }
    if (pageIndex == null || pageIndex <= 0) {
        pageIndex = 1;
    }
    if (pageSize == null) {
        pageSize = 6;
    }
    if (pageSize >= 20) {
        pageSize = 20;
    }
    if (desc == null) {
        desc = true;
    }
    if (endTime == null) {
        endTime = System.currentTimeMillis();
    }
     if (startTime == null) {
        if (StringUtil.isNotEmpty(searchKey) && searchKey.length() > 2 && searchKey.startsWith("#") && searchKey.endsWith("#")) {//增加查询历史数据的入口
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateStr = searchKey.substring(1,searchKey.length()-2);
            Date date = sdf.parse(dateStr);
            startTime = date.getTime();
            endTime = startTime + 1000 * 60 * 5;
        }else{
            startTime = endTime - 1000 * 60 * 5;
        }
    }
    if (endTime - startTime > maxQueryIntervalMs) {
        return Result.ofFail(-1, "time intervalMs is too big, must <= 1h");
    }
    List<String> resources = metricStore.listResourcesOfApp(app);
    logger.debug("queryTopResourceMetric(), resources.size()={}", resources.size());

    if (resources == null || resources.isEmpty()) {
        return Result.ofSuccess(null);
    }
    if (!desc) {
        Collections.reverse(resources);
    }
    if (StringUtil.isNotEmpty(searchKey) && !searchKey.startsWith("#") && !searchKey.endsWith("#")) {
        List<String> searched = new ArrayList<>();
        for (String resource : resources) {
            if (resource.contains(searchKey)) {
                searched.add(resource);
            }
        }
        resources = searched;
    }
    int totalPage = (resources.size() + pageSize - 1) / pageSize;
    List<String> topResource = new ArrayList<>();
    if (pageIndex <= totalPage) {
        topResource = resources.subList((pageIndex - 1) * pageSize,
            Math.min(pageIndex * pageSize, resources.size()));
    }
    final Map<String, Iterable<MetricVo>> map = new ConcurrentHashMap<>();
    logger.debug("topResource={}", topResource);
    long time = System.currentTimeMillis();
    for (final String resource : topResource) {
        List<MetricEntity> entities = metricStore.queryByAppAndResourceBetween(
            app, resource, startTime, endTime);
        logger.debug("resource={}, entities.size()={}", resource, entities == null ? "null" : entities.size());
        List<MetricVo> vos = MetricVo.fromMetricEntities(entities, resource);
        Iterable<MetricVo> vosSorted = sortMetricVoAndDistinct(vos);
        map.put(resource, vosSorted);
    }
    logger.debug("queryTopResourceMetric() total query time={} ms", System.currentTimeMillis() - time);
    Map<String, Object> resultMap = new HashMap<>(16);
    resultMap.put("totalCount", resources.size());
    resultMap.put("totalPage", totalPage);
    resultMap.put("pageIndex", pageIndex);
    resultMap.put("pageSize", pageSize);

    Map<String, Iterable<MetricVo>> map2 = new LinkedHashMap<>();
    // order matters.
    for (String identity : topResource) {
        map2.put(identity, map.get(identity));
    }
    resultMap.put("metric", map2);
    return Result.ofSuccess(resultMap);
}

10、启动、验证

  启动Sentinel Dashboard后,访问http://localhost:8001/service(该应用已经添加了sentinel监控配置),访问几次后,可以查看数据库对应的sentinel_metric表中已经有数据被采集了,同时刷新Sentinel Dashboard的实时监控,也有数据可以看到。

Sentinel Dashboard使用了Angular构建了前端页面,因为不熟悉前端的Angular框架,所以采用了简单的方式扩展了查询。感兴趣的童鞋,可以通过修改metric.html页面实现,更加友好的检索方式。

猜你喜欢

转载自blog.csdn.net/hou_ge/article/details/111501814
今日推荐