熟悉 soul 的数据同步机制中的 websocket 同步
1.什么是数据同步:将admin配置数据同步到soul集群中的jvm内存里,是网管高性能的关键;soul支持 websocket 同步(默认方式,推荐)、zookeeper 同步、http 长轮询同步、nacos 同步四种数据同步机制。
本文主要学习websocket 同步原理及策略
数据同步原理
soul网管启动时,会从配置服务同步配置数据,并支持推拉模式配置变更信息,并且更新本地缓存。管理员在管理后台,变更用户、规则、插件、流量配置,通过推拉模式将变更信息同步给soul网关,具体是push模式,还是pull模式取决于配置。
如下图所示,admin在用户配置发生变更时,会通过EventPublisher发出配置变更通知,由EventDispatcher处理该变更通知,然后根据配置的同步策略(http, websocket, zookeeper),将配置发送给对应的事件处理。
webSocket同步策略,将变更后数据主动推送给soul-web,并且在网管层,会有对应的WebsocketCacheHandler处理器来处理admin的数据推送。
websocket同步策略-配置
在 soul-bootstrap 项目的 pom.xml 文件中引入了 soul-spring-boot-starter-sync-data-websocket 这个 starter 。
org.dromara
soul-spring-boot-starter-sync-data-websocket
${project.version}
在 soul-bootstrap 项目的 application-local.yml 文件中,配置了
soul:
sync:
websocket :
urls: ws://localhost:9295/websocket
soul-admin 配置
在 soul-admin 项目中的 application.yml 文件中默认配置了 websocket.enabled=true 的配置
soul:
sync:
websocket:
enabled: true
@Bean
public SyncDataService websocketSyncDataService(final ObjectProvider websocketConfig, final ObjectProvider pluginSubscriber,
final ObjectProvider> metaSubscribers, final ObjectProvider> authSubscribers) {
log.info("you use websocket sync soul data.......");
return new WebsocketSyncDataService(websocketConfig.getIfAvailable(WebsocketConfig::new), pluginSubscriber.getIfAvailable(),
metaSubscribers.getIfAvailable(Collections::emptyList), authSubscribers.getIfAvailable(Collections::emptyList));
}
在 WebsocketSyncDataService 中创建了 webSocket 客户端,并和 soul-admin 创建的 websocket 服务建立了连接。开启一个异步线程每30秒遍历一次连接,如果连接中断,则重新建立连接。
public WebsocketSyncDataService(final WebsocketConfig websocketConfig,
final PluginDataSubscriber pluginDataSubscriber,
final List metaDataSubscribers,
final List 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);
}
}
从 soul-admin 开始追踪
DataSyncConfiguration 这个类加载了soul.sync.websocket.enabled配置;初始化WebsocketDataChangedListener,WebsocketCollector及ServerEndpointExporter 三个bean
DataChangedEventDispatcher 监听事件类型:
@Override
@SuppressWarnings("unchecked")
public void onApplicationEvent(final DataChangedEvent event) {
for (DataChangedListener listener : listeners) {
switch (event.getGroupKey()) {
case APP_AUTH:
listener.onAppAuthChanged((List) event.getSource(), event.getEventType());
break;
case PLUGIN:
listener.onPluginChanged((List) event.getSource(), event.getEventType());
break;
case RULE:
listener.onRuleChanged((List) event.getSource(), event.getEventType());
break;
case SELECTOR:
listener.onSelectorChanged((List) event.getSource(), event.getEventType());
break;
case META_DATA:
listener.onMetaDataChanged((List) event.getSource(), event.getEventType());
break;
default:
throw new IllegalStateException("Unexpected value: " + event.getGroupKey());
}
}
}
WebsocketDataChangedListener 监听 PluginChange, SelectorChanged, RuleChange
AppAuthChanged,MetaDataChanged 以上五个数据源的变化;
下面追踪onSelectorChanged 数据流转:
public void onSelectorChanged(final List selectorDataList, final DataEventTypeEnum eventType) {
WebsocketData websocketData =
new WebsocketData<>(ConfigGroupEnum.SELECTOR.name(), eventType.name(), selectorDataList);
WebsocketCollector.send(GsonUtils.getInstance().toJson(websocketData), eventType);
}
WebsocketDataHandler 监听到事件类型
@Override
public void handle(final String json, final String eventType) {
List dataList = convert(json);
if (CollectionUtils.isNotEmpty(dataList)) {
DataEventTypeEnum eventTypeEnum = DataEventTypeEnum.acquireByName(eventType);
switch (eventTypeEnum) {
case REFRESH:
case MYSELF:
doRefresh(dataList);
break;
case UPDATE:
case CREATE:
doUpdate(dataList);
break;
case DELETE:
doDelete(dataList);
break;
default:
break;
}
}
}
触发 PluginDataSubscriber.onSelectorSubscribe事件
public void submit(final SelectorData selectorData) {
final List upstreamList = GsonUtils.getInstance().fromList(selectorData.getHandle(), DivideUpstream.class);
if (null != upstreamList && upstreamList.size() > 0) {
UPSTREAM_MAP.put(selectorData.getId(), upstreamList);
UPSTREAM_MAP_TEMP.put(selectorData.getId(), upstreamList);
} else {
UPSTREAM_MAP.remove(selectorData.getId());
UPSTREAM_MAP_TEMP.remove(selectorData.getId());
}
}
admin后台修改数据,规则变更
DataChangedEventDispatcher 监听到数据变化
触发WebsocketDataChangedListener.onRuleChanged
WebsocketCollector.send
for (Session session : SESSION_SET) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("websocket send result is exception: ", e);
}
}
WebsocketDataHandler 监听到事件调用executor 处理更新本地缓存信息
public void executor(final ConfigGroupEnum type, final String json, final String eventType) {
ENUM_MAP.get(type).handle(json, eventType);
}
问题点:
UpstreamCacheManager 设计
public void submit(final SelectorData selectorData) {
final List upstreamList = GsonUtils.getInstance().fromList(selectorData.getHandle(), DivideUpstream.class);
if (null != upstreamList && upstreamList.size() > 0) {
UPSTREAM_MAP.put(selectorData.getId(), upstreamList);
UPSTREAM_MAP_TEMP.put(selectorData.getId(), upstreamList);
} else {
UPSTREAM_MAP.remove(selectorData.getId());
UPSTREAM_MAP_TEMP.remove(selectorData.getId());
}
}
为什么要用两个map存储相同的值
仅仅是为了下面的遍历吗:
private void scheduled() {
if (UPSTREAM_MAP.size() > 0) {
UPSTREAM_MAP.forEach((k, v) -> {
List result = check(v);
if (result.size() > 0) {
UPSTREAM_MAP_TEMP.put(k, result);
} else {
UPSTREAM_MAP_TEMP.remove(k);
}
});
}
}
这样的化UPSTREAM_MAP 里会存储所有的key
删除的时候是不是应该吧这个也删除呢?
public void removeByKey(final String key) {
UPSTREAM_MAP_TEMP.remove(key);
}