目录
1.前情回顾
紧接着前两天的 zookeeper,今天来看下 http 长轮询。
2.配置
数据同步策略官网链接 https://dromara.org/zh-cn/docs/soul/user-dataSync.html
2.1 soul-admin
修改 application.yml 配置文件,打开注释的代码:
soul:
sync:
http:
enabled: true
对应 Bean,默认 enabled 就是 true,只要配置 soul.sync.http 即可。
/**
* the http sync strategy properties.
* @author huangxiaofeng
*/
@Getter
@Setter
@ConfigurationProperties(prefix = "soul.sync.http")
public class HttpSyncProperties {
/**
* Whether enabled http sync strategy, default: true.
*/
private boolean enabled = true;
/**
* Periodically refresh the config data interval from the database, default: 5 minutes.
*/
private Duration refreshInterval = Duration.ofMinutes(5);
}
这里会把配置传入到配置类中,加载 HttpLongPollingDataChangedListener:
@Configuration
public class DataSyncConfiguration {
/**
* http long polling.
*/
@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);
}
}
...
}
2.2 soul-bootstrap
修改 application-local.yml 配置文件
soul:
sync:
http:
url : http://localhost:9095
#url: 配置成你的 soul-admin的 ip与端口地址,多个admin集群环境请使用(,)分隔。
对应 Bean
/**
* The type Http config.
*/
@Data
public class HttpConfig {
private String url;
private Integer delayTime;
private Integer connectionTimeout;
}
使用 Spring starter 机制加载的配置
@Configuration
@ConditionalOnClass(HttpSyncDataService.class)
@ConditionalOnProperty(prefix = "soul.sync.http", name = "url")
@Slf4j
public class HttpSyncDataConfiguration {
/**
* Http sync data service.
*
* @param httpConfig the http config
* @param pluginSubscriber the plugin subscriber
* @param metaSubscribers the meta subscribers
* @param authSubscribers the auth subscribers
* @return the sync data service
*/
@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));
}
/**
* Http config http config.
*
* @return the http config
*/
@Bean
@ConfigurationProperties(prefix = "soul.sync.http")
public HttpConfig httpConfig() {
return new HttpConfig();
}
}
在 pom.xml 文件中 引入以下依赖(代码中已经引入了):
<!--soul data sync start use http-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-sync-data-http</artifactId>
<version>${project.version}</version>
</dependency>
3.启动
3.1 启动 soul-admin
3.2 启动 soul-bootstrap
这里在2.2小节中的 HttpSyncDataConfiguration 中的 httpSyncDataService 方法上打上断点,会调用 HttpSyncDataService 的构造方法:
// HttpSyncDataService.java
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()));
// 初始化 RestTemplate
this.httpClient = createRestTemplate();
// 开启 HTTP 长连接线程
this.start();
}
3.2.1 数据更新器工厂初始化
/**
* The type Data refresh factory.
*/
public final class DataRefreshFactory {
private static final EnumMap<ConfigGroupEnum, DataRefresh> ENUM_MAP = new EnumMap<>(ConfigGroupEnum.class);
/**
* Instantiates a new Data refresh factory.
*
* @param pluginDataSubscriber the plugin data subscriber
* @param metaDataSubscribers the meta data subscribers
* @param authDataSubscribers the auth data subscribers
*/
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));
}
...
}
在构造方法里初始化了5种数据更新器,这个后面判断数据是否变化时会用到。
仔细看下类继承关系及各自覆写方法,又是模板方法(Boolean : refresh(JsonObject)),又是策略模式,6得飞起。
3.2.2 从 soul-admin 获取所有配置数据
// HttpSyncDataService.java
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();
// 这里初始化线程池,核心线程数为 soul-admin 个数,即1个线程负责与1个 soul-admin同步
this.executor = new ThreadPoolExecutor(threadSize, threadSize, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
// 自定义线程池工厂,名字设定好,方便 jstack 时按线程名称查找
SoulThreadFactory.create("http-long-polling", true));
// start long polling, each server creates a thread to listen for changes.
// 开启 http 长轮询,每一个 soul-admin 创建一个线程去监听变化
this.serverList.forEach(server -> this.executor.execute(new HttpLongPollingTask(server)));
} else {
log.info("soul http long polling was started, executor=[{}]", executor);
}
}
// HttpSyncDataService.java
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 {
// 通过 RestTemplate 调用接口获取所有数据,内容有点儿多,就不贴出来了
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
// 判断数据是否发生改变,这是下面分析的重点
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);
// 当前线程休眠30秒
ThreadUtils.sleep(TimeUnit.SECONDS, 30);
}
3.2.3 判断数据是否发生改变
// HttpSyncDataService.java
/**
* update local cache.
* @param json the response from config server.
* @return true: the local cache was updated. false: not updated.
*/
private boolean updateCacheWithJson(final String json) {
JsonObject jsonObject = GSON.fromJson(json, JsonObject.class);
JsonObject data = jsonObject.getAsJsonObject("data");
// if the config cache will be updated?
// 这里调用数据更新器工厂方法
return factory.executor(data);
}
// DataRefreshFactory.java
public boolean executor(final JsonObject data) {
final boolean[] success = {false};
ENUM_MAP.values().parallelStream().forEach(dataRefresh -> success[0] = dataRefresh.refresh(data));
return success[0];
}
这里使用了 3.2.1 小节中分析的 ENUM_MAP,而且这里使用了并行流,5种不同的数据并行执行,缩短数据处理的时间。
这里还定义了一个布尔值的数组,用来存放数据是否发生变化的结果,只要这5种数据中,有1种变化了,就是变化了;只有都没有变化,才是没有变化。
这里迎来了一个重点,每种数据更新器的不同判断方式,也就是上面说的模板方法:dataRefresh.refresh(data)
// AbstractDataRefresh.java
@Override
public Boolean refresh(final JsonObject data) {
boolean updated = false;
// 从所有数据中分别获取各自的数据
JsonObject jsonObject = convert(data);
if (null != jsonObject) {
// 把 json 数据转成 Java Bean
ConfigData<T> result = fromJson(jsonObject);
if (this.updateCacheIfNeed(result)) {
updated = true;
refresh(result.getData());
}
}
return updated;
}
这里以 PluginDataRefresh 为例走一遍流程,其他的小伙伴自己看下吧。
// PluginDataRefresh.java
@Override
protected JsonObject convert(final JsonObject data) {
return data.getAsJsonObject(ConfigGroupEnum.PLUGIN.name());
}
@Override
protected ConfigData<PluginData> fromJson(final JsonObject data) {
return GSON.fromJson(data, new TypeToken<ConfigData<PluginData>>() {
}.getType());
}
@Override
protected boolean updateCacheIfNeed(final ConfigData<PluginData> result) {
return updateCacheIfNeed(result, ConfigGroupEnum.PLUGIN);
}
@Override
protected void refresh(final List<PluginData> data) {
if (CollectionUtils.isEmpty(data)) {
log.info("clear all plugin data cache");
pluginDataSubscriber.refreshPluginDataAll();
} else {
// 这里将 BaseDataCache#PLUGIN_MAP 缓存清空了
pluginDataSubscriber.refreshPluginDataAll();
// 重新订阅
data.forEach(pluginDataSubscriber::onSubscribe);
}
}
// AbstractDataRefresh.java
/**
* If the MD5 values are different and the last update time of the old data is less than
* the last update time of the new data, the configuration cache is considered to have been changed.
* 当 MD5 值不同,或者旧数据的最后更新时间较最新数据靠前,就认为配置的缓存变化了
*
* @param newVal the lasted config
* @param groupEnum the group enum
* @return true : if need update
*/
protected boolean updateCacheIfNeed(final ConfigData<T> newVal, final ConfigGroupEnum groupEnum) {
// first init cache
if (GROUP_CACHE.putIfAbsent(groupEnum, newVal) == null) {
return true;
}
ResultHolder holder = new ResultHolder(false);
GROUP_CACHE.merge(groupEnum, newVal, (oldVal, value) -> {
// must compare the last update time
if (!StringUtils.equals(oldVal.getMd5(), newVal.getMd5()) && oldVal.getLastModifyTime() < newVal.getLastModifyTime()) {
log.info("update {} config: {}", groupEnum, newVal);
holder.result = true;
return newVal;
}
log.info("Get the same config, the [{}] config cache will not be updated, md5:{}", groupEnum, oldVal.getMd5());
return oldVal;
});
return holder.result;
}
fromJson 方法通过 GSON,把 json 数据转成 Java Bean
这个先分析到这里,明天继续。
4. 记录个错误
4.1 由于 sofa 插件没开启的错误
这里有个坑,记录一下。
昨天在使用 zookeeper 时,使用 sofa 插件作为例子在 web 页面开启关闭来着,如果把 sofa 关闭了,以下代码不能进入 if 语句,也就意味着不能初始化 sofa 插件,最终造成 sofa-rpc 包里 for 循环时 NPE。
// SofaPluginDataHandler.java
@Override
public void handlerPlugin(final PluginData pluginData) {
// 如果插件关闭 getEnabled 为false 不能进入
if (null != pluginData && pluginData.getEnabled()) {
SofaRegisterConfig sofaRegisterConfig = GsonUtils.getInstance().fromJson(pluginData.getConfig(), SofaRegisterConfig.class);
SofaRegisterConfig exist = Singleton.INST.get(SofaRegisterConfig.class);
if (Objects.isNull(sofaRegisterConfig)) {
return;
}
if (Objects.isNull(exist) || !sofaRegisterConfig.equals(exist)) {
// If it is null, initialize it
// 导致这里的初始化不能执行
ApplicationConfigCache.getInstance().init(sofaRegisterConfig);
ApplicationConfigCache.getInstance().invalidateAll();
}
Singleton.INST.single(SofaRegisterConfig.class, sofaRegisterConfig);
}
}
// ApplicationConfigCache.java
/**
* Init.
*
* @param sofaRegisterConfig the sofa register config
*/
public void init(final SofaRegisterConfig sofaRegisterConfig) {
if (applicationConfig == null) {
applicationConfig = new ApplicationConfig();
applicationConfig.setAppId("soul_proxy");
applicationConfig.setAppName("soul_proxy");
}
// 这里的 registryConfig 始终为 null
if (registryConfig == null) {
registryConfig = new RegistryConfig();
registryConfig.setProtocol(sofaRegisterConfig.getProtocol());
registryConfig.setId("soul_proxy");
registryConfig.setRegister(false);
registryConfig.setAddress(sofaRegisterConfig.getRegister());
}
}
/**
* Build reference config.
*
* @param metaData the meta data
* @return the reference config
*/
public ConsumerConfig<GenericService> build(final MetaData metaData) {
ConsumerConfig<GenericService> reference = new ConsumerConfig<>();
reference.setGeneric(true);
reference.setApplication(applicationConfig);
// 后面再调用到这里时,sofa-rpc 包里的 AbstractInterfaceConfig 的 registry 数组里有1个值,就是 null
reference.setRegistry(registryConfig);
reference.setInterfaceId(metaData.getServiceName());
reference.setProtocol(RpcConstants.PROTOCOL_TYPE_BOLT);
reference.setInvokeType(RpcConstants.INVOKER_TYPE_CALLBACK);
reference.setRepeatedReferLimit(-1);
String rpcExt = metaData.getRpcExt();
SofaParamExtInfo sofaParamExtInfo = GsonUtils.getInstance().fromJson(rpcExt, SofaParamExtInfo.class);
if (Objects.nonNull(sofaParamExtInfo)) {
if (StringUtils.isNoneBlank(sofaParamExtInfo.getLoadbalance())) {
final String loadBalance = sofaParamExtInfo.getLoadbalance();
reference.setLoadBalancer(buildLoadBalanceName(loadBalance));
}
Optional.ofNullable(sofaParamExtInfo.getTimeout()).ifPresent(reference::setTimeout);
Optional.ofNullable(sofaParamExtInfo.getRetries()).ifPresent(reference::setRetries);
}
Object obj = reference.refer();
if (obj != null) {
log.info("init sofa reference success there meteData is :{}", metaData.toString());
cache.put(metaData.getPath(), reference);
}
return reference;
}
// com.alipay.sofa.rpc.client.router.MeshRouter.java
@Override
public boolean needToLoad(ConsumerBootstrap consumerBootstrap) {
ConsumerConfig consumerConfig = consumerBootstrap.getConsumerConfig();
// 不是直连,且从注册中心订阅配置
final boolean isDirect = StringUtils.isNotBlank(consumerConfig.getDirectUrl());
final List<RegistryConfig> registrys = consumerConfig.getRegistry();
boolean isMesh = false;
if (registrys != null) {
for (RegistryConfig registry : registrys) {
// 这里遍历出来的是1个 null,报 NPE
if (registry.getProtocol().equalsIgnoreCase(RpcConstants.REGISTRY_PROTOCOL_MESH)) {
isMesh = true;
break;
}
}
}
return !isDirect && isMesh;
}
在 soul-admin web 页面,把 sofa 插件开启。
4.2 不开 zk 网关不能启动的错误
不启动 zk,网关一直连接不上zk,早晨不能正常启动。这个错误还在定位,明天继续努力吧,太难了。
Caused by: com.alipay.sofa.rpc.core.exception.SofaRpcRuntimeException: RPC-010060014: 注册 consumer 到 [ZookeeperRegistry] 失败!
看样子还是 sofa 的锅。