一、目的
使用http长轮询同步数据到网关并了解其运作原理
二、内容
2.1 背景
Soul目前支持四种数据同步模式:HTTP长轮询、Zookeeper、WebSocket、Nacos。今天我们主要是针对http长轮询同步策略进行分析。
Soul 借鉴了 Apollo
、Nacos
的设计思想,取其精华,自己实现了 http
长轮询数据同步功能。注意,这里并非传统的 ajax 长轮询
http长轮询同步流程如图所示:
soul-web 网关请求 admin 的配置服务,读取超时时间为 90s,意味着网关层请求配置服务最多会等待 90s,这样便于 admin 配置服务及时响应变更数据,从而实现准实时推送;
修改配置文件,选择http长轮询作为数据同步策略
依次启动soul-admin和soul-bootstrap服务
下面进入源码分析
http 请求到达 sou-admin 之后,并非立马响应数据,而是利用 Servlet3.0 的异步机制,异步响应数据。首先,将长轮询请求任务 LongPollingClient
扔到 BlocingQueue
中,并且开启调度任务,60s 后执行,这样做的目的是 60s 后将该长轮询请求移除队列,即便是这段时间内没有发生配置数据变更。因为即便是没有配置变更,也得让网关知道,总不能让其干等吧,而且网关请求配置服务时,也有 90s 的超时时间。
HttpLongPollingDataChangedListener继承了AbstractDataChangedListener,AbstractDataChangedListener实现了DataChangedListener接口(可以参考websocket数据同步流程:https://blog.csdn.net/qq_38314459/article/details/112917717)
接下来一起看一下HttpLongPollingDataChangedListener里面的doLongPolling方法,这个方法主要是如果配置数据发生更改,则会立即响应更改的组信息。否则,将阻止客户端的请求线程,直到数据发生任何更改或达到指定的超时
public void doLongPolling(final HttpServletRequest request, final HttpServletResponse response) {
// 因为soul-web可能未收到某个配置变更的通知,因此MD5值可能不一致,则立即响应
List<ConfigGroupEnum> changedGroup = compareChangedGroup(request);
String clientIp = getRemoteIp(request);
// 立刻响应
if (CollectionUtils.isNotEmpty(changedGroup)) {
this.generateResponse(response, changedGroup);
log.info("send response with the changed group, ip={}, group={}", clientIp, changedGroup);
return;
}
// 监听配置信息改变。Servlet3.0异步响应http请求
final AsyncContext asyncContext = request.startAsync();
// 设置超时时间
asyncContext.setTimeout(0L);
// 阻塞客户端的线程
scheduler.execute(new LongPollingClient(asyncContext, clientIp, HttpConstants.SERVER_MAX_HOLD_TIMEOUT));
}
class LongPollingClient implements Runnable {
/**
* The Async context.
*/
private final AsyncContext asyncContext;
...
@Override
public void run() {
//加入定时任务,如果60s之内没有配置变更,则60s后执行,响应http请求
this.asyncTimeoutFuture = scheduler.schedule(() -> {
// clients是阻塞队列,保存了来自soul-web的请求信息
clients.remove(LongPollingClient.this);
List<ConfigGroupEnum> changedGroups = compareChangedGroup((HttpServletRequest) asyncContext.getRequest());
sendResponse(changedGroups);
}, timeoutTime, TimeUnit.MILLISECONDS);
clients.add(this);
}
/**
* Send response.
*
* @param changedGroups the changed groups
*/
void sendResponse(final List<ConfigGroupEnum> changedGroups) {
// cancel scheduler
if (null != asyncTimeoutFuture) {
asyncTimeoutFuture.cancel(false);
}
generateResponse((HttpServletResponse) asyncContext.getResponse(), changedGroups);
asyncContext.complete();
}
}
如果这段时间内,管理员变更了配置数据,此时,会挨个移除队列中的长轮询请求,并响应数据,告知是哪个 Group 的数据发生了变更(我们将插件、规则、流量配置、用户配置数据分成不同的组)。网关收到响应信息之后,只知道是哪个 Group 发生了配置变更,还需要再次请求该 Group 的配置数据
//soul-admin发生了配置变更,挨个将队列中的请求移除,并予以响应
class DataChangeTask implements Runnable {
/**
* The Group where the data has changed.
*/
private final ConfigGroupEnum groupKey;
...
@Override
public void run() {
for (Iterator<LongPollingClient> 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);
}
}
}
当 soul-web
网关层接收到 http 响应信息之后,拉取变更信息(如果有变更的话),然后再次请求 soul-admin
的配置服务,如此反复循环;
然后在soul-bootstrap模块里面,HttpSyncDataService来处理网关数据同步和更新。
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);
}
// 更新本地缓存
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);
}
三、总结
今天我们一起学习了Soul网关http长轮询数据同步流程,自己去实践一下会更有体会。