http长轮询
Soul 借鉴了 Apollo、Nacos 的设计思想,取其精华,自己实现了 http 长轮询数据同步功能。注意,这里并非传统的 ajax长轮询!
http 长轮询机制如下所示,soul-web 网关请求 admin 的配置服务,读取超时时间90s,意味着网关层请求配置服务最多会等待 90s,这样便于 admin 配置服务及时响应变更数据,从而实现准实时推送。
http 请求到达 sou-admin 之后,并非立马响应数据,而是利用 Servlet3.0的异步机制,异步响应数据。首先,将长轮询请求任务 LongPollingClient 扔到 BlocingQueue中,并且开启调度任务,60s 后执行,这样做的目的是 60s后将该长轮询请求移除队列,即便是这段时间内没有发生配置数据变更。因为即便是没有配置变更,也得让网关知道,总不能让其干等吧,而且网关请求配置服务时,也有90s 的超时时间。
这是官方对于http长轮询的解释,下面我们结合这段解释和代码实际来看一下。
启动后台和网关
-
网关配置(记得重启)
-
首先在 pom.xml 文件中 引入以下依赖:
<!--soul data sync start use http--> <dependency> <groupId>org.dromara</groupId> <artifactId>soul-spring-boot-starter-sync-data-http</artifactId> <version>${last.version}</version> </dependency>
-
在 springboot的 yml 文件中进行如下配置:
soul : sync: http: url: http://localhost:9095 #url: 配置成你的 soul-admin的 ip与端口地址,多个admin集群环境请使用(,)分隔。
-
-
soul-admin 配置, 或在 soul-admin 启动参数中设置
--soul.sync.http=''
,然后重启服务。soul: sync: http: enabled: true
代码调试
- 启动完成后,我们找到
soul-sync-data-http
下面的HttpSyncDataService
,可以看出我们这个类加载后会找到我们配置的http url,针对于每个url启动一个HttpLongPollingTask
的任务线程。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(); } 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); } }
- 线程任务的实现方法可以看出这个是在一直循环执行的。
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."); }
- 跟着走进去看,会发现在调用了
doLongPolling
方法里的这一段的时候,会卡住很长时间,而这里访问的是localhost:9095/configs/listener
。
String json = this.httpClient.postForEntity(listenerUrl, httpEntity, String.class).getBody();
-
进入到这个方法里面看,这边是使用了定时任务去执行
LongPollingClient
的方法@PostMapping(value = "/listener") public void listener(final HttpServletRequest request, final HttpServletResponse response) { longPollingListener.doLongPolling(request, 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)); }
-
LongPollingClient
这里面的操作就是将这次的任务移除列表,并且将数据发送出去,然后再次往任务列表中添加一个任务。通过这种循环往复的执行,会一直保持监听的状态。@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); }
小结
这次的代码没看懂,只是跟着debug走了一遭。
网关这边对每一个url开启一个线程,线程中通过后台的一个listener循环拉去数据,但是每次数据没有变化,所有这个任务只是空跑(目前看下来是这样的)。
当实际在后台改动数据的时候,会主动触发我们网关的数据更新方法。
后续全部看明白后会再更新。。