soul源码阅读 soul数据同步之http长轮询

启动项目

修改两个配置文件,配置成htpp长轮询。不清楚的话可以先看我的第一篇

启动的时候得注意,先得启动soul-admin,然后再启动soul-bootstrap,要不然会报服务找不到。

源码分析

HttpLongPollingDataChangedListener这个类看名字就在知道是http长轮询数据改变的监听器,查看该类的uml图发现如图3所示。

图3

 在调试过程中发现每隔一分钟就会调用HttpLongPollingDataChangedListener.compareChangedGroup这个方法。

private List<ConfigGroupEnum> compareChangedGroup(final HttpServletRequest request) {
    List<ConfigGroupEnum> changedGroup = new ArrayList<>(4);
    for (ConfigGroupEnum group : ConfigGroupEnum.values()) {
        // md5,lastModifyTime
        String[] params = StringUtils.split(request.getParameter(group.name()), ',');
        if (params == null || params.length != 2) {
            throw new SoulException("group param invalid:" + request.getParameter(group.name()));
        }
        String clientMd5 = params[0];
        long clientModifyTime = NumberUtils.toLong(params[1]);
        ConfigDataCache serverCache = CACHE.get(group.name());
        // do check.
        if (this.checkCacheDelayAndUpdate(serverCache, clientMd5, clientModifyTime)) {
            changedGroup.add(group);
        }
    }
    return changedGroup;
}

经过代码查阅发现LongPollingClient实现了Runnable的run接口,在run接口里使用的调度器,默认是60秒执行一次。

@Override
public void run() {
    this.asyncTimeoutFuture = scheduler.schedule(() -> {
        clients.remove(LongPollingClient.this);
        List<ConfigGroupEnum> changedGroups = compareChangedGroup((HttpServletRequest) asyncContext.getRequest());
        sendResponse(changedGroups);
    }, timeoutTime, TimeUnit.MILLISECONDS);
    clients.add(this);
}

追溯代码发现在HttpLongPollingDataChangedListener.doLongPolling

/**
     * If the configuration data changes, the group information for the change is immediately responded.
     * Otherwise, the client's request thread is blocked until any data changes or the specified timeout is reached.
     *
     * @param request  the request
     * @param response the response
     */
    public void doLongPolling(final HttpServletRequest request, final HttpServletResponse response) {

        // compare group md5
        List<ConfigGroupEnum> changedGroup = compareChangedGroup(request);
        String clientIp = getRemoteIp(request);

        // response immediately.
        if (CollectionUtils.isNotEmpty(changedGroup)) {
            this.generateResponse(response, changedGroup);
            log.info("send response with the changed group, ip={}, group={}", clientIp, changedGroup);
            return;
        }

        // listen for configuration changed.
        final AsyncContext asyncContext = request.startAsync();

        // AsyncContext.settimeout() does not timeout properly, so you have to control it yourself
        asyncContext.setTimeout(0L);

        // block client's thread.
        scheduler.execute(new LongPollingClient(asyncContext, clientIp, HttpConstants.SERVER_MAX_HOLD_TIMEOUT));
    }

其中方法中有这样一行代码这里就是60秒执行一次的入口。

scheduler.execute(new LongPollingClient(asyncContext, clientIp, HttpConstants.SERVER_MAX_HOLD_TIMEOUT));

还发现每隔5分钟就会同步数据库的数据,通过看代码可以得知这里使用了ScheduledExecutorService定时的调用refreshLocalCache更新数据。

@Override
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);
}
private void refreshLocalCache() {
    this.updateAppAuthCache();
    this.updatePluginCache();
    this.updateRuleCache();
    this.updateSelectorCache();
    this.updateMetaDataCache();
}

溯源的时候发现了HttpLongPollingDataChangedListener在soul启动的时候会执行以下代码,其中scheduler是单线程且是守护线程。

/**
 * Instantiates a new Http long polling data changed listener.
 * @param httpSyncProperties the HttpSyncProperties
 */
public HttpLongPollingDataChangedListener(final HttpSyncProperties httpSyncProperties) {
    this.clients = new ArrayBlockingQueue<>(1024);
    this.scheduler = new ScheduledThreadPoolExecutor(1,
            SoulThreadFactory.create("long-polling", true));
    this.httpSyncProperties = httpSyncProperties;
}

先启动soul-admin 然后启动 soul-bootstrap的时候会发现调用了/cofigs/listener接口

@ConditionalOnBean(HttpLongPollingDataChangedListener.class)
@RestController
@RequestMapping("/configs")
@Slf4j
public class ConfigController {

    @Resource
    private HttpLongPollingDataChangedListener longPollingListener;

    /**
     * Listener.
     *
     * @param request  the request
     * @param response the response
     */
    @PostMapping(value = "/listener")
    public void listener(final HttpServletRequest request, final HttpServletResponse response) {
        longPollingListener.doLongPolling(request, response);
    }

}

为什么会调用这个接口,那使用“/configs/listener”查询会发现在HttpSyncDataService.doLongPolling使用了httpclient发送了一个post请求。查看HttpSyncDataService.doLongPolling的调用方,发现是HttpLongPollingTask这个线程去调度的。看样子是会重试3次,每次失败就睡5秒如果3次失败的话就睡5分钟。但哪里才是60秒调度入口呢?

class HttpLongPollingTask implements Runnable {

    private String server;

    private final int retryTimes = 3;

    HttpLongPollingTask(final String server) {
        this.server = server;
    }

    @Override
    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.");
    }
}

直到看到这里,谜底解开。这里用了一个AtomicBoolean防止多线程争用,然后使用调度器每60秒调用一次。

private void start() {
        // It could be initialized multiple times, so you need to control that.
        if (RUNNING.compareAndSet(false, true)) {
            // fetch all group configs.
            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.
            this.serverList.forEach(server -> this.executor.execute(new HttpLongPollingTask(server)));
        } else {
            log.info("soul http long polling was started, executor=[{}]", executor);
        }
    }

而start的入口是HttpSyncDataService构造函数的时候就已经调用了

public HttpSyncDataService(final HttpConfig httpConfig, final PluginDataSubscriber pluginDataSubscriber,
                           final List<MetaDataSubscriber> metaDataSubscribers, final List<AuthDataSubscriber> authDataSubscribers) {
    this.factory = new DataRefreshFactory(pluginDataSubscriber, metaDataSubscribers, authDataSubscribers);
    this.httpConfig = httpConfig;
    this.serverList = Lists.newArrayList(Splitter.on(",").split(httpConfig.getUrl()));
    this.httpClient = createRestTemplate();
    this.start();
}

这个猜想应该是托管给spring容器了,果不其然,在HttpSyncDataConfiguration使用了构造器注入

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

综上,http长轮询一直都有守护线程去调度接口进行数据更新,不仅有主动调用,也有被动调用方式。为什么不建议使用这种方式,看官网的解释是如下

   1.http长轮询使得网关很轻量,时效性略低。

   2.其根据分组key来拉取,如果数据量过大,过多,会有一定的影响。 什么意思呢?就是一个组下面的一个小地方更改,会拉取整个的组数据。

   3.在soul-admin 集群时候,可能会有bug。

其中第二点是compareChangedGroup方法,查看源码的话确实如此,会对整个group进行比较修改。

猜你喜欢

转载自blog.csdn.net/a511310132/article/details/113021436