〇、概述
-
alibaba sentinel
是一个开源的流控框架,项目地址:https://github.com/alibaba/Sentinel,其中,
sentinel-dashboard
提供了一个简单的web页面,可以实现流控规则的页面配置与展示; sentinel-dashboard
默认使用内存存储数据,可以通过简单的配置,把数据源设置为nacos
,实现持久化。根据官方文档介绍和源码中sentinel-extension
模块的目录结构,可以看出sentinel也提供consul-kv
、etcd
、zookeeper
、apollo
等其他持久化方案。但是,只有nacos
是阿里自家的,集成做得最完善,网上搜到的sentinel-dashboard
持久化基本都是使用nacos
;我们项目里没有
nacos
,也不可能为了一个小小的sentinel-dashboard
而引入。所以,我对sentinel-dashboard
做了个二次开发,通过redis
实现持久化。
一、redis持久化
1.0 依赖版本
组件 | 版本 |
---|---|
sentinel-dashboard/sentinel-core | 1.8.0 |
springboot | 2.0.5.RELEASE |
spring-data-redis | 2.0.10.RELEASE |
lettuce | 5.0.5.RELEASE |
1.1 整体思路
1.1.1 程序现状
- 流控规则、降级规则存储在程序内存中;
- 新增、修改、删除规则时,dashboard通过http client向目标应用推送全量规则;
- 规则id基于
AtomicLong
,单点自增
1.1.2 改造方案
- 存储方式修改: 规则存储在程序内存的同时,也存储到redis;
- 推送方式修改: 基于redis发布订阅功能。新增、修改、删除规则后,发布通知至应用,应用重新加载规则;
- id生成方式修改: 基于redis incr命令,生成分布式自增id
1.2 整体设计
- 实现
DynamicRuleProvider<List<FlowRuleEntity>>
接口,根据应用名称查询redis流控规则; - 实现
DynamicRulePublisher<List<FlowRuleEntity>>
接口,保存流控规则、发布更新消息; - 修改
FlowControllerV1
中查询流控规则的方法,改为调用DynamicRuleProvider<List<FlowRuleEntity>>
从redis获取; - 修改
FlowControllerV1
中新增、修改、删除时的发布方法,改为调用DynamicRulePublisher<List<FlowRuleEntity>>
保存到redis并发送更新消息。
1.3 源码修改
- 集成redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${spring.boot.version}</version>
</dependency>
spring:
redis:
sentinel:
master: master
nodes:
- host1:port1
- host2:port2
- host3:port3
password: password
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory)
{
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setEnableTransactionSupport(true);
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
-
redis key常量定义
public final class Constants { public static final String RULE_FLOW_PREFIX = "sentinel:rule:flow:"; public static final String RULE_FLOW_CHANNEL_PREFIX = "sentinel:channel:flow:"; public static final String RULE_FLOW_ID_KEY = "sentinel:id:flow"; }
-
InMemFlowRuleStore
中的id生成修改@Component public class RedisIdGenerator { @Autowired private RedisTemplate<String, Object> redisTemplate; public long nextId(String key) { return redisTemplate.opsForValue().increment(key, 1); } }
@Component public class InMemFlowRuleStore extends InMemoryRuleRepositoryAdapter<FlowRuleEntity> { @Autowired private RedisIdGenerator redisIdGenerator; @Override protected long nextId() { return redisIdGenerator.nextId(RULE_FLOW_ID_KEY); } }
-
DynamicRuleProvider<List<FlowRuleEntity>>
@Component("flowRuleRedisProvider") public class FlowRuleRedisProvider implements DynamicRuleProvider<List<FlowRuleEntity>> { private final Logger logger = LoggerFactory.getLogger(FlowRuleRedisProvider.class); @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public List<FlowRuleEntity> getRules(String appName) throws Exception { Assert.notNull(appName, "应用名称不能为空"); logger.info("拉取redis流控规则开始: {}", appName); String key = RULE_FLOW_PREFIX + appName; String ruleStr = (String)redisTemplate.opsForValue().get(key); if(StringUtils.isEmpty(ruleStr)) { return Collections.emptyList(); } List<FlowRuleEntity> rules = JSON.parseArray(ruleStr, FlowRuleEntity.class); logger.info("拉取redis流控规则成功, 规则数量: {}", CollectionUtils.size(rules)); return rules; } }
-
DynamicRulePublisher<List<FlowRuleEntity>>
@Component("flowRuleRedisPublisher") public class FlowRuleRedisPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> { private final Logger logger = LoggerFactory.getLogger(FlowRuleRedisPublisher.class); @Autowired private RedisTemplate<String, Object> redisTemplate; @Value("${sentinel.channel.enabled:true}") private boolean channelEnabled; @Override public void publish(String app, List<FlowRuleEntity> rules) throws Exception { Assert.notNull(app, "应用名称不能为空"); Assert.notEmpty(rules, "策略规则不为空"); logger.info("推送流控规则开始, 应用名: {}, 规则数量: {}", app, rules.size()); redisTemplate.multi(); String ruleKey = RULE_FLOW_PREFIX + app; String ruleStr = JSON.toJSONString(rules); redisTemplate.opsForValue().set(ruleKey, ruleStr); if (channelEnabled) { String channelKey = RULE_FLOW_CHANNEL_PREFIX + app; redisTemplate.convertAndSend(channelKey, rules.size()); } redisTemplate.exec(); } }
-
FlowControllerV1
中的查询方法apiQueryMachineRules
@Autowired private FlowRuleRedisProvider flowRuleRedisProvider; @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"); } try { List<FlowRuleEntity> rules = flowRuleRedisProvider.getRules(app); rules = repository.saveAll(rules); return Result.ofSuccess(rules); } catch (Throwable throwable) { logger.error("Error when querying flow rules", throwable); return Result.ofThrowable(-1, throwable); } }
-
FlowControllerV1
中新增、修改、删除时的发布方法publishRules
@Autowired private FlowRuleRedisPublisher flowRuleRedisPublisher; @Value("${sentinel.channel.enabled}") private boolean channelEnabled; private CompletableFuture<Void> publishRules(String app, String ip, Integer port) { List<FlowRuleEntity> allRules = repository.findAllByApp(app); try { flowRuleRedisPublisher.publish(app, allRules); return CompletableFuture.completedFuture(null); } catch (Exception e) { logger.error("推送流控规则至redis失败, 应用名称: {}", app, e); } if (!channelEnabled) { List<FlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); return sentinelApiClient.setFlowRuleOfMachineAsync(app, ip, port, rules); } return AsyncUtils.newFailedFuture( new CommandFailedException("sentinel推送流控规则失败, channelEnabled: " + channelEnabled)); }
1.4 效果展示
-
sentinel:id:flow
中的规则id
- 应用服务的流控规则
-
sentinel:channel:flow:服务名称
中的消息
二、应用端开发
2.1 官方实现
其实官方提供了一个简单的实现:
sentinel-datasource-redis: com.alibaba.csp.sentinel.datasource.redis.RedisDataSource
,但经过测试,我发现它有bug(version 1.8.0),程序初始化时没法全量加载规则,但更新时可以监听。作者并不是不知道初始化时要全量加载,只是单纯地写错了:
构造方法
RedisDataSource()
中,调用loadInitialConfig()
初始化redis数据;loadInitialConfig()
中调用getProperty().updateValue(newValue)
刷新本地规则缓存;-
updateValue(newValue)
方法由DynamicSentinelProperty
实现:@Override public boolean updateValue(T newValue) { if (isEqual(value, newValue)) { return false; } RecordLog.info("[DynamicSentinelProperty] Config will be updated to: " + newValue); value = newValue; for (PropertyListener<T> listener : listeners) { listener.configUpdate(newValue); } return true; }
然而,此时的
listeners
还没有初始化,是个空集合,根本加载不了数据;-
参考官方的配置方式,数据源初始化完成后调用
FlowRuleManager.register2Property()
时才会初始化listeners
,监听器的默认实现为com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager$FlowPropertyListener
。ReadableDataSource<String, List<FlowRule>> redisFlowDataSource = new RedisDataSource<>(); FlowRuleManager.register2Property(redisFlowDataSource.getProperty());
所以,需要重新实现
RedisDataSource
,修正初始化数据逻辑。官方已经提供了实现思路,我只要基于官方的实现,稍微改一改就行了。
2.2 整体设计
参照官方的实现,做以下修改:
- 构造方法中不再尝试加载redis初始化数据,仅订阅消息队列;
- 监听器初始化完成后,手动触发一次
updateValue(newValue)
方法,加载初始化数据; - 当然,前提条件是应用端已经集成了
spring-data-redis
并正确配置了RedisTemplate
2.3 代码编写
-
redis key常量定义
public final class Constants { public static final String RULE_FLOW_PREFIX = "sentinel:rule:flow:"; public static final String RULE_FLOW_CHANNEL_PREFIX = "sentinel:channel:flow:"; }
-
RedisDataSource
public class RedisDataSource<T> extends AbstractDataSource<String, T> { private static final Logger logger = LoggerFactory.getLogger(RedisDataSource.class); private static final String REDIS_SUCCESS_MSG = "OK"; private RedisTemplate redisTemplate; private String ruleKey; private String channel; public RedisDataSource(Converter<String, T> parser, RedisTemplate redisTemplate, String ruleKey, String channel) { super(parser); AssertUtil.notNull(redisTemplate, "redisTemplate can not be null"); AssertUtil.notEmpty(ruleKey, "redis ruleKey can not be empty"); AssertUtil.notEmpty(channel, "redis subscribe channel can not be empty"); this.redisTemplate = redisTemplate; this.ruleKey = ruleKey; this.channel = channel; subscribeFromChannel(); } @Override public String readSource() throws Exception { return (String) redisTemplate.opsForValue().get(ruleKey); } @Override public void close() throws Exception { redisTemplate.execute((RedisCallback<String>) connection -> { connection.getSubscription().unsubscribe(channel.getBytes(StandardCharsets.UTF_8)); return REDIS_SUCCESS_MSG; }); } /** * 订阅消息队列 */ private void subscribeFromChannel() { redisTemplate.execute((RedisCallback<String>) connection -> { connection.subscribe((message, pattern) -> { byte[] bytes = message.getBody(); String msg = new String(bytes, StandardCharsets.UTF_8); logger.info("接收到流控规则更新消息: {} ", msg); getProperty().updateValue(parser.convert(msg)); }, channel.getBytes(StandardCharsets.UTF_8)); return REDIS_SUCCESS_MSG; }); } }
-
redis数据源配置
@Configuration public class RedisDataSourceConfig implements ApplicationRunner { private static final Logger logger = LoggerFactory.getLogger(RedisDataSourceConfig.class); @Autowired private RedisTemplate redisTemplate; @Value("${spring.application.name}") private String appName; @Override public void run(ApplicationArguments args) throws Exception { logger.info("初始化sentinel数据源开始, appName: {}", appName); // 初始化流控规则 String ruleFlowKey = RULE_FLOW_PREFIX + appName; Converter<String, List<FlowRule>> flowRuleParser = msg -> { String rulesStr = (String) redisTemplate.opsForValue().get(ruleFlowKey); return JSON.parseArray(rulesStr, FlowRule.class); }; String ruleFlowChannel = RULE_FLOW_CHANNEL_PREFIX + appName; ReadableDataSource<String, List<FlowRule>> redisFlowDataSource = new RedisDataSource<>(flowRuleParser, redisTemplate, ruleFlowKey, ruleFlowChannel); FlowRuleManager.register2Property(redisFlowDataSource.getProperty()); List<FlowRule> flowRuleList = flowRuleParser.convert(EMPTY); redisFlowDataSource.getProperty().updateValue(flowRuleList); logger.info("初始化sentinel数据源结束, appName: {}", appName); } }
2.4 效果展示
- 初始化规则加载
- 新增规则
- 消息监听
三、dashboard前端升级补丁
3.1 问题
使用过程中,我发现dashboard 1.8.0新增/编辑降级规则时,没有统计时长
字段,通过查阅源码得知统计时长statIntervalMs
被设置为默认值1000,无法修改。而1秒的统计时长,对于QPS较低的应用,难以保证采样均匀;
最新版本1.8.2,统计时长
字段可以修改。
3.2 目标
修改dashboard前端angular代码,添加统计时长
字段。
3.3 过程
-
参照1.8.2版本,修改文件
degrade-rule-dialog.html
,添加如下代码:<div class="form-group"> <label class="col-sm-2 control-label">统计时长</label> <div class="col-sm-4"> <div class="input-group"> <input type='number' min="1" class="form-control highlight-border" ng-model='currentRule.statIntervalMs' placeholder="统计时长(ms)" /> <span class="input-group-addon">ms</span> </div> </div> </div>
清空缓存-重启、maven clean-重启、关闭idea-重启,发现字段不仅没加上,弹框还404了...
angular我根本不会啊,全靠搜索引擎。核对了项目中angular的版本(1.4.8),做了一些没啥用的尝试
-
当我给
scripts/app.js
中的路由定义添加了cache属性后,总算好使了.state('dashboard.degrade', { templateUrl: 'app/views/degrade.html', url: '/degrade/:app', controller: 'DegradeCtl', cache: false, resolve: { loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { return $ocLazyLoad.load({ name: 'sentinelDashboardApp', files: [ 'app/scripts/controllers/degrade.js', ] }); }] } })
3.4 效果
- dashboard前端展示
- dashboard后端接收
- 最终推送到业务应用中的规则