Soul源码分析--soul-admin数据同步之websocket、http
之前写过一篇 Soul源码分析–soul-admin源码分析 里面介绍了数据同步的方式和数据同步的种类,现在来深入分析一下各种数据同步的原理,包括 websocket、http、zookeeper、nacos 数据同步。这里我们先可以参考 官网的文档,大概看一下原理,然后根据代码来分析。
这一篇只会分析 websocket、http,下篇会分析 zookeeper、nacos,点这里查看 下一篇 。
webosocket 数据同步
- 启动 soul-admin 项目时,会根据数据源加载相应的数据,这里用 mysql 数据源为例,启动会执行 init 包下面的 LocalDataSourceLoader 类的方法,判断 bean 是否是数据源且初始化设置是否为 true,然后会执行相应初始化数据的方法,控制台会打印相应执行的 sql 文件名称(这里要注意的是,每次都会去执行初始化,所以如果用在生产上的话,应该把 initEnable 这个属性在第一次执行完之后,设置成 false)。
private void execute(final Connection conn) throws Exception {
ScriptRunner runner = new ScriptRunner(conn);
runner.setLogWriter(null);
Resources.setCharset(StandardCharsets.UTF_8);
Reader read = Resources.getResourceAsReader(dataBaseProperties.getInitScript());
log.info("execute soul schema sql: {}", dataBaseProperties.getInitScript());
runner.runScript(read);
runner.closeConnection();
conn.close();
}

- 如果在网关后台选择器配置中配置了一个没启动服务的地址,点保存后,后台会去执行检查,如果没找到相应的服务,这个配置则不生效。

点保存后,控制台打出来的日志,可以看出来,检查地址失败了(不知道什么原因,日志这里打了断点没能进去调度)。

再去看看页面刚刚加的配置,也被清空了。

如果配置的地址不对,后台会有线程去检查这个地址,如果地址不对,再刷新一遍数据,这也是为什么 Soul-WebSocketClient 里面的这段代码会执行两次
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);
}
- 注意:如果只用 websocket 或者 http 同步数据,在 bootstrap 项目里面需要把下面这个 maven 依赖给注释掉

因为这里面会疯狂的连接 zookeeper ,为了测试方便,先把这个注释掉

Http 数据同步
-
老规矩,把 yml 文件里面的 websocket 改成 http 然后启动 soul-admin 项目,首先还是会先去执行初始化脚本,这个跟 websocket 的一样。接下来会进到 UpstreamCheckService 这个类里面来,这里有点不一样的地方是,http 数据同步时,刚启动时会触发这个类里面的 check(final String selectorName, final List upstreamList) 方法,用 websocket 好像是不会的。调试时发现 http 的 handle 是有值的,至于什么时候写进去的,后面再研究,页面也会显示这个值

-
有值之后就开始执行 setup() 方法里的定时任务了,这里创建了一个定时线程数量为cpu核数,间隔10秒后开始执行,每10秒执行一次的定时任务。
if (check) { new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), SoulThreadFactory.create("scheduled-upstream-task", false)) .scheduleWithFixedDelay(this::scheduled, 10, scheduledTime, TimeUnit.SECONDS); }重点是来看这个 check 方法,检查 divide 中配置的地址是否有效,如果有效再次检查是否启用这个配置,如果没启用则先启用这个配置,再加到全局的 Map 里面。这里的 “check the url=XXX is fail” 是不是很眼熟,没错!之前配置错的时候会执行这一句,然后把数据清除掉,最后调用更新方法更新数据库中的记录,和发布到事件中去,让相应的监听处理相应的事件,然后把数据同步到 bootstrap 中
private void check(final String selectorName, final List<DivideUpstream> upstreamList) { List<DivideUpstream> successList = Lists.newArrayListWithCapacity(upstreamList.size()); for (DivideUpstream divideUpstream : upstreamList) { final boolean pass = UpstreamCheckUtils.checkUrl(divideUpstream.getUpstreamUrl()); if (pass) { if (!divideUpstream.isStatus()) { divideUpstream.setTimestamp(System.currentTimeMillis()); divideUpstream.setStatus(true); log.info("UpstreamCacheManager check success the url: {}, host: {} ", divideUpstream.getUpstreamUrl(), divideUpstream.getUpstreamHost()); } successList.add(divideUpstream); } else { divideUpstream.setStatus(false); log.error("check the url={} is fail ", divideUpstream.getUpstreamUrl()); } } if (successList.size() == upstreamList.size()) { return; } if (successList.size() > 0) { UPSTREAM_MAP.put(selectorName, successList); updateSelectorHandler(selectorName, successList); } else { UPSTREAM_MAP.remove(selectorName); updateSelectorHandler(selectorName, null); } } -
soul-bootstrap 启动之后,会和 admin 进行数据同步,admin 相关的类是 HttpLongPollingDataChangedListener,会先执行 afterInitialize() 方法。这个方法的作用是每隔5分钟去刷新一下数据和数据的 md5 值
protected void afterInitialize() { long syncInterval = httpSyncProperties.getRefreshInterval().toMillis(); // Periodically check the data for changes and update the cache scheduler.scheduleWithFixedDelay(() -> { log.info("http sync strategy refresh config start."); try { this.refreshLocalCache(); log.info("http sync strategy refresh config success."); } catch (Exception e) { log.error("http sync strategy refresh config error!", e); } }, syncInterval, syncInterval, TimeUnit.MILLISECONDS); log.info("http sync strategy refresh interval: {}ms", syncInterval); }所以控制台会每隔5分钟出来下面的日志,下面表示正常缓存更新成功
2021-01-27 01:42:59.138 INFO 51481 --- [-long-polling-2] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh config start. 2021-01-27 01:42:59.150 INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[APP_AUTH], old: {group='APP_AUTH', md5='d751713988987e9331980363e24189ce', lastModifyTime=1611682679083}, updated: {group='APP_AUTH', md5='d751713988987e9331980363e24189ce', lastModifyTime=1611682979150} 2021-01-27 01:42:59.197 INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[RULE], old: {group='RULE', md5='82f8a3c07c416d980ef14518d465e3aa', lastModifyTime=1611682679129}, updated: {group='RULE', md5='82f8a3c07c416d980ef14518d465e3aa', lastModifyTime=1611682979197} 2021-01-27 01:42:59.202 INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[SELECTOR], old: {group='SELECTOR', md5='064250aa2a9ebe79633aa89575544dcb', lastModifyTime=1611682679134}, updated: {group='SELECTOR', md5='064250aa2a9ebe79633aa89575544dcb', lastModifyTime=1611682979202} 2021-01-27 01:42:59.203 INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[META_DATA], old: {group='META_DATA', md5='5f79d821e3b601330631a2d53294fb34', lastModifyTime=1611682679135}, updated: {group='META_DATA', md5='5f79d821e3b601330631a2d53294fb34', lastModifyTime=1611682979203} 2021-01-27 01:42:59.203 INFO 51481 --- [-long-polling-2] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh config success. 2021-01-27 01:47:59.210 INFO 51481 --- [-long-polling-2] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh config start. 2021-01-27 01:47:59.225 INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[APP_AUTH], old: {group='APP_AUTH', md5='d751713988987e9331980363e24189ce', lastModifyTime=1611682979150}, updated: {group='APP_AUTH', md5='d751713988987e9331980363e24189ce', lastModifyTime=1611683279225} 2021-01-27 01:47:59.226 INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[PLUGIN], old: {group='PLUGIN', md5='0298afdf3cc5338833c99f44fb88f1e9', lastModifyTime=1611682979151}, updated: {group='PLUGIN', md5='0298afdf3cc5338833c99f44fb88f1e9', lastModifyTime=1611683279226} 2021-01-27 01:47:59.271 INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[RULE], old: {group='RULE', md5='82f8a3c07c416d980ef14518d465e3aa', lastModifyTime=1611682979197}, updated: {group='RULE', md5='82f8a3c07c416d980ef14518d465e3aa', lastModifyTime=1611683279271} 2021-01-27 01:47:59.275 INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[SELECTOR], old: {group='SELECTOR', md5='064250aa2a9ebe79633aa89575544dcb', lastModifyTime=1611682979202}, updated: {group='SELECTOR', md5='064250aa2a9ebe79633aa89575544dcb', lastModifyTime=1611683279275} 2021-01-27 01:47:59.276 INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[META_DATA], old: {group='META_DATA', md5='5f79d821e3b601330631a2d53294fb34', lastModifyTime=1611682979203}, updated: {group='META_DATA', md5='5f79d821e3b601330631a2d53294fb34', lastModifyTime=1611683279276} 2021-01-27 01:47:59.276 INFO 51481 --- [-long-polling-2] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh config success.启动之后对应的长轮询方法是 void doLongPolling(final HttpServletRequest request, final HttpServletResponse response) 这个方法,接到请求后,等60秒之后再去响应 bootstrap 发送的请求,对应的代码
public void run() { this.asyncTimeoutFuture = scheduler.schedule(() -> { clients.remove(LongPollingClient.this); List<ConfigGroupEnum> changedGroups = compareChangedGroup((HttpServletRequest) asyncContext.getRequest()); sendResponse(changedGroups); }, timeoutTime, TimeUnit.MILLISECONDS); clients.add(this); } -
bootstrap 项目里面数据同步的类是 HttpSyncDataService 主要同步的方法是 doFetchGroupConfig(),如果是 http 的话,是网关主动去拉取 admin 相关的数据
private void doFetchGroupConfig(final String server, final ConfigGroupEnum... groups) { StringBuilder params = new StringBuilder(); for (ConfigGroupEnum groupKey : groups) { params.append("groupKeys").append("=").append(groupKey.name()).append("&"); } String url = server + "/configs/fetch?" + StringUtils.removeEnd(params.toString(), "&"); log.info("request configs: [{}]", url); String json = null; try { json = this.httpClient.getForObject(url, String.class); } catch (RestClientException e) { String message = String.format("fetch config fail from server[%s], %s", url, e.getMessage()); log.warn(message); throw new SoulException(message, e); } // update local cache boolean updated = this.updateCacheWithJson(json); if (updated) { log.info("get latest configs: [{}]", json); return; } // not updated. it is likely that the current config server has not been updated yet. wait a moment. log.info("The config of the server[{}] has not been updated or is out of date. Wait for 30s to listen for changes again.", server); ThreadUtils.sleep(TimeUnit.SECONDS, 30); }从日志中可以看到 bootstrap 在启动时判断是不是 http 同步,如果是的话,会去 admin 抓取一次全量数据,成功会打印出下面的日志

之后去创建新线程去启动长轮询线程,创建代码见下图,长轮询实现看下面的代码。

如果同步失败,看重试的次数有没有到3次,如果没到3次,休眠5秒再次重试。如果重试等于3次,则休眠5分钟之后再去重试,如果5分钟之后再次失败,则重新再来,只要是长轮询线程没有停止public void run() { while (RUNNING.get()) { for (int time = 1; time <= retryTimes; time++) { try { doLongPolling(server); } catch (Exception e) { // print warnning log. if (time < retryTimes) { log.warn("Long polling failed, tried {} times, {} times left, will be suspended for a while! {}", time, retryTimes - time, e.getMessage()); ThreadUtils.sleep(TimeUnit.SECONDS, 5); continue; } // print error, then suspended for a while. log.error("Long polling failed, try again after 5 minutes!", e); ThreadUtils.sleep(TimeUnit.MINUTES, 5); } } } log.warn("Stop http long polling."); } -
如果数据有更新的话,会执行 HttpLongPollingDataChangedListener@DataChangeTask 这个线程类的 run 方法,这个方法会及时返回更新的数据
public void run() { for (Iterator<LongPollingClient> iter = clients.iterator(); iter.hasNext();) { LongPollingClient client = iter.next(); iter.remove(); client.sendResponse(Collections.singletonList(groupKey)); log.info("send response with the changed group,ip={}, group={}, changeTime={}", client.ip, groupKey, changeTime); } }打印的日志,但是打了断点死活不进去(还不知道是什么原因),到时候有空会整个图出来

总结
这篇总结了 websocket、http 的数据同步原理,其中 http 数据同步原理可能比较绕,后期会把这部分的图补上,方便快速了解其原理。如果有错误欢迎指出。

907

被折叠的 条评论
为什么被折叠?



