1. 背景

一般情况下,可以不加这个配置热更新。但是如果遇到动态数据维护在配置中的话,热更新还是比较方便的,例如在配置中维护黑白名单数据等等,这样测试环境不用每次都叫测试进行重启。

2. 介绍

2.1 基础架构

apollo mysql等配置会热部署吗 发布后 apollo配置热更新_客户端

  1. 用户在配置中心对配置进行修改并发布。
  2. 配置中心通知Apollo客户端有配置更新(这里的主动推送哪里可以考究,从代码上看应该不是主动推送到客户端,因为客户端是定时任务和长轮询去做的)。
  3. Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用。 

2.2 结构模块

apollo mysql等配置会热部署吗 发布后 apollo配置热更新_客户端_02

看到这里,整个架构看起来就比较清晰了。接下来从上往下简单介绍一下:

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 配置发布后实时推送设计

apollo mysql等配置会热部署吗 发布后 apollo配置热更新_Boo_03

上图简要描述了配置发布的大致过程:

  1. 用户在Portal操作配置发布。
  2. Portal调用Admin Service的接口操作发布。
  3. Admin Service发布配置后,发送ReleaseMessage给各个Config Service(这里的理解并不能说是异步通知,apollo并没有采用外部依赖的中间件,而是利用数据库作为中介,Config Service每秒去轮询)。
  4. Config Service收到ReleaseMessage后,通知对应的客户端(Client)(我觉得正常的是客户端去轮询ReleaseMessage,看配置是否有变化,如果有则去更新配置)。

如何异步

  • Admin Service在配置发布后会往ReleaseMessage表插入一条消息记录,消息内容就是配置发布的AppId+Cluster+Namespace。
  • 然后Config Service有一个线程会每秒扫描一次ReleaseMessage表,看看是否有新的消息记录。
  • Config Service如果发现有新的消息记录,那么就会通知到所有的消息监听器,监听器得到配置发布的AppId+Cluster+Namespace后,会通知对应的客户端。

 

apollo mysql等配置会热部署吗 发布后 apollo配置热更新_客户端_04

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