一、bootstrap端的流程
- 根据官方文档,添加依赖和打开配置项后,重启bootstrap
2021-01-26 00:37:35.791 INFO 39056 --- [ main] b.s.s.d.w.WebsocketSyncDataConfiguration : you use websocket sync soul data.......
2021-01-26 00:37:35.873 INFO 39056 --- [ main] o.d.s.p.s.d.w.WebsocketSyncDataService : websocket connection is successful.....
2021-01-26 00:37:35.876 INFO 39056 --- [ main] .s.s.b.s.s.d.h.HttpSyncDataConfiguration : you use http long pull sync soul data
看到如上日志,根据日志搜寻位置,找到starter(spring boot的核心之一starter,当然直接看也一样)
@Configuration
@ConditionalOnClass(HttpSyncDataService.class)
@ConditionalOnProperty(prefix = "soul.sync.http", name = "url")
@Slf4j
public class HttpSyncDataConfiguration {
//依赖加载HttpSyncDataService初始化
@Bean
public SyncDataService httpSyncDataService(final ObjectProvider<HttpConfig> httpConfig, final ObjectProvider<PluginDataSubscriber> pluginSubscriber,
final ObjectProvider<List<MetaDataSubscriber>> metaSubscribers, final ObjectProvider<List<AuthDataSubscriber>> authSubscribers) {
log.info("you use http long pull sync soul data");
return new HttpSyncDataService(Objects.requireNonNull(httpConfig.getIfAvailable()), Objects.requireNonNull(pluginSubscriber.getIfAvailable()),
metaSubscribers.getIfAvailable(Collections::emptyList), authSubscribers.getIfAvailable(Collections::emptyList));
}
// 初始化htt配置信息HttpConfig
@Bean
@ConfigurationProperties(prefix = "soul.sync.http")
public HttpConfig httpConfig() {
return new HttpConfig();
}
}
- 上一步中的初始化HttpSyncDataService,看一下初始化的参数
逐个进行分析
- this.httpConfig = httpConfig;//获取配置信息
- this.serverList = Lists.newArrayList(Splitter.on(",").split(httpConfig.getUrl()));//根据配置信息得到soul.sync.http的信息,如果是集群的话,可能是配多个?
- this.httpClient = createRestTemplate();//初始化httpclient,为发送http请求做准备
- 上一步的,核心初始化步骤this.factory = new DataRefreshFactory(pluginDataSubscriber, metaDataSubscribers, authDataSubscribers),一种类型的数据是一个组
- DataRefreshFactory的executor和cacheConfigData方法上打断点,观察调用链
依次是HTTPSyncDataService的
- updateCacheWithJson()
- doFetchGroupConfig()
- fetchGroupConfig()
- start()
- HttpSyncDataService构造函数初始化(就是函数的下一步,我这说了半天废话)
上面的调用链一步步的进行分析
- updateCacheWithJson()–跟新本地缓存,每次都是全量
- doFetchGroupConfig()–去admin去fetch所有的配置信息取了,获取第五步里的data
//获取数据+更新
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 {
// 发送请求,对应soul-admin里的/configs/fetch
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);
}
// 全量更新缓存
boolean updated = this.updateCacheWithJson(json);
// 如果没有更新,就变懒一点,睡一会30S,这次没更新,估计短时间也没有更新,感觉是某程序员的神逻辑
if (updated) {
log.info("get latest configs: [{}]", json);
return;
}
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);
}
- fetchGroupConfig()—根据admin获取配置信息
private void fetchGroupConfig(final ConfigGroupEnum... groups) throws SoulException {
//循环admin-http服务器,来源于配置,可以配多个应该
for (int index = 0; index < this.serverList.size(); index++) {
String server = serverList.get(index);
try {
this.doFetchGroupConfig(server, groups);
break;
} catch (SoulException e) {
// no available server, throw exception.
if (index >= serverList.size() - 1) {
throw e;
}
log.warn("fetch config fail, try another one: {}", serverList.get(index + 1));
}
}
}
- start()获取配置数据更新,然后启动一个HttpLongPollingTask任务
private void start() {
// 给RUNNING update为 true
if (RUNNING.compareAndSet(false, true)) {
// 拿到所有的配置数据
this.fetchGroupConfig(ConfigGroupEnum.values());
int threadSize = serverList.size();
this.executor = new ThreadPoolExecutor(threadSize, threadSize, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
SoulThreadFactory.create("http-long-polling", true));
// start long polling, each server creates a thread to listen for changes.
// 线程池执行一个叫HttpLongPollingTask的任务
this.serverList.forEach(server -> this.executor.execute(new HttpLongPollingTask(server)));
} else {
log.info("soul http long polling was started, executor=[{}]", executor);
}
}
- 看以下这个HttpLongPollingTask
class HttpLongPollingTask implements Runnable {
private String server;
private final int retryTimes = 3;
HttpLongPollingTask(final String server) {
this.server = server;
}
@Override
public void run() {
//start初始化里把RUNNING设置为ture,就开启了一个轮询
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.");
}
}
- 再继续看看doLongPolling(),看名字就是去admin端检测一下
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);
// 调用admin的/configs/listener
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);
}
//如果获取响应结果里有groupJson ,说明就有变化,需要立刻全量更新一下doFetchGroupConfig
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);
}
}
}
- 再看看admin的/configs/listener接口里的实现,也是doLongPolling,作用如下
public void doLongPolling(final HttpServletRequest request, final HttpServletResponse response) {
// 因为soul-web可能未收到某个配置变更的通知,因此MD5值可能不一致,则立即响应
// 没有直接比较具体的参数值,使用md5,这块有借鉴意义
List<ConfigGroupEnum> changedGroup = compareMD5(request);
String clientIp = getRemoteIp(request);
if (CollectionUtils.isNotEmpty(changedGroup)) {
this.generateResponse(response, changedGroup);
return;
}
// Servlet3.0异步响应http请求
final AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0L);
scheduler.execute(new LongPollingClient(asyncContext, clientIp, 60));
}
第10步和第11步就是当 soul-web 网关层接收到 http 响应信息之后,拉取变更信息(如果有变更的话),然后再次请求 soul-admin 的配置服务,如此反复循环。
二、小结
基本上到这里就大致通了
- 构造函数的时候,更新RUNNING为true
- 全量拉取一次数据
- 根据RUNNING为true,开启轮询任务
- doLongPolling根据http://localhost:9095/configs/listener调用admin的doLongPolling
- 根据响应,如果groupJson 发生了变化,再去全量同步一下
- 循环上述两步,做到检测-有变化立即全量更新
此外,中间还有一些优化逻辑