Apollo源码阅读(四)Apollo客户端-ConfigRepository

876 阅读4分钟

一、ConfigService.getConfig

  • ConfigService.getConfig这个方法基本加载了Apollo通过com.google.inject.Injector实现的所有单例
  • 执行配置同步的入口
  • DefaultConfigFactory#create创建了DefaultConfig,DefaultConfig是读取Apollo配置的入口,提供了如getProperties等方法
  @Override
  public Config create(String namespace) {
    // 1. 如果是ConfigFileFormat枚举定义的后缀结尾,如yml,创建PropertiesCompatibleFileConfigRepository
    ConfigFileFormat format = determineFileFormat(namespace);
    if (ConfigFileFormat.isPropertiesCompatible(format)) {
      return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format));
    }
    // 2. 其他创建LocalFileConfigRepository
    return new DefaultConfig(namespace, createLocalConfigRepository(namespace));
  }
   LocalFileConfigRepository createLocalConfigRepository(String namespace) {
    // 1. 判断env是否未Local模式,如果是的话,不走Apollo,直接取本地配置文件(-Denv=LOCAL可以不连接Apollo服务端,走本地配置文件)
    if (m_configUtil.isInLocalMode()) {
      return new LocalFileConfigRepository(namespace);
    }
    // 2. 创建RemoteConfigRepository,设置LocalFileConfigRepository的upstream为这个RemoteConfigRepository
    return new LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));
  }

二、 LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));

 public LocalFileConfigRepository(String namespace, ConfigRepository upstream) {
    // 1. 创建本地目录 mac - /opt/data/{app.id}/config-cache/
    this.setLocalCacheDir(findLocalCacheDir(), false);
    // 2. 设置upstream,从upstream(RemoteConfigRepository)同步配置信息并持久化到本地文件,加载配置信息到内存m_fileProperties
    this.setUpstreamRepository(upstream);
    // 3. 如果上面同步配置信息失败,这里会再同步一次,加载配置信息到内存m_fileProperties
    this.trySync();
  }

三、 new RemoteConfigRepository(namespace)

  • 首次配置同步
  • 启动一个线程每5分钟更新一次配置
  • 启动一个线程,使用HttpLongPolling,建立90s长连接,实时获取服务端的配置推送
public RemoteConfigRepository(String namespace) {
    gson = new Gson();
    // 首次配置同步
    this.trySync();
    // 定时从Apollo配置中心服务端拉取应用的最新配置 fallback机制,为了防止推送机制失效导致配置不更新
    this.schedulePeriodicRefresh();
    // 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
    this.scheduleLongPollingRefresh();
  }

四、DefaultConfig、LocalFileConfigRepository和RemoteConfigRepository的关系

  • 首先他们都是namespace级别的,即一个namespace一个实例

  • DefaultConfig的getProperties方法的数据来源是

    public String getProperty(String key, String defaultValue) {
      // 1. -D参数
      String value = System.getProperty(key);
    
      // 2. upstream给到的properties,这里就是LocalFileConfigRepository给的properties
      if (value == null && m_configProperties.get() != null) {
        value = m_configProperties.get().getProperty(key);
      }
      // 3. 系统变量
      if (value == null) {
        value = System.getenv(key);
      }
      // 4. META-INF/config/{namespace}.properties
      if (value == null && m_resourceProperties != null) {
        value = m_resourceProperties.getProperty(key);
      }
      return value == null ? defaultValue : value;
    }
    
  • 当RemoteConfigRepository获取到Apollo服务端的最新配置后,会通知LocalFileConfigRepository更新本地配置文件;当LocalFileConfigRepository更新完本地配置文件中,会通知DefaultConfig更新m_configProperties这个属性

五、RemoteConfigRepository

1、sync方法

  • trySync方法是AbstractConfigRepository定义的,主要是trycatch了异常,真正实现配置同步的方法是子类的sync方法
  @Override
  protected synchronized void sync() {
    // 1. 当前缓存的配置对象
    ApolloConfig previous = m_configCache.get();
    // 2. 调ConfigService获取当前配置信息,如果返回304则用当前的配置对象返回
    ApolloConfig current = loadApolloConfig();
    // 3. 如果当前缓存里的配置与拉取的配置不相等,更新缓存里的配置对象
    if (previous != current) {
      m_configCache.set(current);
      // 4. 通知所有RepositoryChangeListener(首次启动同步配置时,还未注册Listener;后续每次配置更新,都会走这里通知到LocalFileConfigRepository, 然后通知到DefaultConfig)
      this.fireRepositoryChange(m_namespace, this.getConfig());
    }
  }
  
  private ApolloConfig loadApolloConfig() {
    // RateLimiter
    if (!m_loadConfigRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {
        TimeUnit.SECONDS.sleep(5);
    }
    String appId = m_configUtil.getAppId();
    String cluster = m_configUtil.getCluster();
    String dataCenter = m_configUtil.getDataCenter();
    String secret = m_configUtil.getAccessKeySecret();
    int maxRetries = m_configNeedForceRefresh.get() ? 2 : 1;
    // 1. 获取ConfigService的信息 http://localhost:8080/services/config?appId=SampleApp&ip=192.168.0.105
    // appName='APOLLO-CONFIGSERVICE', instanceId='192.168.0.105:apollo-configservice:8080', homepageUrl='http://192.168.0.105:8080/'
    List<ServiceDTO> configServices = getConfigServices();
    String url = null;
    // 2. 最多尝试拉取2次配置maxRetries = 2
    for (int i = 0; i < maxRetries; i++) {
      // 3. 优先使用常轮询过程中调用本机的ConfigService(m_longPollServiceDto)
      List<ServiceDTO> randomConfigServices = Lists.newLinkedList(configServices);
      Collections.shuffle(randomConfigServices);
      if (m_longPollServiceDto.get() != null) {
        randomConfigServices.add(0, m_longPollServiceDto.getAndSet(null));
      }

      for (ServiceDTO configService : randomConfigServices) {
        // 4. 组装URL:http://192.168.0.105:8080/configs/SampleApp/default/application?ip=192.168.0.105
        url = assembleQueryConfigUrl(configService.getHomepageUrl(), appId, cluster, m_namespace,
                dataCenter, m_remoteMessages.get(), m_configCache.get());
        HttpRequest request = new HttpRequest(url);
        // 5. 如果设置了secret,把secret放入请求头
        if (!StringUtils.isBlank(secret)) {
          Map<String, String> headers = Signature.buildHttpHeaders(url, appId, secret);
          request.setHeaders(headers);
        }
        // 6. 调用
        HttpResponse<ApolloConfig> response = m_httpUtil.doGet(request, ApolloConfig.class);
        // 7. 304直接用缓存对象,否则用configService提供的Config
        if (response.getStatusCode() == 304) {
          return m_configCache.get();
        }
        ApolloConfig result = response.getBody();
        return result;
      }
    }
  }

2、 每5分钟请求Apollo服务端拉取配置的定时任务,走的也是trySync

private void schedulePeriodicRefresh() {
        m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit());
    m_executorService.scheduleAtFixedRate(
        new Runnable() {
          @Override
          public void run() {
            trySync();
          }
        }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),
        m_configUtil.getRefreshIntervalTimeUnit());
  }

3、 启动一个线程,使用HttpLongPolling,建立90s长连接,实时获取服务端的配置推送

  • 这里服务端只会告知哪个namespace的配置发生了更新,需要客户端自己使用trySync方法去取配置
 private void scheduleLongPollingRefresh() {
    remoteConfigLongPollService.submit(m_namespace, this);
  }

RemoteConfigLongPollService#doLongPollingRefresh

 private void doLongPollingRefresh(String appId, String cluster, String dataCenter, String secret) {
    final Random random = new Random();
    ServiceDTO lastServiceDto = null;
    while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) {
      if (!m_longPollRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {
        TimeUnit.SECONDS.sleep(5);
      }
      String url = null;
      try {
        // 1. 随机选取一个ConfigService
        if (lastServiceDto == null) {
          List<ServiceDTO> configServices = getConfigServices();
          lastServiceDto = configServices.get(random.nextInt(configServices.size()));
        }
        // 2. 组装URL http://192.168.0.105:8080/notifications/v2?cluster=default&appId=SampleApp&ip=192.168.0.105&notifications=%5B%7B%22namespaceName%22%3A%22application%22%2C%22notificationId%22%3A-1%7D%5D
        url = assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster, dataCenter, m_notifications);
        HttpRequest request = new HttpRequest(url);
        request.setReadTimeout(LONG_POLLING_READ_TIMEOUT); // 90秒长轮询
        // 设置secret
        if (!StringUtils.isBlank(secret)) {
          Map<String, String> headers = Signature.buildHttpHeaders(url, appId, secret);
          request.setHeaders(headers);
        }
        // 3. 返回 [{"namespaceName":"application","notificationId":40,"messages":{"details":{"SampleApp+default+application":40}}}]
        final HttpResponse<List<ApolloConfigNotification>> response =
            m_httpUtil.doGet(request, m_responseType);
        // 4. 处理返回的结果
        if (response.getStatusCode() == 200 && response.getBody() != null) {
          // 4-1. 更新this.m_notifications的value为返回的notificationId
          updateNotifications(response.getBody());
          // 4-2. 更新this.m_remoteNotificationMessages
          updateRemoteNotifications(response.getBody());
          // 4-3. 通知RemoteConfigRepository.onLongPollNotified
          notify(lastServiceDto, response.getBody());
        }
        //try to load balance
        // 如果60秒内没有配置更新,随机触发是否置空lastServiceDto,下次循环换一个ConfigService请求
        if (response.getStatusCode() == 304 && random.nextBoolean()) {
          lastServiceDto = null;
        }
        // 5. 重置失败策略(睡眠)
        m_longPollFailSchedulePolicyInSecond.success();
      } catch (Throwable ex) {
        lastServiceDto = null;
        // 失败睡眠,1,2,4,...秒
        long sleepTimeInSecond = m_longPollFailSchedulePolicyInSecond.fail();
        TimeUnit.SECONDS.sleep(sleepTimeInSecond);
      } 
    }
  }
  • RemoteConfigRepository.onLongPollNotified,最后还是通过trySync同步配置
 // Client长轮询,更新配置入口
  public void onLongPollNotified(ServiceDTO longPollNotifiedServiceDto, ApolloNotificationMessages remoteMessages) {
    // 1. 设置长轮询对应的ConfigService ip和port,之后trySync直接调这个节点查询配置
    m_longPollServiceDto.set(longPollNotifiedServiceDto);
    // 2. 设置收到的notify消息
    m_remoteMessages.set(remoteMessages);
    // 3. 同步配置(和启动流程里的同步逻辑一致)
    m_executorService.submit(new Runnable() {
      @Override
      public void run() {
        m_configNeedForceRefresh.set(true);
        trySync();
      }
    });
  }