Soul源码分析--soul-admin数据同步(1)

Soul源码分析--soul-admin数据同步之websocket、http

  • webosocket 数据同步
  • Http 数据同步
  • 总结

之前写过一篇 Soul源码分析–soul-admin源码分析 里面介绍了数据同步的方式和数据同步的种类,现在来深入分析一下各种数据同步的原理,包括 websocket、http、zookeeper、nacos 数据同步。这里我们先可以参考 官网的文档,大概看一下原理,然后根据代码来分析。
这一篇只会分析 websocket、http,下篇会分析 zookeeper、nacos,点这里查看 下一篇 。

webosocket 数据同步

  1. 启动 soul-admin 项目时,会根据数据源加载相应的数据,这里用 mysql 数据源为例,启动会执行 init 包下面的 LocalDataSourceLoader 类的方法,判断 bean 是否是数据源且初始化设置是否为 true,然后会执行相应初始化数据的方法,控制台会打印相应执行的 sql 文件名称(这里要注意的是,每次都会去执行初始化,所以如果用在生产上的话,应该把 initEnable 这个属性在第一次执行完之后,设置成 false)。
    private void execute(final Connection conn) throws Exception {
        ScriptRunner runner = new ScriptRunner(conn);
        runner.setLogWriter(null);
        Resources.setCharset(StandardCharsets.UTF_8);
        Reader read = Resources.getResourceAsReader(dataBaseProperties.getInitScript());
        log.info("execute soul schema sql: {}", dataBaseProperties.getInitScript());
        runner.runScript(read);
        runner.closeConnection();
        conn.close();
    }

在这里插入图片描述

  1. 如果在网关后台选择器配置中配置了一个没启动服务的地址,点保存后,后台会去执行检查,如果没找到相应的服务,这个配置则不生效。
    Soul源码分析--soul-admin数据同步(1)_第1张图片
    点保存后,控制台打出来的日志,可以看出来,检查地址失败了(不知道什么原因,日志这里打了断点没能进去调度)。
    在这里插入图片描述
    再去看看页面刚刚加的配置,也被清空了。
    Soul源码分析--soul-admin数据同步(1)_第2张图片
    如果配置的地址不对,后台会有线程去检查这个地址,如果地址不对,再刷新一遍数据,这也是为什么 Soul-WebSocketClient 里面的这段代码会执行两次
    private void handleResult(final String result) {
        WebsocketData websocketData = GsonUtils.getInstance().fromJson(result, WebsocketData.class);
        ConfigGroupEnum groupEnum = ConfigGroupEnum.acquireByName(websocketData.getGroupType());
        String eventType = websocketData.getEventType();
        String json = GsonUtils.getInstance().toJson(websocketData.getData());
        websocketDataHandler.executor(groupEnum, json, eventType);
    }
  1. 注意:如果只用 websocket 或者 http 同步数据,在 bootstrap 项目里面需要把下面这个 maven 依赖给注释掉
    Soul源码分析--soul-admin数据同步(1)_第3张图片
    因为这里面会疯狂的连接 zookeeper ,为了测试方便,先把这个注释掉
    在这里插入图片描述

Http 数据同步

  1. 老规矩,把 yml 文件里面的 websocket 改成 http 然后启动 soul-admin 项目,首先还是会先去执行初始化脚本,这个跟 websocket 的一样。接下来会进到 UpstreamCheckService 这个类里面来,这里有点不一样的地方是,http 数据同步时,刚启动时会触发这个类里面的 check(final String selectorName, final List upstreamList) 方法,用 websocket 好像是不会的。调试时发现 http 的 handle 是有值的,至于什么时候写进去的,后面再研究,页面也会显示这个值
    Soul源码分析--soul-admin数据同步(1)_第4张图片

  2. 有值之后就开始执行 setup() 方法里的定时任务了,这里创建了一个定时线程数量为cpu核数,间隔10秒后开始执行,每10秒执行一次的定时任务。

    if (check) {
                new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), SoulThreadFactory.create("scheduled-upstream-task", false))
                        .scheduleWithFixedDelay(this::scheduled, 10, scheduledTime, TimeUnit.SECONDS);
            }
    

    重点是来看这个 check 方法,检查 divide 中配置的地址是否有效,如果有效再次检查是否启用这个配置,如果没启用则先启用这个配置,再加到全局的 Map 里面。这里的 “check the url=XXX is fail” 是不是很眼熟,没错!之前配置错的时候会执行这一句,然后把数据清除掉,最后调用更新方法更新数据库中的记录,和发布到事件中去,让相应的监听处理相应的事件,然后把数据同步到 bootstrap 中

        private void check(final String selectorName, final List upstreamList) {
            List successList = Lists.newArrayListWithCapacity(upstreamList.size());
            for (DivideUpstream divideUpstream : upstreamList) {
                final boolean pass = UpstreamCheckUtils.checkUrl(divideUpstream.getUpstreamUrl());
                if (pass) {
                    if (!divideUpstream.isStatus()) {
                        divideUpstream.setTimestamp(System.currentTimeMillis());
                        divideUpstream.setStatus(true);
                        log.info("UpstreamCacheManager check success the url: {}, host: {} ", divideUpstream.getUpstreamUrl(), divideUpstream.getUpstreamHost());
                    }
                    successList.add(divideUpstream);
                } else {
                    divideUpstream.setStatus(false);
                    log.error("check the url={} is fail ", divideUpstream.getUpstreamUrl());
                }
            }
            if (successList.size() == upstreamList.size()) {
                return;
            }
            if (successList.size() > 0) {
                UPSTREAM_MAP.put(selectorName, successList);
                updateSelectorHandler(selectorName, successList);
            } else {
                UPSTREAM_MAP.remove(selectorName);
                updateSelectorHandler(selectorName, null);
            }
        }
    
  3. soul-bootstrap 启动之后,会和 admin 进行数据同步,admin 相关的类是 HttpLongPollingDataChangedListener,会先执行 afterInitialize() 方法。这个方法的作用是每隔5分钟去刷新一下数据和数据的 md5 值

        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);
        }
    

    所以控制台会每隔5分钟出来下面的日志,下面表示正常缓存更新成功

    2021-01-27 01:42:59.138  INFO 51481 --- [-long-polling-2] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh config start.
    2021-01-27 01:42:59.150  INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener    : update config cache[APP_AUTH], old: {group='APP_AUTH', md5='d751713988987e9331980363e24189ce', lastModifyTime=1611682679083}, updated: {group='APP_AUTH', md5='d751713988987e9331980363e24189ce', lastModifyTime=1611682979150}
    2021-01-27 01:42:59.197  INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener    : update config cache[RULE], old: {group='RULE', md5='82f8a3c07c416d980ef14518d465e3aa', lastModifyTime=1611682679129}, updated: {group='RULE', md5='82f8a3c07c416d980ef14518d465e3aa', lastModifyTime=1611682979197}
    2021-01-27 01:42:59.202  INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener    : update config cache[SELECTOR], old: {group='SELECTOR', md5='064250aa2a9ebe79633aa89575544dcb', lastModifyTime=1611682679134}, updated: {group='SELECTOR', md5='064250aa2a9ebe79633aa89575544dcb', lastModifyTime=1611682979202}
    2021-01-27 01:42:59.203  INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener    : update config cache[META_DATA], old: {group='META_DATA', md5='5f79d821e3b601330631a2d53294fb34', lastModifyTime=1611682679135}, updated: {group='META_DATA', md5='5f79d821e3b601330631a2d53294fb34', lastModifyTime=1611682979203}
    2021-01-27 01:42:59.203  INFO 51481 --- [-long-polling-2] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh config success.
    2021-01-27 01:47:59.210  INFO 51481 --- [-long-polling-2] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh config start.
    2021-01-27 01:47:59.225  INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener    : update config cache[APP_AUTH], old: {group='APP_AUTH', md5='d751713988987e9331980363e24189ce', lastModifyTime=1611682979150}, updated: {group='APP_AUTH', md5='d751713988987e9331980363e24189ce', lastModifyTime=1611683279225}
    2021-01-27 01:47:59.226  INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener    : update config cache[PLUGIN], old: {group='PLUGIN', md5='0298afdf3cc5338833c99f44fb88f1e9', lastModifyTime=1611682979151}, updated: {group='PLUGIN', md5='0298afdf3cc5338833c99f44fb88f1e9', lastModifyTime=1611683279226}
    2021-01-27 01:47:59.271  INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener    : update config cache[RULE], old: {group='RULE', md5='82f8a3c07c416d980ef14518d465e3aa', lastModifyTime=1611682979197}, updated: {group='RULE', md5='82f8a3c07c416d980ef14518d465e3aa', lastModifyTime=1611683279271}
    2021-01-27 01:47:59.275  INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener    : update config cache[SELECTOR], old: {group='SELECTOR', md5='064250aa2a9ebe79633aa89575544dcb', lastModifyTime=1611682979202}, updated: {group='SELECTOR', md5='064250aa2a9ebe79633aa89575544dcb', lastModifyTime=1611683279275}
    2021-01-27 01:47:59.276  INFO 51481 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener    : update config cache[META_DATA], old: {group='META_DATA', md5='5f79d821e3b601330631a2d53294fb34', lastModifyTime=1611682979203}, updated: {group='META_DATA', md5='5f79d821e3b601330631a2d53294fb34', lastModifyTime=1611683279276}
    2021-01-27 01:47:59.276  INFO 51481 --- [-long-polling-2] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh config success.
    

    启动之后对应的长轮询方法是 void doLongPolling(final HttpServletRequest request, final HttpServletResponse response) 这个方法,接到请求后,等60秒之后再去响应 bootstrap 发送的请求,对应的代码

    public void run() {
            this.asyncTimeoutFuture = scheduler.schedule(() -> {
                clients.remove(LongPollingClient.this);
                List changedGroups = compareChangedGroup((HttpServletRequest) asyncContext.getRequest());
                sendResponse(changedGroups);
            }, timeoutTime, TimeUnit.MILLISECONDS);
            clients.add(this);
        }
    
  4. bootstrap 项目里面数据同步的类是 HttpSyncDataService 主要同步的方法是 doFetchGroupConfig(),如果是 http 的话,是网关主动去拉取 admin 相关的数据

        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);
        }
    

    从日志中可以看到 bootstrap 在启动时判断是不是 http 同步,如果是的话,会去 admin 抓取一次全量数据,成功会打印出下面的日志
    https://blog.csdn.net/yutanbo123/article/details/113104859
    之后去创建新线程去启动长轮询线程,创建代码见下图,长轮询实现看下面的代码。
    在这里插入图片描述
    如果同步失败,看重试的次数有没有到3次,如果没到3次,休眠5秒再次重试。如果重试等于3次,则休眠5分钟之后再去重试,如果5分钟之后再次失败,则重新再来,只要是长轮询线程没有停止

    public void run() {
                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.");
            }
    
  5. 如果数据有更新的话,会执行 HttpLongPollingDataChangedListener@DataChangeTask 这个线程类的 run 方法,这个方法会及时返回更新的数据

    public void run() {
                for (Iterator 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);
                }
            }
    

    打印的日志,但是打了断点死活不进去(还不知道是什么原因),到时候有空会整个图出来 在这里插入图片描述

总结

这篇总结了 websocket、http 的数据同步原理,其中 http 数据同步原理可能比较绕,后期会把这部分的图补上,方便快速了解其原理。如果有错误欢迎指出。

你可能感兴趣的:(源码分析,java)