apollo之ConfigService服務

weixin_33912246發表於2018-09-17

apollo configService用於提供給client獲取配置資訊,以及配置更新後實時通知client的服務;configservice僅為client提供服務,且每個環境對應相應的configsevice叢集。

下面通過原始碼來分析configservice功能的具體實現

包路徑

com.ctrip.framework.apollo.configservice

對外API

ConfigController
/**
 * 配置獲取控制層,供client根據名稱空間獲取Config資料資訊
 * @author Jason Song(song_s@ctrip.com)
 */
@RestController
@RequestMapping("/configs")
public class ConfigController {
  /**
   * 配置操作服務
   */
  @Autowired
  private ConfigService configService;
  @Autowired
  private AppNamespaceServiceWithCache appNamespaceService;

  /**
   * 名稱空間工具類
   */
  @Autowired
  private NamespaceUtil namespaceUtil;
  @Autowired
  private InstanceConfigAuditUtil instanceConfigAuditUtil;

  /**
   * json解析器
   */
  @Autowired
  private Gson gson;

  private static final Type configurationTypeReference = new TypeToken<Map<String, String>>() {
      }.getType();

  /**
   * 查詢配置資訊
   * @param appId 應用ID
   * @param clusterName 叢集名稱
   * @param namespace 名稱空間
   * @param dataCenter 資料中心
   * @param clientSideReleaseKey
   * @param clientIp 客戶端IP
   * @param messagesAsString
   * @param request
   * @param response
   * @return
   * @throws IOException
   */
  @RequestMapping(value = "/{appId}/{clusterName}/{namespace:.+}", method = RequestMethod.GET)
  public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String clusterName,
                                  @PathVariable String namespace,
                                  @RequestParam(value = "dataCenter", required = false) String dataCenter,
                                  @RequestParam(value = "releaseKey", defaultValue = "-1") String clientSideReleaseKey,
                                  @RequestParam(value = "ip", required = false) String clientIp,
                                  @RequestParam(value = "messages", required = false) String messagesAsString,
                                  HttpServletRequest request, HttpServletResponse response) throws IOException {
    //構建名稱空間
    String originalNamespace = namespace;
    //strip out .properties suffix
    namespace = namespaceUtil.filterNamespaceName(namespace);
    //fix the character case issue, such as FX.apollo <-> fx.apollo
    namespace = namespaceUtil.normalizeNamespace(appId, namespace);

    if (Strings.isNullOrEmpty(clientIp)) {
      clientIp = tryToGetClientIp(request);
    }

    //轉換通知訊息
    ApolloNotificationMessages clientMessages = transformMessages(messagesAsString);

    //已釋出配置集合
    List<Release> releases = Lists.newLinkedList();

    String appClusterNameLoaded = clusterName;
    if (!ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) {
      //載入給定當前引數下的所有已釋出的配置資訊
      Release currentAppRelease = configService.loadConfig(appId, clientIp, appId, clusterName, namespace,
          dataCenter, clientMessages);

      if (currentAppRelease != null) {
        //新增發布資訊
        releases.add(currentAppRelease);
        //we have cluster search process, so the cluster name might be overridden
        appClusterNameLoaded = currentAppRelease.getClusterName();
      }
    }

    //if namespace does not belong to this appId, should check if there is a public configuration
    if (!namespaceBelongsToAppId(appId, namespace)) {
      //查詢公共的釋出資訊
      Release publicRelease = this.findPublicConfig(appId, clientIp, clusterName, namespace,
          dataCenter, clientMessages);
      if (!Objects.isNull(publicRelease)) {
        //新增公共的釋出資訊
        releases.add(publicRelease);
      }
    }

    if (releases.isEmpty()) {
      response.sendError(HttpServletResponse.SC_NOT_FOUND,
          String.format(
              "Could not load configurations with appId: %s, clusterName: %s, namespace: %s",
              appId, clusterName, originalNamespace));
      Tracer.logEvent("Apollo.Config.NotFound",
          assembleKey(appId, clusterName, originalNamespace, dataCenter));
      return null;
    }

    auditReleases(appId, clusterName, dataCenter, clientIp, releases);

    //合併釋出KEY 用於校驗配置是否有變更操作
    String mergedReleaseKey = releases.stream().map(Release::getReleaseKey)
            .collect(Collectors.joining(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR));

    //校驗釋出KEY與客戶端已存在的釋出KEY是否一致
    if (mergedReleaseKey.equals(clientSideReleaseKey)) {
      //客戶端釋出EKY與查詢到的KEY一致,標識配置未做變更過操作,客戶端的配置為最新配置,返回304
      // Client side configuration is the same with server side, return 304
      response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
      Tracer.logEvent("Apollo.Config.NotModified",
          assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter));
      return null;
    }

    //構建返回例項資訊
    ApolloConfig apolloConfig = new ApolloConfig(appId, appClusterNameLoaded, originalNamespace,
        mergedReleaseKey);
    //新增發布配置資訊
    apolloConfig.setConfigurations(mergeReleaseConfigurations(releases));

    Tracer.logEvent("Apollo.Config.Found", assembleKey(appId, appClusterNameLoaded,
        originalNamespace, dataCenter));
    return apolloConfig;
  }

  private boolean namespaceBelongsToAppId(String appId, String namespaceName) {
    //Every app has an 'application' namespace
    if (Objects.equals(ConfigConsts.NAMESPACE_APPLICATION, namespaceName)) {
      return true;
    }

    //if no appId is present, then no other namespace belongs to it
    if (ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) {
      return false;
    }

    AppNamespace appNamespace = appNamespaceService.findByAppIdAndNamespace(appId, namespaceName);

    return appNamespace != null;
  }

  /**
   * 查詢所有的公共的釋出資訊記錄
   * @param clientAppId the application which uses public config
   * @param namespace   the namespace
   * @param dataCenter  the datacenter
   */
  private Release findPublicConfig(String clientAppId, String clientIp, String clusterName,
                                   String namespace, String dataCenter, ApolloNotificationMessages clientMessages) {
    AppNamespace appNamespace = appNamespaceService.findPublicNamespaceByName(namespace);

    //check whether the namespace's appId equals to current one
    if (Objects.isNull(appNamespace) || Objects.equals(clientAppId, appNamespace.getAppId())) {
      return null;
    }

    String publicConfigAppId = appNamespace.getAppId();

    return configService.loadConfig(clientAppId, clientIp, publicConfigAppId, clusterName, namespace, dataCenter,
        clientMessages);
  }

  /**
   * 合併釋出的配置資訊
   * Merge configurations of releases.
   * Release in lower index override those in higher index
   */
  Map<String, String> mergeReleaseConfigurations(List<Release> releases) {
    //構建配置MAP key-V
    Map<String, String> result = Maps.newHashMap();
    //遍歷所有釋出的配置資訊
    for (Release release : Lists.reverse(releases)) {
      //組裝釋出的配置到map集合中
      result.putAll(gson.fromJson(release.getConfigurations(), configurationTypeReference));
    }
    return result;
  }

}

ConfigController

  • /configs/{appId}/{clusterName}/{namespace:.+} :查詢給定引數下所有已釋出的配置集合,返回ApolloConfig
  • 已釋出的配置,包含public和給定引數下配置兩部分
  • 此API同時記錄當前應用例項資訊(Instance)到DB中,通過InstanceConfigAuditUtil類
  • 此API用於獲取client端中Config的原始資料
ConfigFileController
程式碼略...

ConfigFileController

  • /configfiles/{appId}/{clusterName}/{namespace:.+} : 查詢給定引數下所有釋出的配置集合,組裝成給定檔案格式的字串形式返回,(JSON或properties)格式
  • 此API存在快取功能,快取儲存在記憶體中
  • 已釋出的配置集合通過ConfigController獲取
NotificationControllerV2
程式碼略...

NotificationControllerV2

  • /notifications/v2: 當釋出訊息有更新時通知client配置已變更
  • 介面使用Http Long Polling方式實現,用於配置中心配置變更後動態通知客戶端

長連線實際上我們是通過Http Long Polling實現的,具體而言:

  • 客戶端發起一個Http請求到服務端
  • 服務端會保持住這個連線60秒
  1. 如果在60秒內有客戶端關心的配置變化,被保持住的客戶端請求會立即返回,並告知客戶端有配置變化的namespace資訊,客戶端會據此拉取對應namespace的最新配置
  2. 如果在60秒內沒有客戶端關心的配置變化,那麼會返回Http狀態碼304給客戶端
  • 客戶端在收到服務端請求後會立即重新發起連線,回到第一步

配置查詢服務

ConfigService
/**
 * 配置載入服務介面,用於載入釋出的配置資訊
 * @author Jason Song(song_s@ctrip.com)
 */
public interface ConfigService extends ReleaseMessageListener {

  /**
   * 載入釋出配置資訊
   * Load config
   *
   * @param clientAppId the client's app id 客戶端應用ID
   * @param clientIp the client ip 客戶端IP
   * @param configAppId the requested config's app id 配置應用ID
   * @param configClusterName the requested config's cluster name 配置的叢集名稱
   * @param configNamespace the requested config's namespace name 配置的名稱空間
   * @param dataCenter the client data center  客戶端的資料中心
   * @param clientMessages the messages received in client side 通知訊息
   * @return the Release
   */
  Release loadConfig(String clientAppId, String clientIp, String configAppId, String
      configClusterName, String configNamespace, String dataCenter, ApolloNotificationMessages clientMessages);
}

ConfigService介面

  • 用於載入給定引數下所有已釋出的配置資訊
AbstractConfigService
/**
 * 抽象的配置載入服務,用於載入釋出的配置資訊
 * @author Jason Song(song_s@ctrip.com)
 */
public abstract class AbstractConfigService implements ConfigService {
  @Autowired
  private GrayReleaseRulesHolder grayReleaseRulesHolder;

  /**
   * 載入釋出配置的記錄
   * @param clientAppId the client's app id 客戶端應用ID
   * @param clientIp the client ip 客戶端IP
   * @param configAppId the requested config's app id 配置應用ID
   * @param configClusterName the requested config's cluster name 配置的叢集名稱
   * @param configNamespace the requested config's namespace name 配置的名稱空間
   * @param dataCenter the client data center  客戶端的資料中心
   * @param clientMessages the messages received in client side 通知訊息
   * @return
   */
  @Override
  public Release loadConfig(String clientAppId, String clientIp, String configAppId, String configClusterName,
      String configNamespace, String dataCenter, ApolloNotificationMessages clientMessages) {
    //判斷叢集名稱是否為預設
    // load from specified cluster fist
    if (!Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, configClusterName)) {
      //根據叢集名稱查配置釋出記錄
      Release clusterRelease = findRelease(clientAppId, clientIp, configAppId, configClusterName, configNamespace,
          clientMessages);

      if (!Objects.isNull(clusterRelease)) {
        return clusterRelease;
      }
    }

    // try to load via data center
    if (!Strings.isNullOrEmpty(dataCenter) && !Objects.equals(dataCenter, configClusterName)) {
      //根據資料中心查詢配置釋出記錄
      Release dataCenterRelease = findRelease(clientAppId, clientIp, configAppId, dataCenter, configNamespace,
          clientMessages);
      if (!Objects.isNull(dataCenterRelease)) {
        return dataCenterRelease;
      }
    }

    //載入預設的配置釋出記錄
    // fallback to default release
    return findRelease(clientAppId, clientIp, configAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, configNamespace,
        clientMessages);
  }

  /**
   * 查詢釋出記錄
   * Find release
   */
  private Release findRelease(String clientAppId, String clientIp, String configAppId, String configClusterName,
      String configNamespace, ApolloNotificationMessages clientMessages) {
    Long grayReleaseId = grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(clientAppId, clientIp, configAppId,
        configClusterName, configNamespace);

    Release release = null;

    if (grayReleaseId != null) {
      //查詢釋出記錄
      release = findActiveOne(grayReleaseId, clientMessages);
    }

    if (release == null) {
      //查詢最後的釋出記錄
      release = findLatestActiveRelease(configAppId, configClusterName, configNamespace, clientMessages);
    }

    return release;
  }

  /**
   * 根據ID查詢有效釋出記錄
   * Find active release by id
   */
  protected abstract Release findActiveOne(long id, ApolloNotificationMessages clientMessages);

  /**
   * 根據應用ID,叢集名稱、名稱空間查詢釋出記錄
   * Find active release by app id, cluster name and namespace name
   */
  protected abstract Release findLatestActiveRelease(String configAppId, String configClusterName,
      String configNamespaceName, ApolloNotificationMessages clientMessages);
}

AbstractConfigService

  • 查詢釋出配置的抽象實現,重新抽象了方法findLatestActiveRelease與findActiveOne供子類實現
DefaultConfigService
/**
 * 預設的配置查詢服務,無快取功能
 * config service with no cache
 *
 * @author Jason Song(song_s@ctrip.com)
 */
public class DefaultConfigService extends AbstractConfigService {

  /**
   * 釋出記錄操作服務,通過操作DB資源獲取釋出記錄
   */
  @Autowired
  private ReleaseService releaseService;

  @Override
  protected Release findActiveOne(long id, ApolloNotificationMessages clientMessages) {
    //呼叫釋出記錄操作服務查詢配置釋出記錄
    return releaseService.findActiveOne(id);
  }

  @Override
  protected Release findLatestActiveRelease(String configAppId, String configClusterName, String configNamespace,
                                            ApolloNotificationMessages clientMessages) {
    //呼叫釋出記錄操作服務查詢配置釋出記錄
    return releaseService.findLatestActiveRelease(configAppId, configClusterName,
        configNamespace);
  }

  @Override
  public void handleMessage(ReleaseMessage message, String channel) {
    // since there is no cache, so do nothing
    //無本地快取,每次獲取都是資料庫最近的釋出記錄,所以此處釋出記錄變更監聽處理函式不做任何操作
  }
}

DefaultConfigService

  • 無快取功能的實現,通過DB操作資源來查詢庫中的釋出配置資料
  • DB操作資源:ReleaseService
ConfigServiceWithCache
/**
 * 配置查詢服務,使用guava做本地快取,帶有本地快取功能的實現
 * config service with guava cache
 *
 * @author Jason Song(song_s@ctrip.com)
 */
public class ConfigServiceWithCache extends AbstractConfigService {
  private static final Logger logger = LoggerFactory.getLogger(ConfigServiceWithCache.class);
  /**
   * 預設的快取失效時長 1h
   */
  private static final long DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES = 60;//1 hour
  private static final String TRACER_EVENT_CACHE_INVALIDATE = "ConfigCache.Invalidate";
  private static final String TRACER_EVENT_CACHE_LOAD = "ConfigCache.LoadFromDB";
  private static final String TRACER_EVENT_CACHE_LOAD_ID = "ConfigCache.LoadFromDBById";
  private static final String TRACER_EVENT_CACHE_GET = "ConfigCache.Get";
  private static final String TRACER_EVENT_CACHE_GET_ID = "ConfigCache.GetById";
  private static final Splitter STRING_SPLITTER =
      Splitter.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).omitEmptyStrings();

  /**
   * 釋出記錄操作服務
   */
  @Autowired
  private ReleaseService releaseService;

  /**
   * 釋出訊息操作服務
   */
  @Autowired
  private ReleaseMessageService releaseMessageService;

  /**
   * 構建一個釋出訊息ID與配置釋出記錄對應的關係快取
   */
  private LoadingCache<String, ConfigCacheEntry> configCache;

  private LoadingCache<Long, Optional<Release>> configIdCache;

  /**
   * 空的配置釋出實體
   */
  private ConfigCacheEntry nullConfigCacheEntry;

  public ConfigServiceWithCache() {
    nullConfigCacheEntry = new ConfigCacheEntry(ConfigConsts.NOTIFICATION_ID_PLACEHOLDER, null);
  }

  /**
   * 初始化方法,在例項建立後呼叫
   */
  @PostConstruct
  void initialize() {
    //初始化本地快取
    configCache = CacheBuilder.newBuilder()
        .expireAfterAccess(DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES, TimeUnit.MINUTES)
        .build(new CacheLoader<String, ConfigCacheEntry>() {
          @Override
          public ConfigCacheEntry load(String key) throws Exception {
            //appId+clusterName+namespaceName
            //根據KEY切分名稱空間資訊集合
            List<String> namespaceInfo = STRING_SPLITTER.splitToList(key);
            if (namespaceInfo.size() != 3) {
              Tracer.logError(
                  new IllegalArgumentException(String.format("Invalid cache load key %s", key)));
              return nullConfigCacheEntry;
            }

            Transaction transaction = Tracer.newTransaction(TRACER_EVENT_CACHE_LOAD, key);
            try {
              //查詢最後配置釋出訊息
              ReleaseMessage latestReleaseMessage = releaseMessageService.findLatestReleaseMessageForMessages(Lists
                  .newArrayList(key));
              //查詢最後的配置釋出記錄
              Release latestRelease = releaseService.findLatestActiveRelease(namespaceInfo.get(0), namespaceInfo.get(1),
                  namespaceInfo.get(2));

              transaction.setStatus(Transaction.SUCCESS);

              //構建通知ID,當最後配置釋出訊息為null 通知ID=-1,標識無通知資訊
              long notificationId = latestReleaseMessage == null ? ConfigConsts.NOTIFICATION_ID_PLACEHOLDER : latestReleaseMessage
                  .getId();

              //
              if (notificationId == ConfigConsts.NOTIFICATION_ID_PLACEHOLDER && latestRelease == null) {
                return nullConfigCacheEntry;
              }

              //構建快取例項, 通知ID-最後的配置釋出記錄
              return new ConfigCacheEntry(notificationId, latestRelease);
            } catch (Throwable ex) {
              transaction.setStatus(ex);
              throw ex;
            } finally {
              transaction.complete();
            }
          }
        });
    configIdCache = CacheBuilder.newBuilder()
        .expireAfterAccess(DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES, TimeUnit.MINUTES)
        .build(new CacheLoader<Long, Optional<Release>>() {
          @Override
          public Optional<Release> load(Long key) throws Exception {
            Transaction transaction = Tracer.newTransaction(TRACER_EVENT_CACHE_LOAD_ID, String.valueOf(key));
            try {
              //查詢配置釋出訊息
              Release release = releaseService.findActiveOne(key);

              transaction.setStatus(Transaction.SUCCESS);

              return Optional.ofNullable(release);
            } catch (Throwable ex) {
              transaction.setStatus(ex);
              throw ex;
            } finally {
              transaction.complete();
            }
          }
        });
  }

  @Override
  protected Release findActiveOne(long id, ApolloNotificationMessages clientMessages) {
    Tracer.logEvent(TRACER_EVENT_CACHE_GET_ID, String.valueOf(id));
    return configIdCache.getUnchecked(id).orElse(null);
  }

  @Override
  protected Release findLatestActiveRelease(String appId, String clusterName, String namespaceName,
                                            ApolloNotificationMessages clientMessages) {
    //構建快取KEY
    String key = ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName);

    Tracer.logEvent(TRACER_EVENT_CACHE_GET, key);

    //快取獲取
    ConfigCacheEntry cacheEntry = configCache.getUnchecked(key);

    //校驗快取是否已經失效,失效更新快取
    //cache is out-dated
    if (clientMessages != null && clientMessages.has(key) &&
        clientMessages.get(key) > cacheEntry.getNotificationId()) {
      //invalidate the cache and try to load from db again
      invalidate(key);
      cacheEntry = configCache.getUnchecked(key);
    }

    return cacheEntry.getRelease();
  }

  /**
   * 校驗快取中的KEY
   * @param key
   */
  private void invalidate(String key) {
    configCache.invalidate(key);
    Tracer.logEvent(TRACER_EVENT_CACHE_INVALIDATE, key);
  }

  /**
   * 釋出訊息監聽回撥函式,用於處理新記錄的釋出回撥
   * @param message
   * @param channel
   */
  @Override
  public void handleMessage(ReleaseMessage message, String channel) {
    logger.info("message received - channel: {}, message: {}", channel, message);
    if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(message.getMessage())) {
      return;
    }

    try {
      //校驗快取
      invalidate(message.getMessage());

      //在快取中獲取當前KEY的值,用於更新快取資訊
      //warm up the cache
      configCache.getUnchecked(message.getMessage());
    } catch (Throwable ex) {
      //ignore
    }
  }

  /**
   * 釋出訊息ID與釋出記錄對應關係實體
   */
  private static class ConfigCacheEntry {
    private final long notificationId;
    private final Release release;

    public ConfigCacheEntry(long notificationId, Release release) {
      this.notificationId = notificationId;
      this.release = release;
    }

    public long getNotificationId() {
      return notificationId;
    }

    public Release getRelease() {
      return release;
    }
  }
}

ConfigServiceWithCache
帶有本地快取功能的查詢服務實現,與DefaultConfigService比多了資料本地快取功能。

ConfigService對應操作DB庫

ApolloConfigDB

相關文章