Springboot项目整合Sentinel
1.接入配置中心的情况下,优先整合配置中心,规则方便看。2.如果规则很少改动或者对于实时性不高,可以选择pull模式,实现简单改动代码量较少,相反就选择push模式。dashboard属于可以选配的一环,毕竟也要不了多少服务器成本。参考:sentinel代码 https://github.com/alibaba/Sentinel/tree/master官方文档 https://github.com
1、背景
当前项目没有接入配置中心、只针对两三个对外三方接口做熔断降级,最初的预想是尽量简单化实现,所以在考虑的时候想着尽量把服务端dashboard剥离出来,监控指标可以后续再调研是否可以暴露给grafana,抱着这个目的开始了一番调研。
2、实施
sentinel规则一共有三种模式
推送模式 | 说明 | 优点 | 缺点 |
---|---|---|---|
原始模式 | API 将规则推送至客户端并直接更新到内存中,扩展写数据源(WritableDataSource) | 简单,无任何依赖 | 不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境 |
Pull模式 | 扩展写数据源(WritableDataSource), 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等 | 简单,无任何依赖;规则持久化 | 不保证一致性;实时性不保证,拉取过于频繁也可能会有性能问题。 |
Push模式 | 扩展读数据源(ReadableDataSource),规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。生产环境下一般采用 push 模式的数据源。 | 规则持久化;一致性;快速 | 引入第三方依赖 |
2.1、原始模式
2.1.1、原始模式下使用dashboard
这个模式是dashboard修改的规则的默认模式,在使用dashboard修改规则后,会调用客户端的api接口同步规则,客户端会讲规则存入内存,也就是说如果重启服务,规则就会荡然无存。所以,这种模式只适合在测试或者写demo的时候使用。
2.1.2、原始模式下不使用dashboard
离开dashboard,可以手动写死或者实现WritableDataSource(具体介绍参考2.2),不过个人认为原始模式就是追求简单,与其实现WritableDataSource再配置,不如直接写死来的简单。 代码如下,不过会带来另一个问题:无法动态修改,所以依旧是一个缺陷很多的模式。
private void initRules() {
//=============================规则1=========================
FlowRule rule1 = new FlowRule();
rule1.setResource("rule1");//规则名称
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);//如果设置0则按照线程数限流,如果设置1则按照QPS(每秒查询率)限流
rule1.setCount(100); // 每秒调用最大次数为 100 次
rule1.setControlBehavior(0);//0快速失败,1预警,2排队等候
rule1.setMaxQueueingTimeMs(1000);//排队超时阈值
//=============================规则2=========================
FlowRule rule2 = new FlowRule();
rule2.setResource("rule2");
rule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule2.setCount(10); // 每秒调用最大次数为 10 次
List<FlowRule> rules = new ArrayList<>();
rules.add(rule1);
rules.add(rule2);
FlowRuleManager.loadRules(rules);
}
2.2 Pull模式
这种模式相较于原始模式有了很大的改变,服务在启动的时候会将对应的读数据源注册至对应的 RuleManager,将写数据源注册至 transport 的 WritableDataSourceRegistry 中,即加载到内存里。同时在写入后,服务端也会定时去轮询查询最新的规则并同步到内存。如果使用dashboard修改规则后,dashboard会调用客户端api来推送规则,接着客户端会将规则写入内存,并持久化。
以本地文件数据源为例
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);
}
}
有两个接口需要注意,一个是ReadableDataSource,一个是WritableDataSource,分别对应规则的读取以及规则的写入。
ReadableDataSource
FileRefreshableDataSource的继承树:FileRefreshableDataSource->AutoRefreshDataSource->AbstractDataSource->ReadableDataSource
实际上FileRefreshableDataSource最终也是实现了ReadableDataSource,先看看ReadableDataSource的作用
public interface ReadableDataSource<S, T> {
//加载配置
T loadConfig() throws Exception;
//数据源读取配置,数据源可以是 yaml 配置文件,可以是 MySQL、Redis 等,由实现类决定从哪种数据源读取配置。
S readSource() throws Exception;
//获取 SentinelProperty
SentinelProperty<T> getProperty();
//用于关闭数据源,例如使用文件存储配置时,可在此方法实现关闭文件输入流等
void close() throws Exception;
}
如果动态数据源提供 SentinelProperty,则可以调用 getProperty 方法获取动态数据源SentinelProperty,将 SentinelProperty 注册给规则管理器,动态数据源在读取到配置时就可以调用自身 SentinelProperty 的 updateValue 方法通知规则管理器更新规则。
AutoRefreshDataSource在原有的基础上增加了AutoRefreshDataSource方法,这就是方法主要是用来定时刷新数据源,代码如下。可以看到默认刷新时间是3000ms,可自定义刷新时间,以免刷新间隔比较短对服务有一些额外负载。
public abstract class AutoRefreshDataSource<S, T> extends AbstractDataSource<S, T> {
private ScheduledExecutorService service;
protected long recommendRefreshMs = 3000L;
public AutoRefreshDataSource(Converter<S, T> configParser) {
super(configParser);
this.startTimerService();
}
public AutoRefreshDataSource(Converter<S, T> configParser, long recommendRefreshMs) {
super(configParser);
if (recommendRefreshMs <= 0L) {
throw new IllegalArgumentException("recommendRefreshMs must > 0, but " + recommendRefreshMs + " get");
} else {
this.recommendRefreshMs = recommendRefreshMs;
this.startTimerService();
}
}
private void startTimerService() {
this.service = Executors.newScheduledThreadPool(1, new NamedThreadFactory("sentinel-datasource-auto-refresh-task", true));
this.service.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
if (!AutoRefreshDataSource.this.isModified()) {
return;
}
T newValue = AutoRefreshDataSource.this.loadConfig();
AutoRefreshDataSource.this.getProperty().updateValue(newValue);
} catch (Throwable var2) {
RecordLog.info("loadConfig exception", var2);
}
}
}, this.recommendRefreshMs, this.recommendRefreshMs, TimeUnit.MILLISECONDS);
}
}
WritableDataSource
public interface WritableDataSource<T> {
//写入持久化
void write(T var1) throws Exception;
//关闭文件流等
void close() throws Exception;
}
WritableDataSource的方法作用相对简单点,主要就是写入持久化的规则,比如FileWritableDataSource中就是讲规则写入文件内。
public void write(T value) throws Exception {
this.lock.lock();
try {
String convertResult = (String)this.configEncoder.convert(value);
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(this.file);
byte[] bytesArray = convertResult.getBytes(this.charset);
RecordLog.info(String.format("[FileWritableDataSource] Writing to file %s: %s", this.file.toString(), convertResult), new Object[0]);
outputStream.write(bytesArray);
outputStream.flush();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (Exception var16) {
}
}
}
} finally {
this.lock.unlock();
}
}
实际上在这种模式下,如果不使用dashboard,可以在客户端暴露接口,手动实现WritableDataSource更新规则。多节点的情况下还得使用redis的发布订阅来保证每个节点都更新规则,有这个时间成本不如部署一套dashboard。
2.3 Push模式
一般生产环境使用的是push模式,就目前的开源sentinel代码存在一个问题。
sentinel dashboard推送规则后,服务端节点重启,规则消失
那么以redis为例,要修改需要怎么做?如图中所述,需要修改sentinel dashboard源码,在修改规则或者新增的时候,推送规则到redis,并且使用发布订阅通知服务端的节点,保证数据一致性。或者在不使用dashboard的情况下,服务端开放一个接口修改redis中sentinel规则。
如何修改sentinel dashboard端的代码?
- 实现DynamicRuleProvider接口,根据应用名称查询redis流控规则;
- 实现DynamicRulePublisher接口,保存流控规则、发布更新消息;
- 修改FlowControllerV1中查询流控规则的方法,改为调用DynamicRuleProvider从redis获取;
- 修改FlowControllerV1中新增、修改、删除时的发布方法,改为调用DynamicRulePublisher保存到redis并发送更新消息。
以FlowRule为例
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 {
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);
return rules;
}
}
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 {
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();
}
}
客户端只需引入官方维护好的代码即可
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-redis</artifactId>
</dependency>
3、总结
1.接入配置中心的情况下,优先整合配置中心,规则方便看。
2.如果规则很少改动或者对于实时性不高,可以选择pull模式,实现简单改动代码量较少,相反就选择push模式。dashboard属于可以选配的一环,毕竟也要不了多少服务器成本。
参考:
sentinel代码 https://github.com/alibaba/Sentinel/tree/master
官方文档 https://github.com/alibaba/Sentinel/wiki
更多推荐
所有评论(0)