Sentinel 控制台是流量控制、熔断降级规则统一配置和管理的入口,它为用户提供了机器自发现、簇点链路自发现、监控、规则配置等功能。在 Sentinel 控制台上,我们可以配置规则并实时查看流量控制效果。
Sentinel 控制台中监控数据聚合后直接存在内存中,未进行持久化,且仅保留最近 5 分钟的监控数据。若需要监控数据持久化的功能,可以自行扩展实现 MetricsRepository 接口 。
根据官方文档,对 Sentinel Dashboard 进行优化,将监控数据持久化到 MySQL 数据库,通过 MyBatis-Plus 对监控数据进行操作。
官方地址:https://github.com/alibaba/Sentinel/wiki/在生产环境中使用-Sentinel
Sentinel 监控
Sentinel 会记录资源访问的秒级数据(若没有访问则不进行记录)并保存在本地日志中,具体格式请见 秒级监控日志文档。Sentinel 控制台可以通过 Sentinel 客户端预留的 HTTP API 从秒级监控日志中拉取监控数据,并进行聚合。
目前 Sentinel 控制台中监控数据聚合后直接存在内存中,未进行持久化,且仅保留最近 5 分钟的监控数据。若需要监控数据持久化的功能,可以自行扩展实现 MetricsRepository 接口(0.2.0 版本),然后注册成 Spring Bean 并在相应位置通过 @Qualifier 注解指定对应的 bean name 即可。MetricsRepository 接口定义了以下功能:
- save 与 saveAll:存储对应的监控数据
- queryByAppAndResourceBetween:查询某段时间内的某个应用的某个资源的监控数据
- listResourcesOfApp:查询某个应用下的所有资源
其中默认的监控数据类型为 MetricEntity,包含应用名称、时间戳、资源名称、异常数、请求通过数、请求拒绝数、平均响应时间等信息。对于监控数据的存储,用户需要根据自己的存储精度,来考虑如何存储这些监控数据。为了更好地支撑大规模的集群,生产环境通常需要部署多个控制台实例,通常需要仔细设计下监控分片拉取和写入策略。
同时用户可以自行进行扩展,适配 Grafana 等可视化平台,以便将监控数据更好地进行可视化。
Sentinel Dashboard
MetricsRepository
MetricsRepository接口定义
-
- save:保存 metric 信息
- saveAll:批量保存 metric 信息
- queryByAppAndResourceBetween:通过应用名称(app)、资源名称(resource)、timestamp 开始时间 、timestamp 结束时间查询 metric 列表
- listResourcesOfApp:通过应用名称(app) 查询 Metric 列表
public interface MetricsRepository<T> {
/**
* Save the metric to the storage repository.
*/
void save(T metric);
/**
* Save all metrics to the storage repository.
*/
void saveAll(Iterable<T> metrics);
/**
* Get all metrics by appName and resourceName between a period of time.
*/
List<T> queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime);
/**
* List resource name of provided application name.
*/
List<String> listResourcesOfApp(String app);
}
目前 MetricsRepository 接口的实现是基于内存级别
com.alibaba.csp.sentinel.dashboard.repository.metric.InMemoryMetricsRepository。
MetricEntity
MetricEntity实体类
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;
... ...
}
Sentinel Dashboard 整合 MyBatis Plus
MyBatis Plus 整合步骤
Sentinel Dashboard 整合 MyBatis Plus 将监控数据持久化到 MySQL 步骤 :
1、引入相关依赖
2、修改 application.properties 配置
3、根据 MetricEntity 创建 MySQL 数据库脚本并执行
4、通过 MyBatis Plus Generator 生成 entity、mypper、xml 文件
5、新增 MetricsRepository 的数据库实现 InDatabaseMetricsRepository
6、修改 MetricsRepository 相关注入,改为 InDatabaseMetricsRepository 实现
1、引入相关依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
2、修改 application.properties 中配置
修改 application.properties 文件,在 application.properties 文件中配置 MySQL 数据库连接地址和 mybatis-plus 相关配置。
# mysql
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/spring-boot?characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
# mybatis plus
mybatis-plus.global-config.banner=false
mybatis-plus.configuration.map-underscore-to-camel-case=true
mapper-locations: classpath:mapper/*Mapper.xml
3、创建 MySQL 数据库脚本
MetricEntity 实体类中,已经提供了监控信息的相关数据,根据属性,创建对应的 MySQL 脚本
CREATE TABLE `metric` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`app` varchar(255) DEFAULT NULL COMMENT '应用名称',
`resource` varchar(255) DEFAULT NULL COMMENT '资源名称',
`timestamp` datetime DEFAULT NULL COMMENT '监控信息时间戳',
`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '修改时间',
`pass_qps` bigint(20) DEFAULT NULL COMMENT '通过QPS',
`success_qps` bigint(20) DEFAULT NULL COMMENT '成功QPS',
`block_qps` bigint(20) DEFAULT NULL COMMENT '限流QPS',
`exception_qps` bigint(20) DEFAULT NULL COMMENT '异常QPS',
`rt` decimal(10,2) DEFAULT NULL COMMENT '资源的平均响应时间',
`count` int(10) DEFAULT NULL COMMENT '本次聚合的总条数',
`resource_code` int(10) DEFAULT NULL COMMENT '资源hashcode',
PRIMARY KEY (`id`),
KEY `idx_app_timestamp` (`app`,`timestamp`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Sentinel监控信息表';
4、生成实体类等文件
通过 MyBatis Plus Generator 生成 entity、mypper、xml 文件
MyBatis Plus Generator 工具下载地址:
GitHub spring-boot-mybatis-plus-generator
实体类 Metric.java
public class Metric implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 创建时间
*/
private Date gmtCreate;
/**
* 修改时间
*/
private Date gmtModified;
/**
* 应用名称
*/
private String app;
/**
* 监控信息时间戳
*/
private Date timestamp;
/**
* 资源名称
*/
private String resource;
/**
* 通过QPS
*/
private Long passQps;
/**
* 成功QPS
*/
private Long successQps;
/**
* 限流QPS
*/
private Long blockQps;
/**
* 异常QPS
*/
private Long exceptionQps;
/**
* 资源平均响应时间
*/
private Double rt;
/**
* 本次聚合的总条数
*/
private Integer count;
/**
* 资源hashcode
*/
private Integer resourceCode;
// setter 、getter
}
MetricMapper.java
其中 MetricsRepository 的 saveAll 的实现是通过循环调用 save 方法,如果对数据库进行操作,可以使用批量插入操作,减少对数据库的频繁调用。
metrics.forEach(this::save);
在 MetricMapper.xml 文件中,batchInsert 为批量保存操作接口。
@Mapper
public interface MetricMapper extends BaseMapper<Metric> {
int batchInsert(List<Metric> metricList);
}
MetricMapper.xml文件中 batchInsert 是 MetricMapper 接口中 int batchInsert(List<Metric> metricList) 对应的批量保存 SQL 。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.alibaba.csp.sentinel.dashboard.datasource.mapper.MetricMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.alibaba.csp.sentinel.dashboard.datasource.entity.Metric">
<id column="id" property="id" />
<result column="gmt_create" property="gmtCreate" />
<result column="gmt_modified" property="gmtModified" />
<result column="app" property="app" />
<result column="timestamp" property="timestamp" />
<result column="resource" property="resource" />
<result column="pass_qps" property="passQps" />
<result column="success_qps" property="successQps" />
<result column="block_qps" property="blockQps" />
<result column="exception_qps" property="exceptionQps" />
<result column="rt" property="rt" />
<result column="count" property="count" />
<result column="resource_code" property="resourceCode" />
</resultMap>
<insert id="batchInsert">
insert into metric
(
gmt_create, gmt_modified, app, timestamp, resource, pass_qps,
block_qps, success_qps, exception_qps, rt, count, resource_code
)
values
<foreach collection="list" separator="," item="item">
(
#{item.gmtCreate,jdbcType=TIMESTAMP}, #{item.gmtModified,jdbcType=TIMESTAMP}, #{item.app,jdbcType=VARCHAR},
#{item.timestamp,jdbcType=TIMESTAMP}, #{item.resource,jdbcType=VARCHAR}, #{item.passQps,jdbcType=BIGINT},
#{item.blockQps,jdbcType=BIGINT}, #{item.successQps,jdbcType=BIGINT}, #{item.exceptionQps,jdbcType=BIGINT},
#{item.rt,jdbcType=DECIMAL}, #{item.count,jdbcType=INTEGER}, #{item.resourceCode,jdbcType=INTEGER}
)
</foreach>
</insert>
</mapper>
5、MetricsRepository 数据库持久化实现
新增一个 MetricsRepository 接口的实现类 InDatabaseMetricsRepository.java ,用来将监控信息保存到数据库,并且通过数据库进行操作。
@Component
public class InDatabaseMetricsRepository implements MetricsRepository<MetricEntity> {
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
@Resource
private MetricMapper metricMapper;
@Override
public void save(MetricEntity entity) {
if (entity == null || StringUtil.isBlank(entity.getApp())) {
return;
}
readWriteLock.writeLock().lock();
try {
metricMapper.insert(toPo(entity));
} finally {
readWriteLock.writeLock().unlock();
}
}
@Override
public void saveAll(Iterable<MetricEntity> metrics) {
if (metrics == null) {
return;
}
readWriteLock.writeLock().lock();
try {
List<Metric> metricList = new ArrayList<>();
metrics.forEach(metric -> metricList.add(toPo(metric)));
metricMapper.batchInsert(metricList);
} 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)) {
return results;
}
readWriteLock.readLock().lock();
try {
Metric metric = new Metric();
metric.setApp(app);
metric.setResource(resource);
QueryWrapper<Metric> queryWrapper = new QueryWrapper<>(metric);
queryWrapper.between("timestamp", new Date(startTime), new Date(endTime));
List<Metric> metricList = metricMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(metricList)){
return results;
}
metricList.forEach(e->results.add(toPo(e)));
return results;
} finally {
readWriteLock.readLock().unlock();
}
}
@Override
public List<String> listResourcesOfApp(String app) {
List<String> results = new ArrayList<>();
if (StringUtil.isBlank(app)) {
return results;
}
final long minTimeMs = System.currentTimeMillis() - 1000 * 60;
Map<String, MetricEntity> resourceCount = new ConcurrentHashMap<>(32);
readWriteLock.readLock().lock();
try {
QueryWrapper<Metric> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("app", app);
queryWrapper.ge("timestamp", new Date(minTimeMs));
List<Metric> metricList = metricMapper.selectList(queryWrapper);
List<MetricEntity> metricEntityList = new ArrayList<>();
metricList.forEach(e -> metricEntityList.add(toPo(e)));
if (CollectionUtils.isEmpty(metricEntityList)){
return results;
}
for (MetricEntity newEntity : metricEntityList) {
String resource = newEntity.getResource();
if (resourceCount.containsKey(resource)) {
MetricEntity oldEntity = resourceCount.get(resource);
oldEntity.addPassQps(newEntity.getPassQps());
oldEntity.addRtAndSuccessQps(newEntity.getRt(), newEntity.getSuccessQps());
oldEntity.addBlockQps(newEntity.getBlockQps());
oldEntity.addExceptionQps(newEntity.getExceptionQps());
oldEntity.addCount(1);
} else {
resourceCount.put(resource, MetricEntity.copyOf(newEntity));
}
}
// 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();
}
}
private Metric toPo(MetricEntity metricEntity){
Metric metric = new Metric();
BeanUtils.copyProperties(metricEntity,metric,Metric.class);
return metric;
}
private MetricEntity toPo(Metric metric){
MetricEntity metricEntity = new MetricEntity();
BeanUtils.copyProperties(metric,metricEntity,MetricEntity.class);
return metricEntity;
}
}
6、修改 MetricsRepository 注入
MetricController 和 MetricFetcher 中 使用 MetricsRepository ,修改 MetricsRepository 的注入。
com.alibaba.csp.sentinel.dashboard.controller.MetricController
com.alibaba.csp.sentinel.dashboard.metric.MetricFetcher
官方 Wiki 中
在相应位置通过 @Qualifier 注解指定对应的 bean name 即可
@Qualifier("inDatabaseMetricsRepository")
@Autowired
private MetricsRepository<MetricEntity> metricStore;
如果不想使用 @Autowired 和 @Qualifier,也可以使用 @Resource 注解。
@Resource(name = "inDatabaseMetricsRepository")
private MetricsRepository<MetricEntity> metricStore;
启动 Sentinel Dashboard
使用 mvn clean package
将 Sentinel Dashboard 打成 jar ,在命令行通过 java -jar 启动 Sentinel Dashboard 控制台
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 \
-Dproject.name=sentinel-dashboard -jar target/sentinel-dashboard.jar
也可以使用 Spring Boot 直接启动
我们打开控制台 http://localhost:8080 ,同时调用已经接入 Sentinel 服务,此时会有相关的监控信息
数据库 metric 表保存了接口请求的监控信息
源码
项目源码: