简介
前面两篇,我们一起分别从soul-bootstarp端和soul-admin端分析,当选择了http数据同步方式时,启动时这两端分别做了什么,这一篇,我们对http同步方式做个总结,在全量数据更新以及管理端数据变化时,都做了什么事。
总体流程图
下面这个图,描述了http数据同步方式,全量数据和增量数据更新时,soul-admin和soul-bootstarp端分别做了哪些事情。
流程节点关键源码分析
soul-admin端:
启动过程
- 由于启动过程的代码分析在前面已经分析过,这里不再重复。
网关发送请求到后台时后台的处理
- ConfigController.listener: 作为Controller 层的方法, 提供 /configs/listener 路径供网关调用,如下代码所示:
@RestController
@RequestMapping("/configs")
public class ConfigController {
@PostMapping(value = "/listener")
public void listener(final HttpServletRequest request, final HttpServletResponse response) {
longPollingListener.doLongPolling(request, response);
}
}
- HttpLongPollingDataChangedListener.doLongPolling: 比对数据是否发生变化, 有变化则返回响应给网关, 无变化则 hold 住请求 60s,如下代码所示:
public class HttpLongPollingDataChangedListener extends AbstractDataChangedListener {
public void doLongPolling(final HttpServletRequest request, final HttpServletResponse response) {
// 比较数据是否更新的方法, 非常重要但这里先不分析, 会放到细节模块讲
List<ConfigGroupEnum> changedGroup = compareChangedGroup(request);
String clientIp = getRemoteIp(request);
// 有变化的信息就直接构造响应信息并返回
if (CollectionUtils.isNotEmpty(changedGroup)) {
this.generateResponse(response, changedGroup);
log.info("send response with the changed group, ip={}, group={}", clientIp, changedGroup);
return;
}
// 将请求转换为异步方式, 并且不限制超时时间, 这里就 hold 住请求
final AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0L);
// 另起线程执行 LongPollingClient run方法
scheduler.execute(new LongPollingClient(asyncContext, clientIp, HttpConstants.SERVER_MAX_HOLD_TIMEOUT));
}
}
- LongPollingClient.run: 将这次的请求加入内存缓存 (一个 BlockingQueue 阻塞队列), 并启动一个延时线程, 做释放此次请求的工作,代码如下:
class LongPollingClient implements Runnable {
@Override
public void run() {
// 延时线程 60s 后执行
this.asyncTimeoutFuture = scheduler.schedule(() -> {
// 内存缓存中去除该对象
clients.remove(LongPollingClient.this);
// 得到变化的数据类型
List<ConfigGroupEnum> changedGroups = compareChangedGroup((HttpServletRequest) asyncContext.getRequest());
// 释放请求
sendResponse(changedGroups);
}, timeoutTime, TimeUnit.MILLISECONDS);
// 自定义请求对象添加内存缓存
clients.add(this);
}
}
后台数据变动时长轮询的处理流程
-
DataChangedEventDispatcher: 处理数据信息变动并通知监听器
-
AbstractDataChangedListener: 更新维护的数据信息缓存, 并调用子类需实现的 afterPluginChanged() 方法,代码如下:
public abstract class AbstractDataChangedListener implements DataChangedListener, InitializingBean {
@Override
public void onPluginChanged(final List<PluginData> changed, final DataEventTypeEnum eventType) {
if (CollectionUtils.isEmpty(changed)) {
return;
}
this.updatePluginCache();
this.afterPluginChanged(changed, eventType);
}
}
- HttpLongPollingDataChangedListener: 开启线程通知各个维护的请求, 并传入变动事件类型,代码如下:
public class HttpLongPollingDataChangedListener extends AbstractDataChangedListener {
@Override
protected void afterPluginChanged(final List<PluginData> changed, final DataEventTypeEnum eventType) {
scheduler.execute(new DataChangeTask(ConfigGroupEnum.PLUGIN));
}
}
- DataChangeTask: 循环所有持有的请求, 通知他们数据变动的类型信息, 并剔除维护的队列,代码如下:
class DataChangeTask implements Runnable {
@Override
public void run() {
for (Iterator<LongPollingClient> iter = clients.iterator(); iter.hasNext();) {
LongPollingClient client = iter.next();
iter.remove();
// 调用 LongPollingClient 的 sendResponse() 释放请求
client.sendResponse(Collections.singletonList(groupKey));
log.info("send response with the changed group,ip={}, group={}, changeTime={}", client.ip, groupKey, changeTime);
}
}
}
- LongPollingClient#sendResponse: 取消之前开启的延迟定时任务, 生成事件类型变更响应, 并释放请求,代码如下:
class LongPollingClient implements Runnable {
void sendResponse(final List<ConfigGroupEnum> changedGroups) {
// 取消延迟任务, 对应 run() 方法的开启延迟任务
if (null != asyncTimeoutFuture) {
asyncTimeoutFuture.cancel(false);
}
// 生成对应事件类型变动的响应信息
generateResponse((HttpServletResponse) asyncContext.getResponse(), changedGroups);
// 释放请求
asyncContext.complete();
}
}
soul-bootstrap端
网关启动拉取数据
- HttpSyncDataService#start: 网关启动时, HttpSyncDataService 初始化会调用 start() 方法, 该方法会调用后台拉取数据, 并开启多个线程进行轮询监听,代码如下:
public class HttpSyncDataService implements SyncDataService, AutoCloseable {
private void start() {
// 防止二次调用的CAS操作
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));
this.serverList.forEach(server -> this.executor.execute(new HttpLongPollingTask(server)));
} else {
log.info("soul http long polling was started, executor=[{}]", executor);
}
}
}
- HttpSyncDataService#fetchGroupConfig: 作用仅是根据数据类型, 循环多次调用拉取数据方法(针对同一个后台会请求多次, 每次拉取某一种数据类型的信息), 这里的数据类型指的是 plugin、rule、selector 等,如下:
private void fetchGroupConfig(final ConfigGroupEnum... groups) throws SoulException {
for (int index = 0; index < this.serverList.size(); index++) {
String server = serverList.get(index);
try {
// 根据传入的数据类型枚举, 多次调用拉取数据方法
this.doFetchGroupConfig(server, groups);
break;
} catch (SoulException e) {
if (index >= serverList.size() - 1) {
throw e;
}
log.warn("fetch config fail, try another one: {}", serverList.get(index + 1));
}
}
}
- HttpSyncDataService#doFetchGroupConfig: 请求后台的 /configs/fetch 接口, 拿到某个类型的数据, 并更新缓存. 更新缓存前会检测是否变动, 如果变动则结束, 数据未发生变动则睡眠30s,如下:
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);
}
// 修改缓存信息
boolean updated = this.updateCacheWithJson(json);
// 判断是否修改, 修改则直接结束
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);
}
- HttpSyncDataService#updateCacheWithJson: 取出响应信息中的 data , 即变化的数据信息, 传给数据刷新工厂 DataRefreshFactory,如下:
private DataRefreshFactory factory;
public HttpSyncDataService(...){
this.factory = new DataRefreshFactory(pluginDataSubscriber, metaDataSubscribers, authDataSubscribers);
}
private boolean updateCacheWithJson(final String json) {
JsonObject jsonObject = GSON.fromJson(json, JsonObject.class);
JsonObject data = jsonObject.getAsJsonObject("data");
return factory.executor(data);
}
- DataRefreshFactory#executor: 将数据发送给各类数据刷新类,如下:
public final class DataRefreshFactory {
private static final EnumMap<ConfigGroupEnum, DataRefresh> ENUM_MAP = new EnumMap<>(ConfigGroupEnum.class);
public DataRefreshFactory(final PluginDataSubscriber pluginDataSubscriber,
final List<MetaDataSubscriber> metaDataSubscribers,
final List<AuthDataSubscriber> authDataSubscribers) {
// 注入各类型订阅器到 MAP 中
ENUM_MAP.put(ConfigGroupEnum.PLUGIN, new PluginDataRefresh(pluginDataSubscriber));
ENUM_MAP.put(ConfigGroupEnum.SELECTOR, new SelectorDataRefresh(pluginDataSubscriber));
ENUM_MAP.put(ConfigGroupEnum.RULE, new RuleDataRefresh(pluginDataSubscriber));
ENUM_MAP.put(ConfigGroupEnum.APP_AUTH, new AppAuthDataRefresh(authDataSubscribers));
ENUM_MAP.put(ConfigGroupEnum.META_DATA, new MetaDataRefresh(metaDataSubscribers));
}
public boolean executor(final JsonObject data) {
final boolean[] success = {false};
// Tureen: 所有数据类型的 DataRefresh 全调用
ENUM_MAP.values().parallelStream().forEach(dataRefresh -> success[0] = dataRefresh.refresh(data));
return success[0];
}
}
- AbstractDataRefresh#refresh: 判断是否要更新缓存, 若更新则调用各类型的 refresh() 方法,如下:
@Override
public Boolean refresh(final JsonObject data) {
boolean updated = false;
JsonObject jsonObject = convert(data);
if (null != jsonObject) {
ConfigData<T> result = fromJson(jsonObject);
if (this.updateCacheIfNeed(result)) {
updated = true;
// Turren: 调用 refresh
refresh(result.getData());
}
}
return updated;
}
- PluginDataRefresh#refresh: 调用 plugin 的订阅器, 接下来会通知所有扩展插件的相关事件变动,如下:
@Override
protected void refresh(final List<PluginData> data) {
if (CollectionUtils.isEmpty(data)) {
log.info("clear all plugin data cache");
pluginDataSubscriber.refreshPluginDataAll();
} else {
pluginDataSubscriber.refreshPluginDataAll();
// Turren: http同步, 调用插件数据订阅器
data.forEach(pluginDataSubscriber::onSubscribe);
}
}
网关轮询监听变化
-
HttpSyncDataService.start: 启动线程执行 HttpLongPollingTask 这个 Runnable
-
HttpLongPollingTask.run: 开启循环调用轮询方法,代码如下:
@Override
public void run() {
while (RUNNING.get()) {
for (int time = 1; time <= retryTimes; time++) {
try {
doLongPolling(server);
} catch (Exception e) {
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;
}
log.error("Long polling failed, try again after 5 minutes!", e);
ThreadUtils.sleep(TimeUnit.MINUTES, 5);
}
}
}
}
- HttpLongPollingTask#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));
}
// 构建 http 请求信息
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();
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) {
ConfigGroupEnum[] changedGroups = GSON.fromJson(groupJson, ConfigGroupEnum[].class);
if (ArrayUtils.isNotEmpty(changedGroups)) {
log.info("Group config changed: {}", Arrays.toString(changedGroups));
// 拉取后台对应类型的数据
this.doFetchGroupConfig(server, changedGroups);
}
}
}
总结
通过分析,http数据同步方式和websocket比较,流程上会比较复杂,涉及轮询监控请求、比对信息是否改变、判断是否要更新缓存等,而websocket的方式,基本上就是一旦订阅了消息,收到消息的主动推送即可立刻更新,这是http方式和websocket方式的区别。