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页面实现,更加友好的检索方式。