昨天在跑springCloud测试用例过程中,后面有一些疑惑,就是元数据那边已触发元数据更新,但是bootstrap没有同步过来,元数据中没有看到springCloud的,不过今天重新测试,又没有复现出该问题,今天的主要目的是想了解soul的配置数据从admin同步到bootstrap的过程。
按照官网介绍,soul配置数据有三种同步方式,分别是websocket、zookeeper以及http长轮询,本文仅研究websocket同步。过程如下:
分别启动admin和bootstrap,bootstrap后台发现如下日志:
INFO 45220 --- [ main] b.s.s.d.w.WebsocketSyncDataConfiguration : you use websocket sync soul data.......
INFO 45220 --- [ main] o.d.s.p.s.d.w.WebsocketSyncDataService : websocket connection is successful.....
admin后台发现如下日志:
INFO 44624 --- [0.0-9095-exec-3] o.d.s.a.l.websocket.WebsocketCollector : websocket on open successful....
表示admin和bootstrap的websoket链接已建立。admin是作为配置数据提供方,应该是websocket服务端,先从admin分析,然后再分析客户端bootstrap。
admin端分析(websocket服务端)
- 根据上述admin日志提示,可以找到
WebsocketCollector
类,这个类用@ServerEndpoint("/websocket")
注解成websocket服务端,他对应配置类为DataSyncConfiguration
,配置如下:
/**
* The WebsocketListener(default strategy).
*/
@Configuration
@ConditionalOnProperty(name = "soul.sync.websocket.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(WebsocketSyncProperties.class)
static class WebsocketListener {
/**
* Config event listener data changed listener.
*
* @return the data changed listener
*/
@Bean
@ConditionalOnMissingBean(WebsocketDataChangedListener.class)
public DataChangedListener websocketDataChangedListener() {
return new WebsocketDataChangedListener();
}
/**
* Websocket collector websocket collector.
*
* @return the websocket collector
*/
@Bean
@ConditionalOnMissingBean(WebsocketCollector.class)
public WebsocketCollector websocketCollector() {
return new WebsocketCollector();
}
/**
* Server endpoint exporter server endpoint exporter.
*
* @return the server endpoint exporter
*/
@Bean
@ConditionalOnMissingBean(ServerEndpointExporter.class)
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
可以通过yml来配置是否启用websocket同步。
- WebsocketCollector推送配置数据给bootstrap,是通过如下的send函数进行同步:
public static void send(final String message, final DataEventTypeEnum type) {
if (StringUtils.isNotBlank(message)) {
if (DataEventTypeEnum.MYSELF == type) {
try {
Session session = (Session) ThreadLocalUtil.get(SESSION_KEY);
if (session != null) {
session.getBasicRemote().sendText(message);
}
} catch (IOException e) {
log.error("websocket send result is exception: ", e);
}
return;
}
for (Session session : SESSION_SET) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("websocket send result is exception: ", e);
}
}
}
}
调试的时候,可以通过添加日志,打印出server端推送了什么内容给客户端。
- 追溯send函数上游的调用情况,可以找到DataChangedEventDispatcher类的onApplicationEvent函数这里监听了各种配置数据的变化并进行通告,该函数具体如下:
public void onApplicationEvent(final DataChangedEvent event) {
for (DataChangedListener listener : listeners) {
switch (event.getGroupKey()) {
case APP_AUTH:
listener.onAppAuthChanged((List<AppAuthData>) event.getSource(), event.getEventType());
break;
case PLUGIN:
listener.onPluginChanged((List<PluginData>) event.getSource(), event.getEventType());
break;
case RULE:
listener.onRuleChanged((List<RuleData>) event.getSource(), event.getEventType());
break;
case SELECTOR:
listener.onSelectorChanged((List<SelectorData>) event.getSource(), event.getEventType());
break;
case META_DATA:
listener.onMetaDataChanged((List<MetaData>) event.getSource(), event.getEventType());
break;
default:
throw new IllegalStateException("Unexpected value: " + event.getGroupKey());
}
}
}
- 对应的变化通告发送方是类型为ApplicationEventPublisher的一个publisher,根据元数据操作的不同,在不同地方发送通告,比如启用/禁用元数据,对应在MetaDataServiceImpll类的enabled函数中(可断点验证),对应代码如下:
public String enabled(final List<String> ids, final Boolean enabled) {
List<MetaData> metaDataList = Lists.newArrayList();
for (String id : ids) {
MetaDataDO metaDataDO = metaDataMapper.selectById(id);
if (Objects.isNull(metaDataDO)) {
return AdminConstants.ID_NOT_EXIST;
}
metaDataDO.setEnabled(enabled);
metaDataMapper.updateEnable(metaDataDO);
metaDataList.add(MetaDataTransfer.INSTANCE.mapToData(metaDataDO));
}
// publish change event.
eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.META_DATA, DataEventTypeEnum.UPDATE,
metaDataList));
return StringUtils.EMPTY;
}
通告eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.META_DATA, DataEventTypeEnum.UPDATE, metaDataList))
函数,将元数据启用/禁用的事件通告出去。
bootstrap分析(websocket客户端)
- 在WebsocketSyncDataService类中,可以找到websocket客户端启动的初始化情况,代码如下:
public WebsocketSyncDataService(final WebsocketConfig websocketConfig,
final PluginDataSubscriber pluginDataSubscriber,
final List<MetaDataSubscriber> metaDataSubscribers,
final List<AuthDataSubscriber> authDataSubscribers) {
String[] urls = StringUtils.split(websocketConfig.getUrls(), ",");
executor = new ScheduledThreadPoolExecutor(urls.length, SoulThreadFactory.create("websocket-connect", true));
for (String url : urls) {
try {
clients.add(new SoulWebsocketClient(new URI(url), Objects.requireNonNull(pluginDataSubscriber), metaDataSubscribers, authDataSubscribers));
} catch (URISyntaxException e) {
log.error("websocket url({}) is error", url, e);
}
}
try {
for (WebSocketClient client : clients) {
boolean success = client.connectBlocking(3000, TimeUnit.MILLISECONDS);
if (success) {
log.info("websocket connection is successful.....");
} else {
log.error("websocket connection is error.....");
}
executor.scheduleAtFixedRate(() -> {
try {
if (client.isClosed()) {
boolean reconnectSuccess = client.reconnectBlocking();
if (reconnectSuccess) {
log.info("websocket reconnect is successful.....");
} else {
log.error("websocket reconnection is error.....");
}
}
} catch (InterruptedException e) {
log.error("websocket connect is error :{}", e.getMessage());
}
}, 10, 30, TimeUnit.SECONDS);
}
/* client.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxyaddress", 80)));*/
} catch (InterruptedException e) {
log.info("websocket connection...exception....", e);
}
}
这里根据不同的url启动不同的客户端,url可以在yml配置文件中进行设置,本地测试环境中,yml配置如下:
soul :
sync:
websocket :
urls: ws://localhost:9095/websocket
这里的url对应的就是websocket服务端的url(多个url可以逗号分隔)。
- 从上一步初始化代码中,我们知道websoket客户端类为 SoulWebsocketClient,进入该类,发现如下接收函数:
private void handleResult(final String result) {
WebsocketData websocketData = GsonUtils.getInstance().fromJson(result, WebsocketData.class);
ConfigGroupEnum groupEnum = ConfigGroupEnum.acquireByName(websocketData.getGroupType());
String eventType = websocketData.getEventType();
String json = GsonUtils.getInstance().toJson(websocketData.getData());
websocketDataHandler.executor(groupEnum, json, eventType);
}
在这里接收服务端推送的配置信息
3. 在上述excutor打断点不断往下走,最后走到MetaDataCache缓存类,调用如下函数更新缓存:
public void cache(final MetaData data) {
META_DATA_MAP.put(data.getPath(), data);
}
后记
本篇仅针对配置数据的websocket同步通道进行跟踪和分析,另外一些同步的具体策略还没有具体分析,比如websocket断掉如何再次同步等,后续有待继续研究。