学习目标:
http长轮询数据同步原理
学习内容:
1.如何感知数据的变化
2.数据是否实时同步
学习时间:
2020年1月26号
学习产出:
1.admin数据同步
ConfigController 这个controller里面有一个HttpLongPollingDataChangedListener属性。
HttpLongPollingDataChangedListener继承自AbstractDataChangedListener。而AbstractDataChangedListener实现了DataChangedListener接口和InitializingBean接口;
DataChangedListener监听所有类型数据的变化
public interface DataChangedListener {
/**
* invoke this method when AppAuth was received.
*
* @param changed the changed
* @param eventType the event type
*/
default void onAppAuthChanged(List<AppAuthData> changed, DataEventTypeEnum eventType) {
}
/**
* invoke this method when Plugin was received.
*
* @param changed the changed
* @param eventType the event type
*/
default void onPluginChanged(List<PluginData> changed, DataEventTypeEnum eventType) {
}
/**
* invoke this method when Selector was received.
*
* @param changed the changed
* @param eventType the event type
*/
default void onSelectorChanged(List<SelectorData> changed, DataEventTypeEnum eventType) {
}
/**
* On meta data changed.
*
* @param changed the changed
* @param eventType the event type
*/
default void onMetaDataChanged(List<MetaData> changed, DataEventTypeEnum eventType) {
}
/**
* invoke this method when Rule was received.
*
* @param changed the changed
* @param eventType the event type
*/
default void onRuleChanged(List<RuleData> changed, DataEventTypeEnum eventType) {
}
}
2.DataChangedListener 监听到数据变化首先更新admin本地缓存,然后触发一个任务:DataChangeTask
例如:
protected void afterSelectorChanged(final List<SelectorData> changed, final DataEventTypeEnum eventType) {
scheduler.execute(new DataChangeTask(ConfigGroupEnum.SELECTOR));
}
class DataChangeTask implements Runnable {
/**
* The Group where the data has changed.
*/
private final ConfigGroupEnum groupKey;
/**
* The Change time.
*/
private final long changeTime = System.currentTimeMillis();
/**
* Instantiates a new Data change task.
*
* @param groupKey the group key
*/
DataChangeTask(final ConfigGroupEnum groupKey) {
this.groupKey = 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);
}
}
}
clients存储内容后面会提及。
HttpLongPollingDataChangedListener的构造函数里面,初始化了一个大小为1024的ArrayBlockingQueue(这个queue是用来存LongPollingClient的,后面会提到),还初始化了一个定时线程池,以及一个http的同步策略对象httpSyncProperties。
/**
* 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;
}
上面提到过,AbstractDataChangedListener实现了InitializingBean接口,而InitializingBean有一个afterPropertiesSet方法。看下AbstractDataChangedListener里面对于afterPropertiesSet的具体实现:
@Override
public final void afterPropertiesSet() {
updateAppAuthCache();
updatePluginCache();
updateRuleCache();
updateSelectorCache();
updateMetaDataCache();
afterInitialize();
}
afterInitialize 会开启一个任务5分钟刷新一次本地缓存
@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);
}
在http长轮询数据同步机制里面,soul-admin管理后台的/configs/fetch接口和/configs/listener接口会被soul网关调用。
soul-admin的/configs/fetch接口。它的主要逻辑,就是从soul-admin的内存中取出缓存的相应group的配置数据,并返回
soul-admin的/configs/listener接口,它调用了HttpLongPollingDataChangedListener的doLongPolling方法。
doLongPolling主要逻辑:
- 如果group发生更新,返回response;
- 如果没有,会利用Servlet的异步响应
使用线程池立即执行LongPollingClient这个Runnable的run方法
在这个run方法里面,会将this加入到上文提到的ArrayBlockingQueue里面
会使用定时线程池,在60s之后,执行里面的具体逻辑。具体逻辑为:
从ArrayBlockingQueue里面移除this
将request里面的md5和lastModifyTime,和 当前内存中的比较,判断哪些group的数据有更新。将group的名字作为resposne返回。这里再次比较一下,是为了防止在这60s以内数据有变动。
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));
}
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(final AsyncContext ac, final String ip, final long timeoutTime) {
this.asyncContext = ac;
this.ip = ip;
this.timeoutTime = timeoutTime;
}
@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-admin发生配置变化的时候,还不够实时。因为网关可能不止一台机器,可能是个集群,集群里的每台机器都会和admin建立http长轮询,后面的http请求会被前面的block住,所以如果每次都等前面一个请求60s的话,当数据有变动的时候后面的请求很晚才能知道。所以当管理员进行一些改动的时候,希望所有与admin建立连接的每台网关服务器都能够感知到数据变动。
这个就是前面提到的clients的作用,数据发生变化时,DataChangeTask 会实时相应所有的clients请求;
class DataChangeTask implements Runnable {
/**
* The Group where the data has changed.
*/
private final ConfigGroupEnum groupKey;
/**
* The Change time.
*/
private final long changeTime = System.currentTimeMillis();
/**
* Instantiates a new Data change task.
*
* @param groupKey the group key
*/
DataChangeTask(final ConfigGroupEnum groupKey) {
this.groupKey = 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);
}
}
}