Apollo一次不自动更新问题排查

Apollo一次不自动更新问题排查

描述

内部系统在Apollo指定了一个私有Namespace,然后通过ApolloConfigChangeListener进行监听,但是运行一段时候后,发现动态修改配置后,程序值有时会修改,有时会失效。

排查

java 复制代码
@ApolloConfigChangeListener("EMPLOYEE_POST_RANK.json")
private synchronized void employeePostRankConfigChange(ConfigChangeEvent changeEvent) {
    ConfigFile configFile = ConfigService.getConfigFile(EMPLOYEE_POST_RANK_NAMESPACE, ConfigFileFormat.JSON);
    formatJson(configFile.getContent(), EMPLOYEE_POST_RANK_NAMESPACE);
}

通过排查发现是在ApolloConfigChangeListener监听中并没有直接使用ConfigChangeEvent中的值,而是通过ConfigService.getConfigFile再去获取了配置的值,但是由于使用ApolloConfigChangeListener注解后,Apollo会生成一个监听类通过长轮训动态获取配置变化,如果在使用ConfigService.getConfigFile就会再次创建一个新的监听类,这样会存在两个监听类,同时监听一个配置变更,所以就会导致配置随机到一个监听类上,从而导致配置更新变成随机了。

java 复制代码
```
@ApolloConfigChangeListener("EMPLOYEE_POST_RANK.json")
private synchronized void employeePostRankConfigChange(ConfigChangeEvent changeEvent) {
    formatJson(changeEvent.getChange(Content).getNewValue(), EMPLOYEE_POST_RANK_NAMESPACE);
}
```

经过修改后直接获取ConfigChangeEvent中的getNewValue就可以解决。

下面通过源码分析一下排查过程。

源码分析

1、第一步ApolloConfigChangeListener注解的扫描

1.1、后置处理器ApolloProcessor#postProcessBeforeInitialization方法反射查找Bean的所有方法,然后调用其子类ApolloAnnotationProcessor#processApolloConfigChangeListener方法,会判断当前方法是否添加ApolloConfigChangeListener注解。

ApolloAnnotationProcessor继承ApolloProcessor,ApolloProcessor继承BeanPostProcessor,是一个Bean的后置处理器,在EnableApolloConfig中通过Import导入ApolloConfigRegistrar,ApolloConfigRegistrar内部会通过JDK SPI加载ApolloConfigRegistrarHelper,然后调用registerBeanDefinitions方法将ApolloAnnotationProcessor注入到容器

1.2、获取注解的value,value中的值就是Apollo中配置的Namespace,通过ConfigService.getConfig(resolvedNamespace)创建了一个DefaultConfig类,并且会添加相应的ChangeListener。

1.3、getConfig通过ConfigFactory#create创建DefaultConfig类时,会调用createRemoteConfigRepository方法创建一个RemoteConfigRepository,RemoteConfigRepository#trySync()中获取本地配置,在通过loadApolloConfig拼接服务器地址通过Get请求同步最新配置,同时RemoteConfigRepository静态构造器中创建了一个线程为1的线程池,在构造器中调用schedulePeriodicRefresh会定时5分钟调用trySync()拉取配置,scheduleLongPollingRefresh()方法就是开启长连接,会将namespace和创建的RemoteConfigRepository存放到m_longPollNamespaces Map中。

1.4、长连接分析,在RemoteConfigLongPollService#doLongPollingRefresh中会根据url http://178.16.182.102:11022/notifications/v2?cluster=default&appId=hr-org-service&ip=172.27.2.52&notifications=%5B%7B%22namespaceName%22%3A%22EMPLOYEE_POST_RANK.json%22%2C%22notificationId%22%3A4338%7D%2C%7B%22namespaceName%22%3A%22application%22%2C%22notificationId%22%3A3715%7D%2C%7B%22namespaceName%22%3A%22log4j2请求apollo服务端,如果没有配置更新,或者等待超时,才会返回结果,根据返回状态调用RemoteConfigRepository#onLongPollNotified,里面会调用trySync重新拉去配置

2、在分析监听器中手动在调用ConfigFile configFile = ConfigService.getConfigFile(EMPLOYEE_POST_RANK_NAMESPACE, ConfigFileFormat.JSON)的过程

java 复制代码
@ApolloConfigChangeListener("EMPLOYEE_POST_RANK.json")
    private synchronized void employeePostRankConfigChange(ConfigChangeEvent changeEvent) {
        ConfigFile configFile = ConfigService.getConfigFile("EMPLOYEE_POST_RANK", ConfigFileFormat.JSON);
        formatJson(configFile.getContent(), EMPLOYEE_POST_RANK_NAMESPACE);
//        formatJson(changeEvent.getChange(Content).getNewValue(), EMPLOYEE_POST_RANK_NAMESPACE);
    }

2.1、由于手动调用getConfigFile在监听事件里面,当前方法已经由Bean的后置处理器生产了D efaultConfig,所以当配置发生变化,apollo 服务端根据namespace的长连接返回200,执行trySync重新拉配置,然后触发事件,会调用getConfigFile。

2.2、getConfigFile会再走1.3的步骤,不同的是会调用ConfigFactory#createConfigFile方法,会根据namespace文件类型不同,创建JsonConfigFile,最终也会调用到RemoteConfigRepository#submit方法

从图中可以看到m_longPollNamespaces中EMPLOYEE_POST_RANK.json只有一个,执行完成后会发现EMPLOYEE_POST_RANK.json有两个了。

2.3、上面getConfigFile创建了JsonConfigFile,然后监听器中业务代码来到第二行,configFile.getContent

apollo中配置的数据

可以看到获取的数据是最新的

2.5、从当前分析就可以看出来针对于EMPLOYEE_POST_RANK.json一个namespace,在会存在两个长连接,我们继续修改apollo配置,发现两个长连接都会存在回掉,都会获取最新配置,并且每个RemoteConfigRepository还会存在5分钟的自动配置更新兜底,不应该会存在配置不更新的情况呀。

最终问题定位

从目前源码分析已经确认了不是框架的问题,那么进一步分析业务代码发现下面代码:

typescript 复制代码
private void formatJson(String content, String nameSpace) {
    switch (nameSpace) {
        case EMPLOYEE_POST_RANK_NAMESPACE:
            JSONArray employeePostJsonArray = JSONArray.parseArray(content);
            EMPLOYEE_POST_RANK_LIST = employeePostJsonArray.toJavaList(DictData.class);
            EMPLOYEE_POST_RANK_LIST.sort(Comparator.comparingInt(DictData::getDictNumber));
            break;
}

在监听触发后会创建完getConfigFile,获取到配置信息后会将结果缓存在全局变量EMPLOYEE_POST_RANK_LIST中,并且监听器中调用getConfigFile创建的JsonConfigFile并不会绑定回掉监听器,所以在源码分析中只有ApolloConfigChangeListener创建的DefaultConfig会绑定监听器,而且由于m_longPollNamespaces Map中如果同一个namespace创建了多个Config,就会创建多个RemoteConfigRepository,这样长连接回调总会有先后顺序,如果DefaultConfig的RemoteConfigRepository提前更新,触发监听事件,然后在通过getConfigFile的JsonConfigFile.getContent()获取的就是旧的值

这就是导致配置有时会更新,有时会不更新的问题。

疑问

apollo当前这个设计是否存在问题,对于同一个namespace无论是apollo通过后置处理器创建的Config和用户自己创建的Config,不都应该是一个么,对于在m_longPollNamespaces中的RemoteConfigRepository不是也应该只有一个么,为什么DefaultConfigManager会提供两个方法getConfig和getConfigFile,这两个并且使用的不是一个缓存,还是用的是两个独立的缓存

这样就会导致创建相同一个namespace调用不同的接口,却创建两个Config类和两个RemoteConfigRepository,并且更新配置都是自己独有的。

相关推荐
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平2 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码3 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞4 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod4 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。5 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man5 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*5 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu5 小时前
Go语言结构体、方法与接口
开发语言·后端·golang