irpas技术客

Apollo-配置如何热更新_皮皮皮的代码_apollo热更新

大大的周 7582

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详解】


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #apollo热更新 #1 #2 #介绍21