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¬ifications=%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,并且更新配置都是自己独有的。