【Soul源码阅读】12.soul-admin 与 soul-bootstrap 同步机制之 http 长轮询解析(上)

目录

1.前情回顾

2.配置

2.1 soul-admin

2.2 soul-bootstrap

3.启动

3.1 启动 soul-admin

3.2 启动 soul-bootstrap

3.2.1 数据更新器工厂初始化 

3.2.2 从 soul-admin 获取所有配置数据

3.2.3 判断数据是否发生改变

4. 记录个错误

4.1 由于 sofa 插件没开启的错误

4.2 不开 zk 网关不能启动的错误


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 的锅。

猜你喜欢

转载自blog.csdn.net/hellboy0621/article/details/113195535