一、实现介绍
根据官方wiki文档,sentinel控制台的实时监控数据,默认仅存储 5 分钟以内的数据。如需持久化,需要定制实现相关接口。
https://github.com/alibaba/Sentinel/wiki/在生产环境中使用-Sentinel-控制台 也给出了指导步骤:
1.自行扩展实现 MetricsRepository 接口;
2.注册成 Spring Bean 并在相应位置通过 @Qualifier 注解指定对应的 bean name 即可。
本文使用时序数据库InfluxDB来进行持久化,从下载开始,一步步编写一个基于InfluxDB的存储实现。
二、InfluxDB介绍及安装使用
InfluxDB官网:https://www.influxdata.com
关键词:
- 高性能时序数据库
- go语言编写没有外部依赖
- 支持HTTP API读写
- 支持类SQL查询语法
- 通过数据保留策略(Retention Policies)支持自动清理历史数据
- 通过连续查询(Continuous Queries)支持数据归档
最新版本:1.6.4
下载
windows:wget https://dl.influxdata.com/influxdb/releases/influxdb-1.6.4_windows_amd64.zip
linux:wget https://dl.influxdata.com/influxdb/releases/influxdb-1.6.4_linux_amd64.tar.gz
注:windows下载安装wget https://eternallybored.org/misc/wget/
在windows环境,解压zip文件至D:influxdbinfluxdb-1.6.4-1目录:
![1a2c0ab1a3acbd321804cca7d41e92c5.png](https://i-blog.csdnimg.cn/blog_migrate/027cca3891ffab11f9b09b8506917d89.jpeg)
打开cmd命令行窗口,在D:influxdbinfluxdb-1.6.4-1执行命令启动influxdb服务端:influxd
![ef7a892bb783938301dc818844be457a.png](https://i-blog.csdnimg.cn/blog_migrate/89f431e020da79b8baf4ca28b785a235.jpeg)
再打开一个cmd窗口,在目录下输入influx启动客户端: // 后面可以带上参数:-precision rfc3339 指定时间格式显示
![3b4089d51d63f99e43d8eb922075c193.png](https://i-blog.csdnimg.cn/blog_migrate/8131ff80ff4ad3c76b24b6377555b77e.jpeg)
show databases发现只有系统的2个数据库,这里我们新建一个sentinel_db,输入命令:create database sentinel_db
![62f81b1dc86484e174d1a6e5c384ab56.png](https://i-blog.csdnimg.cn/blog_migrate/85830d2d2e65920e92019f1b198705e2.jpeg)
use sentinel_db 使用sentinel_db数据库
show measurements 查看数据库中的数据表(measurement)
![ec26e3b3ec7caf8ccb62d9c2c824a104.png](https://i-blog.csdnimg.cn/blog_migrate/6f1254700208a0aa6cbbf16e72d188b0.jpeg)
可以看到,这几个InfluxDB命令跟MySQL很相似。
InfluxDB名词概念:
database:数据库 // 关系数据库的database
measurement:数据库中的表 // 关系数据库中的table
point:表里的一行数据 // 关系数据库中的row
point由3部分组成:
time:每条数据记录的时间,也是数据库自动生成的主索引;// 类似主键
fields:各种记录的值;// 没有索引的字段
tags:各种有索引的属性 // 有索引的字段
三、修改Sentinel Dashboard
在官方github上,有一个java的客户端库:
https://github.com/influxdata/influxdb-java
在sentinel-dashboard的pom.xml中,加入maven依赖:
org.influxdb influxdb-java 2.17
封装一个工具类:存储InfluxDB连接信息以及方便调用
/** * @author cdfive * @date 2018-10-19 */@Componentpublic class InfluxDBUtils { private static Logger logger = LoggerFactory.getLogger(InfluxDBUtils.class); private static String url; private static String username; private static String password; private static InfluxDBResultMapper resultMapper = new InfluxDBResultMapper(); @Value("${influxdb.url}") public void setUrl(String url) { InfluxDBUtils.url = url; } @Value("${influxdb.username}") public void setUsername(String username) { InfluxDBUtils.username = username; } @Value("${influxdb.password}") public void setPassword(String password) { InfluxDBUtils.password = password; } public static void init(String url, String username, String password) { InfluxDBUtils.url = url; InfluxDBUtils.username = username; InfluxDBUtils.password = password; } public static T process(String database, InfluxDBCallback callback) { InfluxDB influxDB = null; T t = null; try { influxDB = InfluxDBFactory.connect(url, username, password); influxDB.setDatabase(database); t = callback.doCallBack(database, influxDB); } catch (Exception e) { logger.error("[process exception]", e); } finally { if (influxDB != null) { try { influxDB.close(); } catch (Exception e) { logger.error("[influxDB.close exception]", e); } } } return t; } public static void insert(String database, InfluxDBInsertCallback influxDBInsertCallback) { process(database, new InfluxDBCallback() { @Override public T doCallBack(String database, InfluxDB influxDB) { influxDBInsertCallback.doCallBack(database, influxDB); return null; } }); } public static QueryResult query(String database, InfluxDBQueryCallback influxDBQueryCallback) { return process(database, new InfluxDBCallback() { @Override public T doCallBack(String database, InfluxDB influxDB) { QueryResult queryResult = influxDBQueryCallback.doCallBack(database, influxDB); return (T) queryResult; } }); } public static List queryList(String database, String sql, Map paramMap, Class clasz) { QueryResult queryResult = query(database, new InfluxDBQueryCallback() { @Override public QueryResult doCallBack(String database, InfluxDB influxDB) { BoundParameterQuery.QueryBuilder queryBuilder = BoundParameterQuery.QueryBuilder.newQuery(sql); queryBuilder.forDatabase(database); if (paramMap != null && paramMap.size() > 0) { Set> entries = paramMap.entrySet(); for (Map.Entry entry : entries) { queryBuilder.bind(entry.getKey(), entry.getValue()); } } return influxDB.query(queryBuilder.create()); } }); return resultMapper.toPOJO(queryResult, clasz); } public interface InfluxDBCallback { T doCallBack(String database, InfluxDB influxDB); } public interface InfluxDBInsertCallback { void doCallBack(String database, InfluxDB influxDB); } public interface InfluxDBQueryCallback { QueryResult doCallBack(String database, InfluxDB influxDB); }}
其中:
url、username、password用于存储InfluxDB的连接、用户名、密码信息,定义为static属性,因此在set方法上使用@Value注解从配置文件读取属性值;
resultMapper用于查询结果到实体类的映射;
init方法用于初始化url、username、password;
process为通用的处理方法,负责打开关闭连接,并且调用InfluxDBCallback回调方法;
insert为插入数据方法,配合InfluxDBInsertCallback回调使用;
query为通用的查询方法,配合InfluxDBQueryCallback回调方法使用,返回QueryResult对象;
queryList为查询列表方法,调用query得到QueryResult,再通过resultMapper转换为List;
在resources目录下的application.properties文件中,增加InfluxDB的配置:
influxdb.url=${influxdb.url}influxdb.username=${influxdb.username}influxdb.password=${influxdb.password}
用${xxx}占位符,这样可以通过maven的pom.xml添加profile配置不同环境(开发、测试、生产) 或 从配置中心读取参数。
在datasource.entity包下,新建influxdb包,下面新建sentinel_metric数据表(measurement)对应的实体类MetricPO:
package com.alibaba.csp.sentinel.dashboard.datasource.entity;import org.influxdb.annotation.Column;import org.influxdb.annotation.Measurement;import java.time.Instant;/** * @author cdfive * @date 2018-10-19 */@Measurement(name = "sentinel_metric")public class MetricPO { @Column(name = "time") private Instant time; @Column(name = "id") private Long id; @Column(name = "gmtCreate") private Long gmtCreate; @Column(name = "gmtModified") private Long gmtModified; @Column(name = "app", tag = true) private String app; @Column(name = "resource", tag = true) private String resource; @Column(name = "passQps") private Long passQps; @Column(name = "successQps") private Long successQps; @Column(name = "blockQps") private Long blockQps; @Column(name = "exceptionQps") private Long exceptionQps; @Column(name = "rt") private double rt; @Column(name = "count") private int count; @Column(name = "resourceCode") private int resourceCode; // getter setter省略}
该类参考MetricEntity创建,加上influxdb-java包提供的注解,通过@Measurement(name = "sentinel_metric")指定数据表(measurement)名称,
time作为时序数据库的时间列;
app、resource设置为tag列,通过注解标识为tag=true;
其它字段为filed列;
接着在InMemoryMetricsRepository所在的repository.metric包下新建InfluxDBMetricsRepository类,实现MetricsRepository接口:
package com.alibaba.csp.sentinel.dashboard.repository.metric;import java.util.ArrayList;import java.util.Date;import java.util.HashMap;import java.util.Iterator;import java.util.List;import java.util.Map;import java.util.concurrent.TimeUnit;import java.util.stream.Collectors;import org.apache.commons.lang.time.DateFormatUtils;import org.apache.commons.lang.time.DateUtils;import org.influxdb.InfluxDB;import org.influxdb.dto.Point;import org.springframework.stereotype.Repository;import org.springframework.util.CollectionUtils;import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity;import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricPO;import com.alibaba.csp.sentinel.dashboard.util.InfluxDBUtils;import com.alibaba.csp.sentinel.util.StringUtil;/** * metrics数据InfluxDB存储实现 * @author cdfive * @date 2018-10-19 */@Repository("influxDBMetricsRepository")public class InfluxDBMetricsRepository implements MetricsRepository { /**时间格式*/ private static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS"; /**数据库名称*/ private static final String SENTINEL_DATABASE = "sentinel_db"; /**数据表名称*/ private static final String METRIC_MEASUREMENT = "sentinel_metric"; /**北京时间领先UTC时间8小时 UTC: Universal Time Coordinated,世界统一时间*/ private static final Integer UTC_8 = 8; @Override public void save(MetricEntity metric) { if (metric == null || StringUtil.isBlank(metric.getApp())) { return; } InfluxDBUtils.insert(SENTINEL_DATABASE, new InfluxDBUtils.InfluxDBInsertCallback() { @Override public void doCallBack(String database, InfluxDB influxDB) { if (metric.getId() == null) { metric.setId(System.currentTimeMillis()); } doSave(influxDB, metric); } }); } @Override public void saveAll(Iterable metrics) { if (metrics == null) { return; } Iterator iterator = metrics.iterator(); boolean next = iterator.hasNext(); if (!next) { return; } InfluxDBUtils.insert(SENTINEL_DATABASE, new InfluxDBUtils.InfluxDBInsertCallback() { @Override public void doCallBack(String database, InfluxDB influxDB) { while (iterator.hasNext()) { MetricEntity metric = iterator.next(); if (metric.getId() == null) { metric.setId(System.currentTimeMillis()); } doSave(influxDB, metric); } } }); } @Override public List queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime) { List results = new ArrayList(); if (StringUtil.isBlank(app)) { return results; } if (StringUtil.isBlank(resource)) { return results; } // 将查询的开始时间和结束减去8小时,因为influxdb使用的是UTC时间,北京时间比UTC时间慢8个小时 endTime = endTime - UTC_8 * 60 * 60 *1000; startTime = startTime - UTC_8 * 60 * 60 *1000; StringBuilder sql = new StringBuilder(); sql.append("SELECT * FROM " + METRIC_MEASUREMENT); sql.append(" WHERE app=$app"); sql.append(" AND resource=$resource"); sql.append(" AND time>=$startTime"); sql.append(" AND time<=$endTime"); Map paramMap = new HashMap(); paramMap.put("app", app); paramMap.put("resource", resource); paramMap.put("startTime", DateFormatUtils.format(new Date(startTime), DATE_FORMAT_PATTERN)); paramMap.put("endTime", DateFormatUtils.format(new Date(endTime), DATE_FORMAT_PATTERN)); List metricPOS = InfluxDBUtils.queryList(SENTINEL_DATABASE, sql.toString(), paramMap, MetricPO.class); if (CollectionUtils.isEmpty(metricPOS)) { return results; } for (MetricPO metricPO : metricPOS) { results.add(convertToMetricEntity(metricPO)); } return results; } @Override public List listResourcesOfApp(String app) { List results = new ArrayList<>(); if (StringUtil.isBlank(app)) { return results; } StringBuilder sql = new StringBuilder(); sql.append("SELECT * FROM " + METRIC_MEASUREMENT); sql.append(" WHERE app=$app"); sql.append(" AND time>=$startTime"); Map paramMap = new HashMap(); long startTime = System.currentTimeMillis() - 1000 * 60; // 将查询的开始时间减去8小时,因为influxdb使用的是UTC时间,北京时间比UTC时间慢8个小时 startTime = startTime - UTC_8 * 60 * 60 *1000; paramMap.put("app", app); paramMap.put("startTime", DateFormatUtils.format(new Date(startTime), DATE_FORMAT_PATTERN)); List metricPOS = InfluxDBUtils.queryList(SENTINEL_DATABASE, sql.toString(), paramMap, MetricPO.class); if (CollectionUtils.isEmpty(metricPOS)) { return results; } List metricEntities = new ArrayList(); for (MetricPO metricPO : metricPOS) { metricEntities.add(convertToMetricEntity(metricPO)); } Map 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()); } private MetricEntity convertToMetricEntity(MetricPO metricPO) { MetricEntity metricEntity = new MetricEntity(); metricEntity.setId(metricPO.getId()); metricEntity.setGmtCreate(new Date(metricPO.getGmtCreate())); metricEntity.setGmtModified(new Date(metricPO.getGmtModified())); metricEntity.setApp(metricPO.getApp()); metricEntity.setTimestamp(Date.from(metricPO.getTime())); metricEntity.setResource(metricPO.getResource()); metricEntity.setPassQps(metricPO.getPassQps()); metricEntity.setSuccessQps(metricPO.getSuccessQps()); metricEntity.setBlockQps(metricPO.getBlockQps()); metricEntity.setExceptionQps(metricPO.getExceptionQps()); metricEntity.setRt(metricPO.getRt()); metricEntity.setCount(metricPO.getCount()); return metricEntity; } private void doSave(InfluxDB influxDB, MetricEntity metric) { influxDB.write(Point.measurement(METRIC_MEASUREMENT) .time(metric.getTimestamp().getTime(), TimeUnit.MILLISECONDS) .tag("app", metric.getApp()) .tag("resource", metric.getResource()) .addField("id", metric.getId()) .addField("gmtCreate", metric.getGmtCreate().getTime()) .addField("gmtModified", metric.getGmtModified().getTime()) .addField("passQps", metric.getPassQps()) .addField("successQps", metric.getSuccessQps()) .addField("blockQps", metric.getBlockQps()) .addField("exceptionQps", metric.getExceptionQps()) .addField("rt", metric.getRt()) .addField("count", metric.getCount()) .addField("resourceCode", metric.getResourceCode()) .build()); }}
其中:
save、saveAll方法通过调用InfluxDBUtils.insert和InfluxDBInsertCallback回调方法,往sentinel_db库的sentinel_metric数据表写数据;
saveAll方法不是循环调用save方法,而是在回调内部循环Iterable metrics处理,这样InfluxDBFactory.connect连接只打开关闭一次;
doSave方法中,.time(DateUtils.addHours(metric.getTimestamp(), 8).getTime(), TimeUnit.MILLISECONDS)
因InfluxDB的UTC时间暂时没找到修改方法,所以这里time时间列加了8个小时时差;
queryByAppAndResourceBetween、listResourcesOfApp里面的查询方法,使用InfluxDB提供的类sql语法,编写查询语句即可。
最后一步,在MetricController、MetricFetcher两个类,找到metricStore属性,在@Autowired注解上面加上@Qualifier("jpaMetricsRepository")注解:
@Qualifier("influxDBMetricsRepository")@Autowiredprivate MetricsRepository metricStore;
四、验证成果
设置sentinel-dashboard工程启动参数:-Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard
启动工程,打开http://localhost:8080,查看各页面均显示正常,
在命令行通过InfluxDB客户端命令,show measurements,可以看到已经生成了sentinel_metric数据表(measurement);
查询总数:select count(id) from sentinel_metric
查询最新5行数据:select * from sentinel_metric order by time desc limit 5
注:命令行语句结束不用加分号
代码参考:https://github.com/cdfive/Sentinel/tree/winxuan_develop/sentinel-dashboard
扩展:
1.考虑以什么时间维度归档历史数据;
2.结合grafana将监控数据进行多维度的统计和呈现。
五、参考
Sentinel官方文档:
https://github.com/alibaba/Sentinel/wiki/控制台
https://github.com/alibaba/Sentinel/wiki/在生产环境中使用-Sentinel-控制台
InfluxDB官网文档 https://docs.influxdata.com/influxdb/v1.6/introduction/getting-started/
InfluxDB简明手册 https://xtutu.gitbooks.io/influxdb-handbook/content/
原文链接:https://www.cnblogs.com/cdfive2018/p/9914838.html
注:在原文的链接基础之上修正了Sentinal控制台可以显示监控数据,但是通过Grafana因为时区的问题不能够正常展示的问题。