【高性能网关soul学习】8. soul 数据同步之http长轮询
本文目标:介绍 soul-admin 与 soul-web 之间通过 Http 进行同步的细节
配置http进行数据同步
- 网关服务引入依赖
- 同时需要注释掉 soul-spring-boot-starter-sync-data-websocket 依赖 和 zk依赖
<!--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>
- yaml配置为进行http同步
# 网关服务配置
soul :
sync:
zookeeper:
url: localhost:2181
sessionTimeout: 5000
connectionTimeout: 2000
#url: 配置成你的zk地址,集群环境请使用(,)分隔
# soul-admin 配置,只启用zk进行数据同步
soul:
sync:
websocket:
enabled: false
zookeeper:
url: localhost:2181
sessionTimeout: 5000
connectionTimeout: 2000
启动服务 soul-admin 服务和 soul-bootstrap 服务和 测试的业务服务
- soul-bootstrap 中的 控制台日志:
you use http long pull sync soul data
request configs: [http://localhost:9095/configs/fetch?groupKeys=APP_AUTH&groupKeys=PLUGIN&groupKeys=RULE&groupKeys=SELECTOR&groupKeys=META_DATA]
http 数据同步启动流程
private void start() {
// It could be initialized multiple times, so you need to control that.
if (RUNNING.compareAndSet(false, true)) {
// fetch all group configs.(app_auth\plugin\rule\selector\meta_data 等五种数据类型)
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 DataRefreshFactory(final PluginDataSubscriber pluginDataSubscriber,
final List<MetaDataSubscriber> metaDataSubscribers,
final List<AuthDataSubscriber> authDataSubscribers) {
ENUM_MAP.put(ConfigGroupEnum.PLUGIN, new PluginDataRefresh(pluginDataSubscriber));
ENUM_MAP.put(ConfigGroupEnum.SELECTOR, new SelectorDataRefresh(pluginDataSubscriber));
ENUM_MAP.put(ConfigGroupEnum.RULE, new RuleDataRefresh(pluginDataSubscriber));
ENUM_MAP.put(ConfigGroupEnum.APP_AUTH, new AppAuthDataRefresh(authDataSubscribers));
ENUM_MAP.put(ConfigGroupEnum.META_DATA, new MetaDataRefresh(metaDataSubscribers));
}
// 将请求获取到的数据写入缓存
public boolean executor(final JsonObject data) {
final boolean[] success = {false};
// ENUM_MAP中存放的是对于各种类型 DataSubscriber 的封装,因此逻辑和之前大致相似,通过 DataSubscriber 写入缓存
ENUM_MAP.values().parallelStream().forEach(dataRefresh -> success[0] = dataRefresh.refresh(data));
return success[0];
}
http 数据同步 长轮询机制
soul-bootstrap 端 HttpLongPollingTask
- 核心方法就是一个 doLongPolling(server)
- 本质上就是请求 String listenerUrl = server + “/configs/listener”; 然后等待返回
- 时间相关的配置,如果一直出现异常会重试3次,然后休眠5分钟
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.");
}
}
soul-admin 端 类 HttpLongPollingDataChangedListener
负责处理长轮询
- 比较 soul-bootstrap 端的数据的md5的值 和 最后修改时间 比较得出数据是否变更
- 如果数据有变更,则直接返回
- 如果数据没有变更,则创建一个 LongPollingClient 悬挂 60s,等待 scheduler 运行该Runnable,重新判断是否有数据变更,不管有没有,都直接返回结果
/**
* 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 and lastModifyTime
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 {
@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);
}
}
总结:
- 介绍了 soul 关于 http 进行数据同步的相关配置
- 介绍了 soul-admin 关于 http 数据同步的流程
- 启动时全量 fetch 拉取数据
- 运行期间使用 http 长轮询方式确保 soul-admin 和 soul-bootstrap 之间的数据同步不超过60s