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();
}
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);
}
老规矩,把 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 upstreamList) {
List 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 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 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 数据同步原理可能比较绕,后期会把这部分的图补上,方便快速了解其原理。如果有错误欢迎指出。