一、准备工作
-
1、打开soul-admin即控制台(后文用控制台代替)和 soul-bootstrap即网关服务(后文用网关服务代替)配置中的 http 数据同步配置
-
启动控制台和网关服务
二、http长轮训方式同步数据
1、通过控制台日志入手
学习一个成熟的项目我们可以从它的官方文档、单元测试入手,也可以从它的日志入手。长轮训,猜测是客户端有定时任务在不断的轮训服务端数据,即网关一直在轮训控制台数据,不断的更新本地缓存。
在启动网关服务的时候,可以看到一行网关日志
...HttpSyncDataService: request configs: [http://localhost:9095/configs/fetch?groupKeys=APP_AUTH&groupKeys=PLUGIN&groupKeys=RULE&groupKeys=SELECTOR&groupKeys=META_DATA]
通过请求 http://localhost:9095/configs/fetch?groupKeys=APP_AUTH&groupKeys=PLUGIN&groupKeys=RULE&groupKeys=SELECTOR&groupKeys=META_DATA 可以拿到全部配置。之后半天了,网关服务的日志也没再更新,这不科学,应该还有轮训请求,说明网关服务日志打印不全。
再来看一下控制台日志
不难看出来,每隔5分钟更新一下各类数据,让我们从日志中出现的类 HttpLongPollingDataChangedListener 入手
先看 被 afterPropertiesSet 方法调用的 HttpLongPollingDataChangedListener#afterInitialize 方法,也就是打印日志的来源方法。
InitializingBean接口为bean提供了属性初始化后的处理方法,它只包括
afterPropertiesSet方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法
@Override
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);
}
private void refreshLocalCache() {
this.updateAppAuthCache();
this.updatePluginCache();
this.updateRuleCache();
this.updateSelectorCache();
this.updateMetaDataCache();
}
可以看出来在初始化 HttpLongPollingDataChangedListener bean 的时候,创建了一个定时执行的线程,查库并写到本地缓存,然后呢?这只是写到控制台的 JVM 内存里,并没有发送给网关服务。
2、控制台端接收http请求
那些我们就看看有哪些方法 get 了这个缓存 Map 的数据,其中又一个 Controller 接口 /configs/listener。我们看一些Server层的执行方法 HttpLongPollingDataChangedListener#doLongPolling 有很多知识点。
public void doLongPolling(final HttpServletRequest request, final HttpServletResponse response) {
// compare group md5
List<ConfigGroupEnum> changedGroup = compareChangedGroup(request);
String clientIp = getRemoteIp(request);
// response immediately.
if (CollectionUtils.isNotEmpty(changedGroup)) {
this.generateResponse(response, changedGroup);
log.info("send response with the changed group, ip={}, group={}", clientIp, changedGroup);
return;
}
// listen for configuration changed.
final AsyncContext asyncContext = request.startAsync();
// AsyncContext.settimeout() does not timeout properly, so you have to control it yourself
asyncContext.setTimeout(0L);
// block client's thread.
scheduler.execute(new LongPollingClient(asyncContext, clientIp, HttpConstants.SERVER_MAX_HOLD_TIMEOUT));
}
首先发现执行逻辑是丢到线程池执行,而线程池的初始化是
this.scheduler = new ScheduledThreadPoolExecutor(1, SoulThreadFactory.create("long-polling", true));
其中 HttpLongPollingDataChangedListener#compareChangedGroup 调用 checkCacheDelayAndUpdate 方法,在checkCacheDelayAndUpdate 方法中还有锁,获取锁成功后会查库更新控制台本地缓存。
也就是有缓存更新异步返回查询结果,告诉网关服务有数据更新。
单个线程,那这异步的意义是什么呢?接着看线程 LongPollingClient 的 run 方法来揭晓答案
单线程有很多好处:当处理逻辑很快(一般是无IO操作),单线程的TPS也相当可以;处理逻辑可以串行,无锁、代码结构简单。
@Override
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);
}
总感觉这里怪怪的 clients.remove(LongPollingClient.this) 和 clients.add(this) 相当于并发执行,到底是先删除在增加,还是先增加在删除,二意性吧。 上面代码的 clients 的初始化是一个有界阻塞队列
this.clients = new ArrayBlockingQueue<>(1024);
LongPollingClient#run方法是将请求处理丢到线程后,再将 LongPollingClient 对象再丢到 clients 队列中,这是什么操作,像是把请求阻塞中,然后异步返回结果。
这里应该是用到了Tomcat 异步 Servlet 技术,Soul网关有太多的知识点的实践,太棒了。
有点吃力看起来,以后画一下这本门的线程模型图或者时序图,挺有意思的。我们从 clients 中的元素出列队代码线程DataChangeTask#run看起来,执行它的方法就是我们前两天看到的监听事件的执行方法。
@Override
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);
}
}
也就是当有事件到来时,更新控制台本地缓存,响应网关服务的请求,告诉有请求进来(在阻塞队列里)网关有数据更新了。
3、网关服务端发送http请求
看控制台逻辑看晕了,以后要画画图补上,下面我们根据控制台接口 /configers/listener 接口找到网关服务调用方法 HttpSyncDataService#doLongPolling
private void doLongPolling(final String server) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>(8);
for (ConfigGroupEnum group : ConfigGroupEnum.values()) {
ConfigData<?> cacheConfig = factory.cacheConfigData(group);
String value = String.join(",", cacheConfig.getMd5(), String.valueOf(cacheConfig.getLastModifyTime()));
params.put(group.name(), Lists.newArrayList(value));
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity httpEntity = new HttpEntity(params, headers);
String listenerUrl = server + "/configs/listener";
log.debug("request listener configs: [{}]", listenerUrl);
JsonArray groupJson = null;
try {
String json = this.httpClient.postForEntity(listenerUrl, httpEntity, String.class).getBody();
log.debug("listener result: [{}]", json);
groupJson = GSON.fromJson(json, JsonObject.class).getAsJsonArray("data");
} catch (RestClientException e) {
String message = String.format("listener configs fail, server:[%s], %s", server, e.getMessage());
throw new SoulException(message, e);
}
if (groupJson != null) {
// fetch group configuration async.
ConfigGroupEnum[] changedGroups = GSON.fromJson(groupJson, ConfigGroupEnum[].class);
if (ArrayUtils.isNotEmpty(changedGroups)) {
log.info("Group config changed: {}", Arrays.toString(changedGroups));
this.doFetchGroupConfig(server, changedGroups);
}
}
}
注意看这一行这个方法 this.doFetchGroupConfig(server, changedGroups),这说明 /configers/listener 接口只是告诉网关服务,数据有没有更新和哪些类型数据更新了,然后网关服务再 /configs/fetch 接口更新数据。
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);
}
/configs/fetch 接口眼熟吧,也就是网关服务刚启动时,调用的接口来获取控制台数据。然后我们再反过头去看控制台这个接口的代码
public ConfigData<?> fetchConfig(final ConfigGroupEnum groupKey) {
ConfigDataCache config = CACHE.get(groupKey.name());
switch (groupKey) {
case APP_AUTH:
List<AppAuthData> appAuthList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<AppAuthData>>() {
}.getType());
return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), appAuthList);
case PLUGIN:
List<PluginData> pluginList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<PluginData>>() {
}.getType());
return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), pluginList);
case RULE:
List<RuleData> ruleList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<RuleData>>() {
}.getType());
return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), ruleList);
case SELECTOR:
List<SelectorData> selectorList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<SelectorData>>() {
}.getType());
return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), selectorList);
case META_DATA:
List<MetaData> metaList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<MetaData>>() {
}.getType());
return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), metaList);
default:
throw new IllegalStateException("Unexpected groupKey: " + groupKey);
}
}
ConfigDataCache config = CACHE.get(groupKey.name()); 这不就是从控制台缓存 Map 中查数据吗。至于网关服务拿到数据怎么更新到网关本地缓存,昨天已经讲了,顺着今天网关服务调用/configs/fetch接口拿到数据往下看,应该不难。
今天逻辑有点服务,线程和锁有点巧妙,以后要多点时间画画图