共计 5846 个字符,预计需要花费 15 分钟才能阅读完成。
背景
对于 AREX
AREX 是基于实在申请与数据的自动化回归测试平台,利用 Java Agent 和字节码加强技术,在生产环境中记录实在申请链路的入口和依赖的申请和响应数据,而后在测试环境中进行模仿申请回放,并逐个验证整个调用链路的逻辑正确性。AREX Agent 当初曾经反对了大部分开源组件的 Mock,本文将介绍 Agent 如何实现 Apollo 配置核心的 Mock。
对于 Apollo
Apollo(阿波罗)是一款牢靠的分布式配置管理核心,诞生于携程框架研发部,可能集中化治理利用不同环境、不同集群的配置,配置批改后可能实时推送到利用端。
以下是官网对 Apollo 根底模型的形容:
- 用户在配置核心对配置进行批改并公布;
- 配置核心告诉 Apollo 客户端有配置更新;
- Apollo 客户端从配置核心拉取最新的配置、更新本地配置并告诉到利用。
实现原理
下图简要形容了 Apollo 客户端的实现原理:
- 客户端和服务端放弃了一个长连贯,从而能第一工夫取得配置更新的推送。(通过 Http Long Polling 实现)
- 客户端还会定时从 Apollo 配置核心服务端拉取利用的最新配置。
- 客户端从 Apollo 配置核心服务端获取到利用的最新配置后,会保留在内存中
图片起源:https://www.apolloconfig.com/#/zh/design/apollo-design
开发过程
从上图可知 AREX 只须要反对 Apollo 客户端的录制和回放,即 Java 利用我的项目外部援用 apollo-client
的组件:
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>{apollo-client.version}</version>
</dependency>
通常,我的项目中应用 Apollo 的形式次要有以下三种:
- Spring Autowired 注解
configBean
(外部还是应用EnableApolloConfig
注解) - 基于 Apollo 自带的注解
ApolloConfig
,如代码中的config
对象 - API 形式,如代码中的
config1
对象:
<!—->
@Autowired
ConfigBean configBean; // 第一种形式,外部基于 EnableApolloConfig 注解
@ApolloConfig("TEST1.lucas")
private Config config; // 第二种形式
private Config config1; // 第三种形式,在代码中调用 getAppConfig 实例化
public void test() {config1 = ConfigService.getAppConfig();
System.out.println("timeout="+config.getProperty("timeout", "0"));
System.out.println("switch="+config.getBooleanProperty("switch", false));
System.out.println("json="+config.getProperty("json", ""));
System.out.println("white.list="+config1.getProperty("flight.change.white.list", ""));
System.out.println("configBean="+configBean);
// 监听 Apollo 配置变更
ConfigChangeListener changeListener = changeEvent -> {System.out.println("Changes for namespace:" + changeEvent.getNamespace());
};
config.addChangeListener(changeListener);
}
@Component
@Configuration
@EnableApolloConfig("TEST1.sofia")
public class ConfigBean {@Value("${age:0}")
int age;
@Value("${name:}")
String name;
@ApolloJsonValue("${resume:[]}")
private List<JsonBean> jsonBean;
}
如果 AREX 须要实现 Apollo 的录制和回放就要 兼容这 3 种应用形式,通过查看 Apollo 源码发现前两种基于注解 EnableApolloConfig
,ApolloConfig
和最初一种调用 API 的形式底层都是通过 ConfigService.getAppConfig()
创立的实例,也就是说底层 API 是共用的,这样咱们就能够润饰这些 Apollo 底层的办法插入 AREX 的字节码,达到录制和回放的目标。
录制实现
Apollo 里所有的配置项是依据 Namespace 辨别的,咱们要获取所有的配置实例,即拿到所有 Namespace 对应的 config instance 能力录制。进一步查看 apollo-client
源码发现,config instance 都保护在 DefaultConfigManager
类的 Mapm_configs
里。
但须要思考以下几个问题:
m_configs
属性是 private 的,且没有相干 API 能够获取到;- 这个实例创立后,在业务运行期间很少会调用该类,所以通过惯例的 AREX Mock 形式可能无奈获取到这个
m_configs
; - 拿到
m_configs
实例后还须要获取 Config 类中的m_configProperties
,这外面才是真正的配置数据。
UML 依赖如下图:
所以衡量下来应用反射的形式获取所有的配置并录制(只有第一次启动和配置产生变更了才通过反射录制,频率很低)。
另外一个须要思考的点就是录制的机会,比方下面代码展现的我的项目中应用 Apollo 的形式中的第 3 种,在业务接口 test() 中才创立 config1
实例:
Config config1 = ConfigService.getAppConfig()
这种能够了解为增量配置实例(比照前两种注解的形式在我的项目启动时已创立好的全量配置实例),所以咱们在录制的时候须要思考这两种形式都要录制到,目前的做法是在申请完主入口 servlet/dubbo
接口,返回后果前,即 postHandle
后置点进行录制,这样不论是哪种形式创立的 config 实例咱们都能获取到并录制下来。(具体实现参考:ApolloServletV3RequestHandler#postHandle)
如果录制期间 Apollo 配置产生了变更,咱们能够通过在 Apollo 源码:com.ctrip.framework.apollo.internals.DefaultConfig#updateAndCalcConfigChanges
办法退出润饰代码,监听变更事件,从新关上咱们的录制开关,这样就能够在下次录制时录制到新的配置。(具体实现参考:ApolloDefaultConfigInstrumentation$UpdateAdvice)
AREX 在录制时会生成一个版本号,用来辨别不同时间段内录制的这一批用例 (Case) 属于哪个版本号,即起到一个批次的概念,如上面时间轴所示:
回放实现
回放的实现能够参考录制的实现,通过反射给 m_configProperties
赋值,应用 Mock 的配置笼罩掉实在的配置。
但同样有以下几个问题须要思考:
- 如何触发利用设置的配置变更监听办法,如下面 Apollo 应用形式里的
changeListener
办法; - 回放期间,Apollo 长轮询获取配置变更后可能笼罩掉咱们回放的配置,须要防止这种状况;
- 回放多个版本的配置如何保障配置数据的正确性;
- 回放完结后如何还原回原来的配置。
基于以上几点,如果还应用录制的实现形式来做回放是不全面的,无奈满足这些非凡场景。
咱们通过查看源码后确定最终的实现计划是通过润饰 com.ctrip.framework.apollo.internals.RemoteConfigRepository#loadApolloConfig
办法,在申请服务器配置前间接返回咱们 Mock 的配置数据,这样就能利用 Apollo 现有的机制触发残缺的配置更新流程,也就达到了咱们回放的目标。当然触发回放是调用 com.ctrip.framework.apollo.internals.RemoteConfigRepository#sync
→ loadApolloConfig
。
解决方案如下:
- 如果在回放过程中则不会调用实在的 Apollo-Server 服务,间接返回 Mock 的配置;
- 如果回放完结后不再 Mock 该办法(不回放超过 1 分钟则认为回放配置完结),执行失常的逻辑,即应用实在的配置。
流程图如下所示:
因为 Apollo 的长轮询始终在运行中,如果回放完结,此时 Apollo 发现服务器的配置和咱们 AREX 回放的配置不统一则会触发更新本地配置的操作,达到还原的目标。(具体实现参考:ApolloConfigHelper#replayAllConfigs)
另外下面列的第 2 点问题:如何保障回放多个版本的配置数据正确性?
参考 录制实现 中的时间轴图,AREX 在第一次(我的项目启动时)和后续配置产生变更时才录制配置,此时生成的版本号(UUID)也会作为回放时版本号应用,即如果录制了多个版本号,那回放时也是依照不同的版本号顺次做的回放,也就是说通过 AREX 生成的版本号来辨别不同版本的配置。实现形式是在每次回放配置时,在结构返回的 Apollo 配置实体 com.ctrip.framework.apollo.core.dto.ApolloConfig
类后,设置 releaseKey
属性为咱们 AREX 的版本号,这样就能够保障回放多个版本的配置数据正确性。(releaseKey
是 Apollo Client 和 Server 端交互的关键字段,Server 端依据此字段判断配置是否和 Client 统一,不统一则会返回一个新的 releaseKey
值,统一则返回 304 状态)具体实现参考:ApolloConfigHelper#getReplayConfig
版本差别
另外还须要留神润饰的 apollo-client
不同版本之间源码的差别,有些办法或类在不同的版本会有一些差别,咱们在决定润饰底层办法前最难看下不同版本之间的差别,否则可能因为用户我的项目应用的 apollo-client
版本和 arex-agent
润饰的版本不一样而失败。
比方咱们润饰的上面这个 Apollo 办法 com.ctrip.framework.apollo.internals.DefaultConfig#updateAndCalcConfigChanges
别离在 1.0.0 和 1.2.0 两个版本的入参差别:
v1.0.0
v1.2.0
咱们在润饰这个办法时要兼容这种差别,能力让咱们插入的字节码在不同版本下都能失常工作,另外对于咱们要润饰组件的版本抉择倡议选低版本的,依照个别的开闭准则,尽量能做到兼容。
联调
这里的联调指的是跟 AREX Schedule Service 的回放联调,用户在页面点击回放按钮时,Schedule 会先依照录制时生成的所有配置版本号进行一次分组,即这个时间段内的所有用例分完组后再依据版本号,每次回放前先进行一次版本号的切换,即告知 arex-agent
须要回放 Apollo 的配置了。
这次版本切换的申请不作为实在的回放后果,仅仅是作为一次版本预热,等版本号对应的配置切换胜利后再进行惯例的回放流程,前面如果要切换其余的版本号时一样的流程,如下图所示:
同一批用例 (case) 的版本号一样,同一批次的只录制或回放一次。
配置版本号别离存在数据库的 RollingServletMocker
、RollingDubboProviderMocker
、RollingConfigFileMocker
,能够先去入口表 RollingServletMocker/RollingDubboProviderMocker -> targetRequest -> attributes -> configBatchNo (录制的配置版本号),而后应用 configBatchNo 值关联到 RollingConfigFileMocker 表的 recordId,即可查出这个版本号录制的所有配置。
总结思考
以上就是如何在 Agent 中实现 Apollo 配置核心录制和回放的具体实现细节,总体来说 Apollo Config 的源码逻辑还是比拟清晰的,能够借鉴下它和服务端交互的实现形式,如长轮询 + 服务端推送的机制,另外 Apollo 对 Spring 的反对也做得很好,比方基于 BeanPostProcessor
机制,实现自定义注解初始化 Config 实例,以及通过继承 EnumerablePropertySource
类利用 Spring 现有的注解 @Value("${timeout:0}")
来实现读取自定义的配置,对业务方来说应用就会很不便,升高接入老本。
注意事项
- 回放期间如果有其余申请到这台机器,也会返回回放的 Apollo 配置,Apollo 的配置都是存在内存中共享的,所以回放期间最好不要申请这个机器的利用,待回放完结,配置会主动还原。
- Apollo 有 local 模式(
com.ctrip.framework.apollo.spi.DefaultConfigFactory#createLocalConfigRepository
),这个的应用场景很少,所以 AREX Agent 临时不思考反对这种模式,如果大家有应用 local 模式且须要录制回放的,能够在 Github 提交 Issue。 - AREX 生成的配置版本号 (
UUID.randomUUID()
) 是无序的,Schedule 如果依照版本号分组做预热,可能不反对排序,不过目前临时没有这种业务场景。
AREX 文档:http://arextest.com/zh-Hans/docs/intro/
AREX 官网:http://arextest.com/
AREX GitHub:https://github.com/arextest
AREX 官网 QQ 交换群:656108079