Soul网关源码分析-http数据同步(三)

276 阅读6分钟

简介

前面两篇,我们一起分别从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方式的区别。