上一章我们学习了如何使用 Sentinel Dashboard 以及其底层通讯原理,其能帮我们在日常运维中很方便的进行规则管理。但还存在一个较大的缺陷,目前规则都存储在 dashboard 或业务系统的内存中,当发生重启时,之前维护的规则都会丢失,这对一个追求稳定运行的系统来说是万万不能接受的。所幸的是 sentinel 本身就支持规则的持久化存储以及从外部数据源中加载规则。
读取外部数据源
sentinel 可以从外部数据源加载规则,其支持多种主流的外部数据源,按照工作模式可以大致分为两种:
- 推送模式(push base)的外部数据源
- 拉取模式(pull base)的外部数据源
推送模式的外部数据源,指的是该数据源支持订阅模式,当数据源中的内容发生变化时会主动推送给 sentinel 进行规则变更。比较典型的有 zookeeper、apollo、nacose、redis 等。
拉取模式的外部数据源,指的是该数据源无法在内容发生变化时主动通知到 sentinel,只能由 sentinel 主动拉取内容进行比对,判断内容是否发生变化。比较典型的有 eureka、file(文件)等。
使用方式 & 实现原理
上面介绍了两种模式的外部数据源,那我们该怎么使用呢?sentinel 对这些数据源进行了封装,为我们屏蔽了这些数据源使用上的差异。我们可以通过以下方式来订阅数据源,读取其中的规则,并在后续发生内容变更时,及时的去更新 sentinel 规则。
// 我们以 zookeeper 为例,创建对应组件的可读数据源
// 第四个参数 Function<String, T> 用于将数据源中读取到的文本转换为规则
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ZookeeperDataSource<>(remoteAddress, groupId, flowDataId,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
// 从数据源中获取 sentinel 相关配置,并且更新到对应的 RuleManager
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
sentinel 底层是如何实现规则的初始化和增量加载的呢?阅读相关代码,我们可以发现其主要实际逻辑与 ReadableDataSource、SentinelProperty、PropertyListener 三个类有关,其类图如下:
- ReadableDataSource 提供 getProperty 方法获取相关 sentinel 配置
- SentinelProperty 提供注册 PropertyListener 来监听属性的变化
- PropertyListener 当属性发送变化时,更新对应 RuleManager 中的规则
此三个类的工作模式如上图。当数据源中的内容发生变化时,ReadableDataSource 会调用 SentinelProperty#updateValue 方法来触发相应的 PropertyListener 进行属性变更。通常情况下,PropertyListener 实现会在属性变更时同步更新对应 RuleManager 中的规则。那么 ReadableDataSource 是如何感知外部数据源内容的变化呢?下面我们将分别了解推送模式和拉取模式数据源的具体实现。
推送模式的数据源实现
push 模式主要适用于支持订阅模式的数据源,典型的数据源有: zookeeper、apollo、nacos 等。我们以 zookeeper 为例,介绍对应的 push 模式的 ReadableDataSource 实现。
在 zookeeper 中我们可以订阅节点,在节点内容发生变化时,执行对应回调方法,ZookeeperDataSource 就是通过该特性实现相关属性值变化时的感知。相关源码解析如下:
public ZookeeperDataSource(final String serverAddr, final List<AuthInfo> authInfos, final String groupId, final String dataId,
Converter<String, T> parser) {
// ....
init(serverAddr, authInfos);
}
private void init(final String serverAddr, final List<AuthInfo> authInfos) {
// 注册结点监听器
initZookeeperListener(serverAddr, authInfos);
// 加载初始化规则
loadInitialConfig();
}
private void loadInitialConfig() {
try {
// 获取节点内容,并通过自定义的 Convert 转换为规则
T newValue = loadConfig();
if (newValue == null) {
RecordLog.warn("[ZookeeperDataSource] WARN: initial config is null, you may have to check your data source");
}
// 更新 SentinelProperty 值
getProperty().updateValue(newValue);
} catch (Exception ex) {
RecordLog.warn("[ZookeeperDataSource] Error when loading initial config", ex);
}
}
private void initZookeeperListener(final String serverAddr, final List<AuthInfo> authInfos) {
try {
this.listener = CuratorCacheListener.builder().forNodeCache(() -> {
try {
T newValue = loadConfig();
RecordLog.info("[ZookeeperDataSource] New property value received for ({}, {}): {}",
serverAddr, path, newValue);
// Update the new value to the property.
getProperty().updateValue(newValue);
} catch (Exception ex) {
RecordLog.warn("[ZookeeperDataSource] loadConfig exception", ex);
}
}).build();
// 创建 zk 连接,并注册监听器
// ...
} catch (Exception e) {
RecordLog.warn("[ZookeeperDataSource] Error occurred when initializing Zookeeper data source", e);
e.printStackTrace();
}
}
拉取模式的数据源实现
一些数据源并不支持发订阅的模式,这个时候数据源中的内容发生变化后,该如何感知到呢?
既然 sentinel 不能被动收到通知,那就主动拉取。主动拉取的实现一般是通过定时访问数据源内容,判断是否发生变化。仅支持 Pull 模式的数据源有 file、eureka,我们以 file 为例,介绍对应的 pull 模式的 ReadableDataSource 实现。相关源码解析如下:
public FileRefreshableDataSource(File file, Converter<String, T> configParser, long recommendRefreshMs, int bufSize,
Charset charset) throws FileNotFoundException {
// 参数验证和赋值
// If the file does not exist, the last modified will be 0.
this.lastModified = file.lastModified();
// 加载初始化规则
firstLoad();
}
private void firstLoad() {
try {
T newValue = loadConfig();
getProperty().updateValue(newValue);
} catch (Throwable e) {
RecordLog.info("loadConfig exception", e);
}
}
// 此方法实际由定时任务触发
@Override
public String readSource() throws Exception {
if (!file.exists()) {
// Will throw FileNotFoundException later.
RecordLog.warn(String.format("[FileRefreshableDataSource] File does not exist: %s", file.getAbsolutePath()));
}
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
FileChannel channel = inputStream.getChannel();
if (channel.size() > buf.length) {
throw new IllegalStateException(file.getAbsolutePath() + " file size=" + channel.size()
+ ", is bigger than bufSize=" + buf.length + ". Can't read");
}
int len = inputStream.read(buf);
return new String(buf, 0, len, charset);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception ignore) {
}
}
}
}
@Override
protected boolean isModified() {
long curLastModified = file.lastModified();
if (curLastModified != this.lastModified) {
this.lastModified = curLastModified;
return true;
}
return false;
}
规则的持久化
上面我们了解了 sentinel 如何初始和增量的获取外部数据源中的规则内容,那么 sentinel 如何在运行时当规则发生变化时对其持久化的呢?
sentinel 中定义了 ReadableDataSource
抽象类用于抽象外部数据源内容的读取操作,相应的也定义了 WritableDataSource
抽象类用于抽象规则变化时的持久化操作。该接口定义非常简单,只有两个方法:
- write 将规则写入数据源中
- close 用于数据源的关闭
其使用方式也非常的简单:
// 具体写法请参考:sentinel-demo/sentinel-demo-dynamic-file-rule
// 创建写入数据源的实现
WritableDataSource<List<FlowRule>> wds = new FileWritableDataSource<>(flowRulePath, Objects::toString);
// 将其注册到 WritableDataSourceRegistry 中的对应规则(如:FlowRule、SystemRule)数据源中
// 当相关规则变更时,会触发 WritableDataSource#write 操作
WritableDataSourceRegistry.registerFlowDataSource(wds);
当数据源被注册以后,其什么时候会触发规则的写入呢?我们查看 WritableDataSource#write
方法的调用链路可以发现,其在以下几个规则变更时会被调用:
- 一般规则(限流规则、授权规则、系统规则等)变更时
- 热点规则变更时
- 网关路由分组变更时
- 网关限流规则变更时
总结
在本章我们了解了 sentinel 是如何从外部数据源加载规则,以及如何在规则发生变更时写入外部数据源进行持久化的。
sentinel 支持从大部分主流的外部数据源加载规则,按照工作模式大致可以分为两种:
- 推送模式的数据源,此类型数据源支持订阅模式,当数据内容发生变化时,主动推送到 sentinel 进行对应的规则更新
- 拉取模式的数据源,此类型数据不支持订阅模式,由 sentinel 定时去拉取内容,判断内容是否变化,从而更新对应的规则
sentinel 会在以下几种规则发生变更时触发已注册的数据源写入动作:
- 一般规则(限流规则、授权规则、系统规则等)变更时
- 热点规则变更时
- 网关路由分组变更时
- 网关限流规则变更时