1. 背景
一般情况下,可以不加这个配置热更新。但是如果遇到动态数据维护在配置中的话,热更新还是比较方便的,例如在配置中维护黑白名单数据等等,这样测试环境不用每次都叫测试进行重启。
2. 介绍
2.1 基础架构
- 用户在配置中心对配置进行修改并发布。
- 配置中心通知Apollo客户端有配置更新(这里的主动推送哪里可以考究,从代码上看应该不是主动推送到客户端,因为客户端是定时任务和长轮询去做的)。
- Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用。
2.2 结构模块
看到这里,整个架构看起来就比较清晰了。接下来从上往下简单介绍一下:
Portal服务:提供Web界面供用户管理配置,通过MetaServer获取AdminService服务列表(IP+Port),通过IP+Port访问AdminService服务。
Client:实际上就是我们创建的SpringBoot项目,引入ApolloClient的maven依赖,为应用提供配置获取、实时更新等功能。
Meta Server:从Eureka获取Config Service和Admin Service的服务信息,相当于是一个Eureka Client。主要是为了封装服务发现的细节,对Portal和Client而言,永远通过一个Http接口获取Admin Service和Config Service的服务信息,而不需要关心背后实际的服务注册和发现组件。Meta Server只是一个逻辑角色,在部署时和Config Service是在一个JVM进程中的,所以IP、端口和Config Service一致。
Eureka:注册中心。Config Service和Admin Service会向Eureka注册服务。为了简单起见,目前Eureka在部署时和Config Service是在一个JVM进程中的。
Config Service:提供配置获取接口。提供配置更新推送接口(基于Http long polling)。服务对象为Apollo客户端(Client)。
Admin Service:提供配置管理接口。提供配置发布、修改等接口。服务对象为Portal。
2.3 配置发布后实时推送设计
上图简要描述了配置发布的大致过程:
- 用户在Portal操作配置发布。
- Portal调用Admin Service的接口操作发布。
- Admin Service发布配置后,发送ReleaseMessage给各个Config Service(这里的理解并不能说是异步通知,apollo并没有采用外部依赖的中间件,而是利用数据库作为中介,Config Service每秒去轮询)。
- Config Service收到ReleaseMessage后,通知对应的客户端(Client)(我觉得正常的是客户端去轮询ReleaseMessage,看配置是否有变化,如果有则去更新配置)。
如何异步
- Admin Service在配置发布后会往ReleaseMessage表插入一条消息记录,消息内容就是配置发布的AppId+Cluster+Namespace。
- 然后Config Service有一个线程会每秒扫描一次ReleaseMessage表,看看是否有新的消息记录。
- Config Service如果发现有新的消息记录,那么就会通知到所有的消息监听器,监听器得到配置发布的AppId+Cluster+Namespace后,会通知对应的客户端。
3. 源码
3.1 应用启动时配置初始化
实现ApplicationContextInitializer和EnvironmentPostProcessor,来做配置的初始化工作以namespace为单位,分批同步获取远程配置,
// com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer
public class ApolloApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>, EnvironmentPostProcessor, Ordered {
//...
public void initialize(ConfigurableApplicationContext context) {
ConfigurableEnvironment environment = context.getEnvironment();
String enabled = environment.getProperty("apollo.bootstrap.enabled", "false");
if (!Boolean.valueOf(enabled)) {
logger.debug("Apollo bootstrap config is not enabled for context {}, see property: ${{}}", context, "apollo.bootstrap.enabled");
} else {
logger.debug("Apollo bootstrap config is enabled for context {}", context);
this.initialize(environment);
}
}
protected void initialize(ConfigurableEnvironment environment) {
if (!environment.getPropertySources().contains("ApolloBootstrapPropertySources")) {
// 根据配置的namespace加载配置,默认加载application
String namespaces = environment.getProperty("apollo.bootstrap.namespaces", "application");
logger.debug("Apollo bootstrap namespaces: {}", namespaces);
List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
CompositePropertySource composite = new CompositePropertySource("ApolloBootstrapPropertySources");
Iterator i$ = namespaceList.iterator();
while(i$.hasNext()) {
String namespace = (String)i$.next();
// 这里是入口,apollo配置远程加载
Config config = ConfigService.getConfig(namespace);
composite.addPropertySource(this.configPropertySourceFactory.getConfigPropertySource(namespace, config));
}
environment.getPropertySources().addFirst(composite);
}
}
//...
public void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication) {
this.initializeSystemProperty(configurableEnvironment);
Boolean eagerLoadEnabled = (Boolean)configurableEnvironment.getProperty("apollo.bootstrap.eagerLoad.enabled", Boolean.class, false);
if (eagerLoadEnabled) {
Boolean bootstrapEnabled = (Boolean)configurableEnvironment.getProperty("apollo.bootstrap.enabled", Boolean.class, false);
if (bootstrapEnabled) {
this.initialize(configurableEnvironment);
}
}
}
//...
}
同步一次远程配置,同时开启定时任务以及长轮询
// com.ctrip.framework.apollo.internals.RemoteConfigRepository#RemoteConfigRepository
public RemoteConfigRepository(String namespace) {
m_namespace = namespace;
m_configCache = new AtomicReference<>();
m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);
m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);
remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);
m_longPollServiceDto = new AtomicReference<>();
m_remoteMessages = new AtomicReference<>();
m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());
m_configNeedForceRefresh = new AtomicBoolean(true);
m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),
m_configUtil.getOnErrorRetryInterval() * 8);
gson = new Gson();
// 同步加载配置
this.trySync();
// 每5分钟同步一次
this.schedulePeriodicRefresh();
// 长轮询异步刷新配置
this.scheduleLongPollingRefresh();
}
死循环不断长轮询请求 ConfigServer 的配置变化通知接口 notifications/v2,如果配置有变更,就会返回变更信息,然后向定时任务线程池提交一个任务,任务内容是执行 sync 方法。如果配置没有变化,就等到超时为止。这个比短轮询不断请求服务端好,控制了连接数。
// com.ctrip.framework.apollo.internals.RemoteConfigLongPollService#doLongPollingRefresh
private void doLongPollingRefresh(String appId, String cluster, String dataCenter) {
final Random random = new Random();
ServiceDTO lastServiceDto = null;
while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) {
if (!m_longPollRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {
//wait at most 5 seconds
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
}
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "pollNotification");
String url = null;
try {
if (lastServiceDto == null) {
List<ServiceDTO> configServices = getConfigServices();
lastServiceDto = configServices.get(random.nextInt(configServices.size()));
}
url =
assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster, dataCenter,
m_notifications);
logger.debug("Long polling from {}", url);
HttpRequest request = new HttpRequest(url);
request.setReadTimeout(LONG_POLLING_READ_TIMEOUT);
transaction.addData("Url", url);
final HttpResponse<List<ApolloConfigNotification>> response =
m_httpUtil.doGet(request, m_responseType);
logger.debug("Long polling response: {}, url: {}", response.getStatusCode(), url);
if (response.getStatusCode() == 200 && response.getBody() != null) {
updateNotifications(response.getBody());
updateRemoteNotifications(response.getBody());
transaction.addData("Result", response.getBody().toString());
notify(lastServiceDto, response.getBody());
}
//try to load balance
if (response.getStatusCode() == 304 && random.nextBoolean()) {
lastServiceDto = null;
}
m_longPollFailSchedulePolicyInSecond.success();
transaction.addData("StatusCode", response.getStatusCode());
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
lastServiceDto = null;
Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
transaction.setStatus(ex);
long sleepTimeInSecond = m_longPollFailSchedulePolicyInSecond.fail();
logger.warn(
"Long polling failed, will retry in {} seconds. appId: {}, cluster: {}, namespaces: {}, long polling url: {}, reason: {}",
sleepTimeInSecond, appId, cluster, assembleNamespaces(), url, ExceptionUtil.getDetailMessage(ex));
try {
TimeUnit.SECONDS.sleep(sleepTimeInSecond);
} catch (InterruptedException ie) {
//ignore
}
} finally {
transaction.complete();
}
}
}
3.2 配置更新后会通知监听器
// com.ctrip.framework.apollo.internals.RemoteConfigRepository#sync
protected synchronized void sync() {
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");
try {
ApolloConfig previous = m_configCache.get();
ApolloConfig current = loadApolloConfig();
//reference equals means HTTP 304
if (previous != current) {
// 配置更新
logger.debug("Remote Config refreshed!");
m_configCache.set(current);
// 通知监听器
this.fireRepositoryChange(m_namespace, this.getConfig());
}
if (current != null) {
Tracer.logEvent(String.format("Apollo.Client.Configs.%s", current.getNamespaceName()),
current.getReleaseKey());
}
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
throw ex;
} finally {
transaction.complete();
}
}
// com.ctrip.framework.apollo.internals.AbstractConfig#fireConfigChange
protected void fireConfigChange(final ConfigChangeEvent changeEvent) {
// 监听器
for (final ConfigChangeListener listener : m_listeners) {
// check whether the listener is interested in this change event
if (!isConfigChangeListenerInterested(listener, changeEvent)) {
continue;
}
m_executorService.submit(new Runnable() {
@Override
public void run() {
String listenerName = listener.getClass().getName();
Transaction transaction = Tracer.newTransaction("Apollo.ConfigChangeListener", listenerName);
try {
listener.onChange(changeEvent);
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
Tracer.logError(ex);
logger.error("Failed to invoke config change listener {}", listenerName, ex);
} finally {
transaction.complete();
}
}
});
}
}
3.3 配置监听器,实时刷新应用配置
public class ApolloConfigRefreshAutoConfiguration implements ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(ApolloConfigRefreshAutoConfiguration.class);
/**
* spring控制器
*/
private ApplicationContext applicationContext;
@Value("${apollo.bootstrap.namespaces}")
private String[] namespaces;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 启动时增加监听
*/
@PostConstruct
public void addRefreshListener() {
for (String namespace : namespaces) {
Config config = ConfigService.getConfig(namespace);
//对namespace增加监听方法
config.addChangeListener(changeEvent -> {
for (String key : changeEvent.changedKeys()) {
logger.info("Refresh apollo config:{}", changeEvent.getChange(key));
}
//将变动的配置刷新到应用中,org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder配置重新绑定
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
});
}
}
}
4. 实战
5. FAQ
5.1 ServerLoader和ClassLoader区别?
简单来说,ClassLoader是全类都可以进行加载的。但是ServerLoader是对接口的实现类进行加载的,且该实现类需要在META-INF/services进行维护。
5.2 从资料上看配置更新是apollo主动推送的,但是从客户端应用代码看配置更新是定时任务以及长轮询做的?
5.3 定时任务和长轮询的工作应该是有一点稍微不同的,定时任务是直接同步配置,长轮询是先查询是否有配置更新的通知,有的话再同步配置(我猜,没有依据)?
6. 参考资料
【Apollo自动加载热更新】
【apollo @value没生效_3千字Apollo配置中心的总结,让配置“智能”起来】
【Apollo 3 定时/长轮询拉取配置的设计】
【ServiceLoader详解】