sentinel
官方文档
生产环境完整版改造demo:https://download.csdn.net/download/u013553309/15533552?spm=1001.2014.3001.5503
sentinel
包的引入很多文章、资料都贴着下面的包
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-annotation-aspectjartifactId>
<version>1.6.3version>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-coreartifactId>
<version>1.6.3version>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-transport-simple-httpartifactId>
<version>1.6.3version>
dependency>
官网给出的包确是
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
<version>2.1.2.RELEASEversion>
dependency>
第一种方法没试过,但个人觉得不要与标准去做对,出现问题连参照都没有,启动、编译报错一顿NoClassFound
是最伤的,包反而是会越补越乱
如果是cloud gateway
网关类型对sentinel
做整合,需要严格上面的官方文档地址去做包的引入
我在使用com.alibaba.cloud
包引入sentinel
时,没有注意到与springcloud
包版本的匹配【我cloud项目使用的是cloud的原生包,除了sentinel包使用阿里巴巴的cloud包组件】,启动报错出现错误
版本比对
@Bean
@SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class)
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(App.class, args);
log.info("{} is started!", App.class.getSimpleName());
}
@Slf4j
public class ExceptionUtil {
/**
* 普通阻塞错误
*/
public static ClientHttpResponse handleException(HttpRequest request, byte[] body, ClientHttpRequestExecution execution, BlockException exception) {
log.info("feign调用错误发生: " + exception.getMessage());
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.TOO_MANY_REQUESTS;
}
@Override
public int getRawStatusCode() throws IOException {
return 0;
}
@Override
public String getStatusText() throws IOException {
return null;
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return null;
}
@Override
public HttpHeaders getHeaders() {
return null;
}
};
}
}
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("sentinel")
@SentinelResource(value = "test.sentinel", fallback = "demotion")
public JsonResult<String> test(@RequestParam String param) {
log.info("测试sentinel:{}", param);
return JsonResult.ok(param);
}
/**
* sentinel接口熔断后的降级处理
*/
public String demotion(String param, Throwable e) {
return String.format("sentinel服务降级,入参:%s,异常:%s", param, e.getCause());
}
}
使用postman
或者其他rest
请求工具模拟发送请求
由于sentinel
客户端的监控数据,我并没有做持久化,只显示5分钟之内的监控数据,但是我们可以去查看对应的sentinel日志来回顾历史数据
对应windows
的我的电脑目录是: C:\Users\liuleiba\logs\csp
参照官网的可以去寻找需要的数据 :实时监控
官网原话:生产环境的 Sentinel Dashboard 需要具备下面几个特性:
集中管理和推送规则。sentinel-core 提供 API 和扩展接口来接收信息。开发者需要根据自己的环境,选取一个可靠的推送规则方式;同时,规则最好在控制台中集中管理。
这种做法的好处是简单,无依赖;坏处是重启应用之后。在sentinel-dashboard
上配置好的规则就会消失,仅用于简单测试,不能用于生产环境。
public class FileDataSourceInit implements InitFunc {
@Override
public void init() throws Exception {
String flowRulePath = "xxx";
ReadableDataSource<String, List<FlowRule>> ds = new FileRefreshableDataSource<>(
flowRulePath, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
})
);
// 将可读数据源注册至 FlowRuleManager.
FlowRuleManager.register2Property(ds.getProperty());
WritableDataSource<List<FlowRule>> wds = new FileWritableDataSource<>(flowRulePath, this::encodeJson);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerFlowDataSource(wds);
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。使用 pull 模式的数据源时一般不需要对 Sentinel 控制台进行改造。
这种实现方法好处是简单,不引入新的依赖,坏处是无法保证监控数据的一致性。
Push
模式了!支持可靠、快速的实时监控和历史监控数据查询。sentinel-core 记录秒级的资源运行情况,并且提供 API 来拉取资源运行信息。当机器大于一台以上的时候,可以通过 Dashboard 来拉取,聚合,并且存储这些信息。这个时候,Dashboard 需要有一个存储媒介,来存储历史运行情况。
区分用户角色,来进行操作。生产环境下的权限控制是非常重要的,理论上只有管理员等高级用户才有权限去修改应用的规则。
由于开发者有各自不一样的环境和需求,我们会对“规则管理和推送”,“监控”这两个方面给出建议以及最佳实践;对于权限控制,由于每个开发者的环境都不一样,我们在最佳实践中仅仅使用了简单的认证。开发者可以依循自己的需求,结合实际生产环境,选择最适合自己的方式。
对于上述的操作,我们没有考虑以上的3个问题,但是实际情况而言,规则管理及推送和监控数据的持久化是我们不得不面对的问题,因为默认情况下的sentinel-dashboard
只存储5分钟以内的数据存在内存当中
至此,关于概念上的sentinel
的说明和介绍就到此为止了,下面重点关注一下在生产环境中的使用需要准备去做的事情,以及我在使用途中遇到的问题汇总一下
sentinel
的异常统一处理有很多文章说的是在@SentinelResource
的注解属性中配置,个人觉得比较麻烦,我是在异常中统一处理的
@ExceptionHandler(UndeclaredThrowableException.class)
public JsonResult flowException(UndeclaredThrowableException ex) {
JsonResult jsonResult = new JsonResult();
if (ex.getUndeclaredThrowable() instanceof FlowException) {
jsonResult.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
jsonResult.setMessage("请求服务器过于频繁,请稍侯再试");
} else if (ex.getUndeclaredThrowable() instanceof BlockException) {
jsonResult.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
jsonResult.setMessage("目标服务未启用,请稍候再试");
} else {
jsonResult.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
jsonResult.setMessage("系统未知异常,请联系管理员");
}
return jsonResult;
}
sentinel-dashboard
,持久化监控数据sentinel-dashboard
版本过新这个会直接导致我们引入其他包,像我引入了mybatisplus
想做监控数据的持久化,但是很惨,版本包的问题困扰我一天,需要人为的对sentinel-dashboard
模块的包,尤其是对spring-boot
基础依赖版本有要求,如果出现spring
包的问题,比如@Configuration
注解报找不到proxyBeanMethods
,则可以升级看看,我当时下载的时候没注意sentinel-dashboard
版本,结果发现所依赖的spring-boot
基础版本还挺高的,算是自己给自己挖坑了,不过好在解决了!
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.10.2version>
dependency>
如果缺少其他的包,缺一个补一个就行!
下载sentinel-dashboard
源码: https://github.com/alibaba/Sentinel
关于生产要注意的问题,和规则的推送模式上面已经提到过,先改造监控数据的持久化!
监控数据由于sentinel-dashboard
本身就是使用的内存做的存储,所以只能够提供5分钟范围内的数据存储,这一点在生产上是极其不能接受的,至少也是需要一段时间内的监控数据,我们才可以监测到系统的运行情况,虽然不是每个程序都是百万级别的访问量,但是该做的改造还是要去做。
为了解析和操作方便,此处我使用的是mysql
作为存储介质来存放监控数据,其实换成mongodb
可能要更好一点。
sentinel-dashboard
模块的pom
文件<properties>
<spring.boot.version>2.2.0.RELEASEspring.boot.version>
<curator.version>4.0.1curator.version>
properties>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
<version>8.0.16version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.0version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.10.2version>
dependency>
application.yaml
文件配置redis
和datasource
数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
password: roo1t
url: jdbc:mysql://47.115.158.78:3306/sentinel?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root1
redis:
host: 47.115.158.78
port: 6399
timeout: 20s
# 数据库索引
database: 0
jedis:
pool:
#连接池最大连接数(使用负值表示没有限制)
max-active: 300
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1s
#连接池中的最大空闲连接
max-idle: 100
#连接池中的最小空闲连接
min-idle: 20
mybatis-plus:
configuration:
default-statement-timeout: 30000
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
global-config:
banner: false
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
refresh: true
mapper-locations: classpath*:mapper/*Mapper.xml
typeAliasesPackage: com.alibaba.csp.sentinel.dashboard.datasource.entity
metric
包是存储监控数据的工具repo层,使用的是默认的内存模式存储,所以我们需要创建一个DbMetricsRepository
来替换它,但是存储的数据格式属性,是一模一样的
package com.alibaba.csp.sentinel.dashboard.repository.metric;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelMetricEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.mappers.SentinelMetricsMapper;
import com.alibaba.csp.sentinel.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author liulei
* @version 1.0
*/
@Component
public class DbMetricsRepository implements MetricsRepository<MetricEntity> {
private static final Logger logger = LoggerFactory.getLogger(DbMetricsRepository.class);
private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
@Autowired
private SentinelMetricsMapper metricsMapper;
@Override
public void save(MetricEntity entity) {
if (entity == null || StringUtil.isBlank(entity.getApp())) {
return;
}
readWriteLock.writeLock().lock();
try {
SentinelMetricEntity metricEntity = new SentinelMetricEntity();
BeanUtils.copyProperties(entity, metricEntity);
metricsMapper.insert(metricEntity);
} finally {
readWriteLock.writeLock().unlock();
}
logger.info("存储监控信息:{}", entity);
}
@Override
public void saveAll(Iterable<MetricEntity> metrics) {
Iterator<MetricEntity> iterator = metrics.iterator();
while (iterator.hasNext()) {
MetricEntity metricEntity = iterator.next();
this.save(metricEntity);
}
logger.info("批量存储监控信息:{}", metrics);
}
@Override
public List<MetricEntity> queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime) {
List<SentinelMetricEntity> metricEntities = this.metricsMapper.queryByAppAndResourceBetween(app, resource,
dateFormat.format(new Date(startTime)), dateFormat.format(new Date(endTime)));
List<MetricEntity> entities = new ArrayList<>();
for (SentinelMetricEntity entity : metricEntities) {
MetricEntity metricEntity = new MetricEntity();
BeanUtils.copyProperties(entity, metricEntity);
entities.add(metricEntity);
}
logger.info("查询一段时间内的监控数据:{}", entities);
return entities;
}
@Override
public List<String> listResourcesOfApp(String app) {
List<String> strings = this.metricsMapper.listResourcesOfApp(app);
logger.info("根据app查询监控资源:{}", strings);
return strings;
}
}
package com.alibaba.csp.sentinel.dashboard.datasource.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
/**
* 复制框架原生自带的MetricEntity对象,改成和数据库中映射的字段即可
*
* @author liulei
* @version 1.0
*/
@TableName("sentinel_metric")
public class SentinelMetricEntity {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableId(value = "gmt_create")
private Date gmtCreate;
@TableId(value = "gmt_modified")
private Date gmtModified;
@TableId(value = "app")
private String app;
/**
* 监控信息的时间戳
*/
@TableId(value = "timestamp")
private Date timestamp;
@TableId(value = "resource")
private String resource;
@TableId(value = "pass_qps")
private Long passQps;
@TableId(value = "success_qps")
private Long successQps;
@TableId(value = "block_qps")
private Long blockQps;
@TableId(value = "exception_qps")
private Long exceptionQps;
/**
* summary rt of all success exit qps.
*/
@TableId(value = "rt")
private double rt;
/**
* 本次聚合的总条数
*/
@TableId(value = "count")
private int count;
@TableId(value = "resource_code")
private int resourceCode;
public static SentinelMetricEntity copyOf(MetricEntity oldEntity) {
SentinelMetricEntity entity = new SentinelMetricEntity();
entity.setId(oldEntity.getId());
entity.setGmtCreate(oldEntity.getGmtCreate());
entity.setGmtModified(oldEntity.getGmtModified());
entity.setApp(oldEntity.getApp());
entity.setTimestamp(oldEntity.getTimestamp());
entity.setResource(oldEntity.getResource());
entity.setPassQps(oldEntity.getPassQps());
entity.setBlockQps(oldEntity.getBlockQps());
entity.setSuccessQps(oldEntity.getSuccessQps());
entity.setExceptionQps(oldEntity.getExceptionQps());
entity.setRt(oldEntity.getRt());
entity.setCount(oldEntity.getCount());
return entity;
}
public synchronized void addPassQps(Long passQps) {
this.passQps += passQps;
}
public synchronized void addBlockQps(Long blockQps) {
this.blockQps += blockQps;
}
public synchronized void addExceptionQps(Long exceptionQps) {
this.exceptionQps += exceptionQps;
}
public synchronized void addCount(int count) {
this.count += count;
}
public synchronized void addRtAndSuccessQps(double avgRt, Long successQps) {
this.rt += avgRt * successQps;
this.successQps += successQps;
}
/**
* {@link #rt} = {@code avgRt * successQps}
*
* @param avgRt average rt of {@code successQps}
* @param successQps
*/
public synchronized void setRtAndSuccessQps(double avgRt, Long successQps) {
this.rt = avgRt * successQps;
this.successQps = successQps;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Date getGmtCreate() {
return gmtCreate;
}
public void setGmtCreate(Date gmtCreate) {
this.gmtCreate = gmtCreate;
}
public Date getGmtModified() {
return gmtModified;
}
public void setGmtModified(Date gmtModified) {
this.gmtModified = gmtModified;
}
public String getApp() {
return app;
}
public void setApp(String app) {
this.app = app;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public String getResource() {
return resource;
}
public void setResource(String resource) {
this.resource = resource;
this.resourceCode = resource.hashCode();
}
public Long getPassQps() {
return passQps;
}
public void setPassQps(Long passQps) {
this.passQps = passQps;
}
public Long getBlockQps() {
return blockQps;
}
public void setBlockQps(Long blockQps) {
this.blockQps = blockQps;
}
public Long getExceptionQps() {
return exceptionQps;
}
public void setExceptionQps(Long exceptionQps) {
this.exceptionQps = exceptionQps;
}
public double getRt() {
return rt;
}
public void setRt(double rt) {
this.rt = rt;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getResourceCode() {
return resourceCode;
}
public Long getSuccessQps() {
return successQps;
}
public void setSuccessQps(Long successQps) {
this.successQps = successQps;
}
@Override
public String toString() {
return "DBMetricEntity{" +
"id=" + id +
", gmtCreate=" + gmtCreate +
", gmtModified=" + gmtModified +
", app='" + app + '\'' +
", timestamp=" + timestamp +
", resource='" + resource + '\'' +
", passQps=" + passQps +
", blockQps=" + blockQps +
", successQps=" + successQps +
", exceptionQps=" + exceptionQps +
", rt=" + rt +
", count=" + count +
", resourceCode=" + resourceCode +
'}';
}
}
package com.alibaba.csp.sentinel.dashboard.datasource.mappers;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelMetricEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author liulei
* @version 1.0
*/
public interface SentinelMetricsMapper extends BaseMapper<SentinelMetricEntity> {
/**
* 监控数据条件查询1
*/
List<SentinelMetricEntity> queryByAppAndResourceBetween(@Param("app") String app, @Param("resource") String resource,
@Param("start") String start, @Param("end") String end);
/**
* 监控税局条件查询2
*/
List<String> listResourcesOfApp(String app);
}
<mapper namespace="com.alibaba.csp.sentinel.dashboard.datasource.mappers.SentinelMetricsMapper">
<select id="queryByAppAndResourceBetween"
resultType="com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelMetricEntity">
select * from sentinel_metric
where app = #{app}
<if test="start != null and start != ''">
and `timestamp` >= #{start}
if>
<if test="end != null and end != ''">
and `timestamp` <= #{end}
if>
select>
<select id="listResourcesOfApp" resultType="java.lang.String">
select resource from sentinel_metric
where app = #{app}
select>
mapper>
为此,我们可以根据MetricEntity
反推出监控的数据表sql
CREATE TABLE `sentinel_metric` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id,主键',
`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '修改时间',
`app` varchar(100) DEFAULT NULL COMMENT '应用名称',
`timestamp` datetime DEFAULT NULL COMMENT '统计时间',
`resource` varchar(500) DEFAULT NULL COMMENT '资源名称',
`pass_qps` int(11) DEFAULT NULL COMMENT '通过qps',
`success_qps` int(11) DEFAULT NULL COMMENT '成功qps',
`block_qps` int(11) DEFAULT NULL COMMENT '限流qps',
`exception_qps` int(11) DEFAULT NULL COMMENT '发送异常的次数',
`rt` double DEFAULT NULL COMMENT '所有successQps的rt的和',
`count` int(11) DEFAULT NULL COMMENT '本次聚合的总条数',
`resource_code` int(11) DEFAULT NULL COMMENT '资源的hashCode',
PRIMARY KEY (`id`),
KEY `app_idx` (`app`) USING BTREE,
KEY `resource_idx` (`resource`) USING BTREE,
KEY `timestamp_idx` (`timestamp`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=229 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.dashboard.controller;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import com.alibaba.csp.sentinel.dashboard.domain.Result;
import com.alibaba.csp.sentinel.dashboard.repository.metric.MetricsRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity;
import com.alibaba.csp.sentinel.dashboard.domain.vo.MetricVo;
/**
* @author leyou
*/
@Controller
@RequestMapping(value = "/metric", produces = MediaType.APPLICATION_JSON_VALUE)
public class MetricController {
private static Logger logger = LoggerFactory.getLogger(MetricController.class);
// 查询最近24小时的数据
private static final long dayQueryIntervalMs = 1000 * 60 * 60 * 24 * 1;
@Autowired
@Qualifier("dbMetricsRepository")
private MetricsRepository<MetricEntity> metricStore;
@ResponseBody
@RequestMapping("/queryTopResourceMetric.json")
public Result<?> queryTopResourceMetric(final String app,
Integer pageIndex,
Integer pageSize,
Boolean desc,
Long startTime, Long endTime, String searchKey) {
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;
}
// 人为修改成统计最近24小时内的
if (endTime == null) {
endTime = System.currentTimeMillis();
}
if (startTime == null) {
startTime = endTime - dayQueryIntervalMs;
}
// 注释掉下面的代码,因为1个小时之内的数据无太大意义
/*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)) {
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);
}
@ResponseBody
@RequestMapping("/queryByAppAndResource.json")
public Result<?> queryByAppAndResource(String app, String identity, Long startTime, Long endTime) {
if (StringUtil.isEmpty(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
if (StringUtil.isEmpty(identity)) {
return Result.ofFail(-1, "identity can't be null or empty");
}
if (endTime == null) {
endTime = System.currentTimeMillis();
}
if (endTime == null) {
endTime = System.currentTimeMillis();
}
if (startTime == null) {
startTime = endTime - dayQueryIntervalMs;
}
List<MetricEntity> entities = metricStore.queryByAppAndResourceBetween(
app, identity, startTime, endTime);
List<MetricVo> vos = MetricVo.fromMetricEntities(entities, identity);
return Result.ofSuccess(sortMetricVoAndDistinct(vos));
}
private Iterable<MetricVo> sortMetricVoAndDistinct(List<MetricVo> vos) {
if (vos == null) {
return null;
}
Map<Long, MetricVo> map = new TreeMap<>();
for (MetricVo vo : vos) {
MetricVo oldVo = map.get(vo.getTimestamp());
if (oldVo == null || vo.getGmtCreate() > oldVo.getGmtCreate()) {
map.put(vo.getTimestamp(), vo);
}
}
return map.values();
}
}
对了,别忘记在DashboardApplication
类中加上mybatis-plus
包扫描注解
咱单独开个项目,配置一下与sentinel-dashboard
的连接即可
spring:
cloud:
sentinel:
transport:
port: 8719 #与sentinel服务端交互的端口,默认就是8719
dashboard: localhost:8080
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
此处我这里的实践主要是:管理规则及模式
使用的是push模式
,且将流控规则
持久化到redis
当中
官方文档中提到生产的实践时,反复提及push模式
,只有这一种模式才适合生产的使用,并且可以保证数据的一致性与安全!
@Slf4j
@Component("redisRuleProvider")
public class RedisRuleProviderImpl implements DynamicRuleProvider<List<FlowRuleEntity>> {
@Autowired
private RedisTemplate redisTemplate;
@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
if (StringUtil.isBlank(appName)) {
return new ArrayList<>();
}
String rules = (String) redisTemplate.opsForValue().get(RedisConfig.ruleKey + appName);
log.info("订阅规则:{}-{}", appName, rules);
return JSONObject.parseArray(rules, FlowRuleEntity.class);
}
}
package com.alibaba.csp.sentinel.dashboard.rule.impl;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.dashboard.rule.config.RedisConfig;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 发布规则
*
* @author liulei
* @version 1.0
*/
@Slf4j
@Component("redisRulePublisher")
public class RedisRulePublisherImpl implements DynamicRulePublisher<List<FlowRuleEntity>> {
@Autowired
private RedisTemplate redisTemplate;
@Override
public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
log.info("发布规则:{}-{}", app, rules);
redisTemplate.opsForValue().set(RedisConfig.ruleKey + app, JSON.toJSONString(rules));
}
}
再搭配一个redis
乱码修正的bean
,注入进去
@Configuration
public class RedisConfig {
public static final String ruleKey = "sentinel.rules.flow.ruleKey";
@Autowired(required = false)
public void setRedisTemplate(RedisTemplate redisTemplate) {
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
}
}
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.dashboard.controller;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import com.alibaba.csp.sentinel.dashboard.auth.AuthAction;
import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo;
import com.alibaba.csp.sentinel.dashboard.domain.Result;
import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter;
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Flow rule controller.
*
* @author leyou
* @author Eric Zhao
*/
@RestController
@RequestMapping(value = "/v1/flow")
public class FlowControllerV1 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV1.class);
// 内存存储repo,改成redis存储规则后,它的作用主要是协助redis做规则的存储
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
// =====改造新增代码开始=====
@Autowired
@Qualifier("redisRuleProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("redisRulePublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
// =====改造新增代码结束=====
@Autowired
private SentinelApiClient sentinelApiClient;
@GetMapping("/rules")
@AuthAction(PrivilegeType.READ_RULE)
public Result<List<FlowRuleEntity>> apiQueryMachineRules(@RequestParam String app,
@RequestParam String ip,
@RequestParam Integer port) {
if (StringUtil.isEmpty(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
if (StringUtil.isEmpty(ip)) {
return Result.ofFail(-1, "ip can't be null or empty");
}
if (port == null) {
return Result.ofFail(-1, "port can't be null");
}
try {
//List rules = sentinelApiClient.fetchFlowRuleOfMachine(app, ip, port);
// =====改造新增代码开始=====[如果非得和机器挂钩,可以使用组合key来区分]
List<FlowRuleEntity> rules = ruleProvider.getRules(app);
if (rules != null && !rules.isEmpty()) {
for (FlowRuleEntity entity : rules) {
entity.setApp(app);
if (entity.getClusterConfig() != null && entity.getClusterConfig().getFlowId() != null) {
entity.setId(entity.getClusterConfig().getFlowId());
}
}
}
// =====改造新增代码结束=====
// 存储到内存
rules = repository.saveAll(rules);
return Result.ofSuccess(rules);
} catch (Throwable throwable) {
logger.error("Error when querying flow rules", throwable);
return Result.ofThrowable(-1, throwable);
}
}
private <R> Result<R> checkEntityInternal(FlowRuleEntity entity) {
if (StringUtil.isBlank(entity.getApp())) {
return Result.ofFail(-1, "app can't be null or empty");
}
if (StringUtil.isBlank(entity.getIp())) {
return Result.ofFail(-1, "ip can't be null or empty");
}
if (entity.getPort() == null) {
return Result.ofFail(-1, "port can't be null");
}
if (StringUtil.isBlank(entity.getLimitApp())) {
return Result.ofFail(-1, "limitApp can't be null or empty");
}
if (StringUtil.isBlank(entity.getResource())) {
return Result.ofFail(-1, "resource can't be null or empty");
}
if (entity.getGrade() == null) {
return Result.ofFail(-1, "grade can't be null");
}
if (entity.getGrade() != 0 && entity.getGrade() != 1) {
return Result.ofFail(-1, "grade must be 0 or 1, but " + entity.getGrade() + " got");
}
if (entity.getCount() == null || entity.getCount() < 0) {
return Result.ofFail(-1, "count should be at lease zero");
}
if (entity.getStrategy() == null) {
return Result.ofFail(-1, "strategy can't be null");
}
if (entity.getStrategy() != 0 && StringUtil.isBlank(entity.getRefResource())) {
return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0");
}
if (entity.getControlBehavior() == null) {
return Result.ofFail(-1, "controlBehavior can't be null");
}
int controlBehavior = entity.getControlBehavior();
if (controlBehavior == 1 && entity.getWarmUpPeriodSec() == null) {
return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1");
}
if (controlBehavior == 2 && entity.getMaxQueueingTimeMs() == null) {
return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2");
}
if (entity.isClusterMode() && entity.getClusterConfig() == null) {
return Result.ofFail(-1, "cluster config should be valid");
}
return null;
}
@PostMapping("/rule")
@AuthAction(PrivilegeType.WRITE_RULE)
public Result<FlowRuleEntity> apiAddFlowRule(@RequestBody FlowRuleEntity entity) {
Result<FlowRuleEntity> checkResult = checkEntityInternal(entity);
if (checkResult != null) {
return checkResult;
}
entity.setId(null);
Date date = new Date();
entity.setGmtCreate(date);
entity.setGmtModified(date);
entity.setLimitApp(entity.getLimitApp().trim());
entity.setResource(entity.getResource().trim());
try {
entity = repository.save(entity);
boolean publishRules = publishRules(entity.getApp(), entity.getIp(), entity.getPort());
if (!publishRules) {
logger.info("publish flow rules fail after rule query");
}
return Result.ofSuccess(entity);
} catch (Throwable t) {
Throwable e = t instanceof ExecutionException ? t.getCause() : t;
logger.error("Failed to add new flow rule, app={}, ip={}", entity.getApp(), entity.getIp(), e);
return Result.ofFail(-1, e.getMessage());
}
}
@PutMapping("/save.json")
@AuthAction(PrivilegeType.WRITE_RULE)
public Result<FlowRuleEntity> apiUpdateFlowRule(Long id, String app,
String limitApp, String resource, Integer grade,
Double count, Integer strategy, String refResource,
Integer controlBehavior, Integer warmUpPeriodSec,
Integer maxQueueingTimeMs) {
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
FlowRuleEntity entity = repository.findById(id);
if (entity == null) {
return Result.ofFail(-1, "id " + id + " dose not exist");
}
if (StringUtil.isNotBlank(app)) {
entity.setApp(app.trim());
}
if (StringUtil.isNotBlank(limitApp)) {
entity.setLimitApp(limitApp.trim());
}
if (StringUtil.isNotBlank(resource)) {
entity.setResource(resource.trim());
}
if (grade != null) {
if (grade != 0 && grade != 1) {
return Result.ofFail(-1, "grade must be 0 or 1, but " + grade + " got");
}
entity.setGrade(grade);
}
if (count != null) {
entity.setCount(count);
}
if (strategy != null) {
if (strategy != 0 && strategy != 1 && strategy != 2) {
return Result.ofFail(-1, "strategy must be in [0, 1, 2], but " + strategy + " got");
}
entity.setStrategy(strategy);
if (strategy != 0) {
if (StringUtil.isBlank(refResource)) {
return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0");
}
entity.setRefResource(refResource.trim());
}
}
if (controlBehavior != null) {
if (controlBehavior != 0 && controlBehavior != 1 && controlBehavior != 2) {
return Result.ofFail(-1, "controlBehavior must be in [0, 1, 2], but " + controlBehavior + " got");
}
if (controlBehavior == 1 && warmUpPeriodSec == null) {
return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1");
}
if (controlBehavior == 2 && maxQueueingTimeMs == null) {
return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2");
}
entity.setControlBehavior(controlBehavior);
if (warmUpPeriodSec != null) {
entity.setWarmUpPeriodSec(warmUpPeriodSec);
}
if (maxQueueingTimeMs != null) {
entity.setMaxQueueingTimeMs(maxQueueingTimeMs);
}
}
Date date = new Date();
entity.setGmtModified(date);
try {
entity = repository.save(entity);
if (entity == null) {
return Result.ofFail(-1, "save entity fail: null");
}
boolean publishRules = publishRules(entity.getApp(), entity.getIp(), entity.getPort());
if (!publishRules) {
logger.info("publish flow rules fail after rule save.json");
}
return Result.ofSuccess(entity);
} catch (Throwable t) {
Throwable e = t instanceof ExecutionException ? t.getCause() : t;
logger.error("Error when updating flow rules, app={}, ip={}, ruleId={}", entity.getApp(),
entity.getIp(), id, e);
return Result.ofFail(-1, e.getMessage());
}
}
@DeleteMapping("/delete.json")
@AuthAction(PrivilegeType.WRITE_RULE)
public Result<Long> apiDeleteFlowRule(Long id) {
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
FlowRuleEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofSuccess(null);
}
try {
repository.delete(id);
} catch (Exception e) {
return Result.ofFail(-1, e.getMessage());
}
try {
boolean publishRules = publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort());
if (!publishRules) {
logger.info("publish flow rules fail after rule delete");
}
return Result.ofSuccess(id);
} catch (Throwable t) {
Throwable e = t instanceof ExecutionException ? t.getCause() : t;
logger.error("Error when deleting flow rules, app={}, ip={}, id={}", oldEntity.getApp(),
oldEntity.getIp(), id, e);
return Result.ofFail(-1, e.getMessage());
}
}
// private CompletableFuture publishRules(String app, String ip, Integer port) {
// List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
// return sentinelApiClient.setFlowRuleOfMachineAsync(app, ip, port, rules);
// }
// 新增代码
private boolean publishRules(String app, String ip, Integer port) {
//核心代码,sentinel-dashboard通过http的形式进行数据推送,客户端接收后将规则保存在本地内存中
List<FlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
try {
// 再推送到redis中
rulePublisher.publish(app, rules);
logger.info("添加限流规则成功{}", JSON.toJSONString(rules));
} catch (Exception e) {
e.printStackTrace();
logger.warn("publishRules failed", e);
return false;
}
return sentinelApiClient.setFlowRuleOfMachine(app, ip, port, rules);
}
}
启动项目,重新测试,即可!
降级、热点、授权,除了降级还稍微有点改造的必要以外,其他的暂时先不改造了,授权的话,无关人员没必要让他登录,降级的话,原理和规则的改造类似,DegradeController
找到这个,咔咔按照监控的存储一样类似的改造即可!如果连这个都自己处理不了的话,那么我的文章说明你也没有看懂,那就多看两次,哈哈哈