Soul网关源码解析(八):http长连接同步数据
Soul网关源码解析(八):http长连接同步数据Http长连接同步数据数据同步配置admin启动时的同步处理bootstrap启动时的同步处理小结参考
Http长连接同步数据
数据同步配置
与前面的websocket,zookeeper相同,http长连接需要admin与bootstrap两边都要配置,admin的配置
soul : sync: http: enabled: true
bootstrap的配置
soul : sync: http: url : http://localhost:9095
admin启动时的同步处理
和前面的websocket,zookeeper一样,都是从DataChangedEventDispatcher类的afterPropertiesSet方法进入后构造Http长连接的监听器对象HttpLongPollingDataChangedListener,该对象初始化一个阻塞阻塞队列,用于存放多个长连接的客户端对象信息,当服务端(admin)超过一定时间没有数据更改,则向客户端返回空数据;如果在给定时间数据有变动,则响应更改的Group数据给到客户端。另外初始化了定时线程池,用于定时检查数据变动以及变动后响应服务端。
#DataSyncConfiguration @Configuration @ConditionalOnProperty(name = "soul.sync.http.enabled", havingValue = "true") @EnableConfigurationProperties(HttpSyncProperties.class) static class HttpLongPollingListener { @Bean @ConditionalOnMissingBean(HttpLongPollingDataChangedListener.class) public HttpLongPollingDataChangedListener httpLongPollingDataChangedListener(final HttpSyncProperties httpSyncProperties) { return new HttpLongPollingDataChangedListener(httpSyncProperties); } } #HttpLongPollingDataChangedListener public HttpLongPollingDataChangedListener(final HttpSyncProperties httpSyncProperties) { this.clients = new ArrayBlockingQueue<>(1024); this.scheduler = new ScheduledThreadPoolExecutor(1, SoulThreadFactory.create("long-polling", true)); this.httpSyncProperties = httpSyncProperties; } // 初始化之后,执行完一次本地缓存刷新任务,默认等5分钟再刷新一次,这个值可以设置 // soul.sync.http.refreshInterval = xxx 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); } // 真正在数据变动时,给客户端响应,但如果没有变动,则阻塞客户端请求一定时间 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)); } // 内部类 class LongPollingClient implements Runnable { ... // 客户端线程任务提交后执行的内容,根据上下文判断Group内数据是否变动, 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); } }
bootstrap启动时的同步处理
当配置中含有soul.sync.http.url,则匹配到HttpSyncDataConfiguration类,然后初始化HttpSyncDataService对象,初始化过程中创建了熟悉的RestTemplate,接着先get一把服务端的数据,然后创建线程池监听服务端的数据变化,并在变化时更新
#HttpSyncDataConfiguration @Configuration @ConditionalOnClass(HttpSyncDataService.class) @ConditionalOnProperty(prefix = "soul.sync.http", name = "url") @Slf4j public class 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)); } ... } #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(); } // 这个非常熟悉,创建 Httpclient的template private RestTemplate createRestTemplate() { OkHttp3ClientHttpRequestFactory factory = new OkHttp3ClientHttpRequestFactory(); factory.setConnectTimeout((int) this.connectionTimeout.toMillis()); // 读取超时90s factory.setReadTimeout((int) HttpConstants.CLIENT_POLLING_READ_TIMEOUT); return new RestTemplate(factory); } //这里先是获取Group数据,然后开启线程池执行对服务端数据监听 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); } } 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) { // no available server, throw exception. if (index >= serverList.size() - 1) { throw e; } log.warn("fetch config fail, try another one: {}", serverList.get(index + 1)); } } } 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("&"); } //http://localhost:9095/configs/fetch?groupKeys=APP_AUTH&groupKeys=PLUGIN&groupKeys=RULE&groupKeys=SELECTOR&groupKeys=META_DATA String url = server + "/configs/fetch?" + StringUtils.removeEnd(params.toString(), "&"); log.info("request configs: [{}]", url); String json = null; try { // 这里向服务端(admin)发起请求获取,走到AbstractDataChangedListener.fetchConfig方法 // 这里如果发生超时,bootstrap程序会直接结束 /* 这里返回的是各个数据的md5值与更新时间,如下所示 "META_DATA":{ "md5":"d751713988987e9331980363e24189ce", "lastModifyTime":1611324450079, "data":[ ] */ 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 // 这里调用各个数据定义的检查更新的函数,当md5与时间均有变动,才认定为需要更新 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); } // 线程池里的线程干的事情就是发请求到服务端,获取数据,服务端有变化就返回,没变化就让这个请求阻塞一段时间,要么到达服务端设置的阻塞值,要么自己连接超时。 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); 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); } 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); } } } #Admin端的AbstractDataChangedListener public ConfigData<?> fetchConfig(final ConfigGroupEnum groupKey) { ConfigDataCache config = CACHE.get(groupKey.name()); switch (groupKey) { case APP_AUTH: List<AppAuthData> appAuthList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<AppAuthData>>() { }.getType()); return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), appAuthList); case PLUGIN: List<PluginData> pluginList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<PluginData>>() { }.getType()); return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), pluginList); case RULE: List<RuleData> ruleList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<RuleData>>() { }.getType()); return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), ruleList); case SELECTOR: List<SelectorData> selectorList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<SelectorData>>() { }.getType()); return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), selectorList); case META_DATA: List<MetaData> metaList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<MetaData>>() { }.getType()); return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), metaList); default: throw new IllegalStateException("Unexpected groupKey: " + groupKey); } }
小结
本小结首先介绍了http长连接同步数据的配置方式,接着admin启动时同步数据的流程,然后介绍了bootstrap启动时的同步处理,先get服务端数据,然后起线程池监听同步,自此结束。希望能帮到你,初识soul这样一个极致性能的网关项目。