soul网关系列(十):soul-admin与网关数据同步之http长轮询-bootstrap端

一、bootstrap端的流程

  1. 根据官方文档,添加依赖和打开配置项后,重启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();
    }
}
  1. 上一步中的初始化HttpSyncDataService,看一下初始化的参数
    在这里插入图片描述
    逐个进行分析
  • this.httpConfig = httpConfig;//获取配置信息
  • this.serverList = Lists.newArrayList(Splitter.on(",").split(httpConfig.getUrl()));//根据配置信息得到soul.sync.http的信息,如果是集群的话,可能是配多个?
  • this.httpClient = createRestTemplate();//初始化httpclient,为发送http请求做准备
  1. 上一步的,核心初始化步骤this.factory = new DataRefreshFactory(pluginDataSubscriber, metaDataSubscribers, authDataSubscribers),一种类型的数据是一个组
    在这里插入图片描述
  2. DataRefreshFactory的executor和cacheConfigData方法上打断点,观察调用链
    在这里插入图片描述依次是HTTPSyncDataService的
  • updateCacheWithJson()
  • doFetchGroupConfig()
  • fetchGroupConfig()
  • start()
  • HttpSyncDataService构造函数初始化(就是函数的下一步,我这说了半天废话)

上面的调用链一步步的进行分析

  1. updateCacheWithJson()–跟新本地缓存,每次都是全量
    在这里插入图片描述
  2. 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);
    }
  1. 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));
            }
        }
    }
  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);
        }
    }
  1. 看以下这个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.");
        }
    }
  1. 再继续看看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);
            }
        }
    }
  1. 再看看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 发生了变化,再去全量同步一下
  • 循环上述两步,做到检测-有变化立即全量更新

此外,中间还有一些优化逻辑

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值