微服務系列(三)如何選擇配置中心

微服務系列(三)如何選擇配置中心

前面通過源碼分析過了註冊中心的實現,本文繼續思考如何選擇【微服務基礎設施組件:配置中心】

對於註冊中心,要對於項目特性,並結合註冊中心的功能和實現特性來決定匹配度,那麼對於配置中心,我們最應該瞭解的則是其配置信息的組織模式(決定系統的擴展性、可維護性等)、數據的存儲方式和通信方式(決定性能、響應速度)、以及組件之間的契合度(筆者認爲,如果兩個組件之間是完全解耦的又不需要較多的額外開發就能配合工作的話,那麼就認爲兩者的契合度很高)。

git上有很多開源的優秀的配置中心的實現,本文僅針對Apollo、Nacos以及Spring Cloud Config來介紹和分析,本質上spring cloud config應與前面兩者劃分開。

Apollo:呼聲很高,功能很強大,較成熟

Nacos:合併了註冊中心的功能,開發中

Spring Cloud Config:知名度高,使用者廣泛

前言

由於配置中心的底層實現與註冊中心的實現有很多相似之處,筆者將會減少源碼分析的部分,重點分析配置中心可能面臨的難題以及一些常用的解決方案。

Apollo

Apollo的README.md中有描述這樣幾個不錯的特性(附上界面圖):

  • 統一管理不同環境、不同集羣的配置
    • Apollo提供了一個統一界面集中式管理不同環境(environment)、不同集羣(cluster)、不同命名空間(namespace)的配置。
    • 同一份代碼部署在不同的集羣,可以有不同的配置,比如zk的地址等
    • 通過命名空間(namespace)可以很方便的支持多個不同應用共享同一份配置,同時還允許應用對共享的配置進行覆蓋
  • 配置修改實時生效(熱發佈)
    • 用戶在Apollo修改完配置併發布後,客戶端能實時(1秒)接收到最新的配置,並通知到應用程序。
  • 版本發佈管理
    • 所有的配置發佈都有版本概念,從而可以方便的支持配置的回滾。
  • 灰度發佈
    • 支持配置的灰度發佈,比如點了發佈後,只對部分應用實例生效,等觀察一段時間沒問題後再推給所有應用實例。
  • 權限管理、發佈審覈、操作審計
    • 應用和配置的管理都有完善的權限管理機制,對配置的管理還分爲了編輯和發佈兩個環節,從而減少人爲的錯誤。
    • 所有的操作都有審計日誌,可以方便的追蹤問題。
  • 客戶端配置信息監控
    • 可以方便的看到配置在被哪些實例使用
  • 提供Java和.Net原生客戶端
    • 提供了Java和.Net的原生客戶端,方便應用集成
    • 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便應用使用(需要Spring 3.1.1+)
    • 同時提供了Http接口,非Java和.Net應用也可以方便的使用
  • 提供開放平臺API
    • Apollo自身提供了比較完善的統一配置管理界面,支持多環境、多數據中心配置管理、權限、流程治理等特性。
    • 不過Apollo出於通用性考慮,對配置的修改不會做過多限制,只要符合基本的格式就能夠保存。
    • 在我們的調研中發現,對於有些使用方,它們的配置可能會有比較複雜的格式,如xml, json,需要對格式做校驗。
    • 還有一些使用方如DAL,不僅有特定的格式,而且對輸入的值也需要進行校驗後方可保存,如檢查數據庫、用戶名和密碼是否匹配。
    • 對於這類應用,Apollo支持應用方通過開放接口在Apollo進行配置的修改和發佈,並且具備完善的授權和權限控制
  • 部署簡單
    • 配置中心作爲基礎服務,可用性要求非常高,這就要求Apollo對外部依賴儘可能地少
    • 目前唯一的外部依賴是MySQL,所以部署非常簡單,只要安裝好Java和MySQL就可以讓Apollo跑起來
    • Apollo還提供了打包腳本,一鍵就可以生成所有需要的安裝包,並且支持自定義運行時參數

在這裏插入圖片描述

可以看到,基本涵蓋了大部分生產用的功能,是一個比較成熟的中間件,下面就繼續瞭解它的API接入文檔。

  • 爲了支持高可用,需要配置meta server
  • 支持SPI擴展,用戶可自定義尋址邏輯
  • 可配置集羣(集羣的概念是某命名空間下的多個服務組成的集合)
  • Java API接入方式,client模塊提供了ConfigService接口提供服務,包括配置拉取、配置監聽等功能
  • 官方提供了Spring整合Apollo client的文檔,但沒有提供接入Spring Cloud的實現
  • 僅支持xml、properties、xml文件格式的配置
  • 提供遷徙方案

大致整理了一下,下面通過源碼重點了解以下幾個部分:

  1. 配置meta server來支持的高可用是如何實現的,可能存在哪些問題
  2. 配置的拉取是通過什麼協議通信,配置監聽又是如何實現的
  3. Apollo client會對配置文件進行本地文件緩存,是如何緩存的呢
  4. 權限控制採用了什麼模式
  5. 客戶端配置信息監控是如何做到的
  6. Apollo server端的配置信息是如何存儲的

從官網推薦的文章上找到的一張Apollo架構圖,好讓我們初步的認識它

在這裏插入圖片描述

這張架構圖可以簡單回答問題1、4、6:

Config Service提供配置信息的管理,它的高可用依賴於Eureka集羣和Meta Service,Meta Service起中轉請求的作用,接收發現Config Service和Admin Service服務的請求,並向Eureka請求服務列表並返回給客戶端。

運維相關的控制由Admin Service承擔,它依賴於Config DB和Portal DB。

Config Service則依賴於Config DB,可以猜測到配置信息也是以表記錄的形式存儲在數據庫中。

下面就用代碼說話,完全理解Apollo的運行機制。

->apollo-configservice模塊

有metaservice和configservice兩個部分,在metaservice中發現configservice和adminservice服務發現的方式是http協議通信的方式。

@RestController
@RequestMapping("/services")
public class ServiceController {

  private final DiscoveryService discoveryService;

  private static Function<InstanceInfo, ServiceDTO> instanceInfoToServiceDTOFunc = instance -> {
    ServiceDTO service = new ServiceDTO();
    service.setAppName(instance.getAppName());
    service.setInstanceId(instance.getInstanceId());
    service.setHomepageUrl(instance.getHomePageUrl());
    return service;
  };

  public ServiceController(final DiscoveryService discoveryService) {
    this.discoveryService = discoveryService;
  }


  @RequestMapping("/meta")
  public List<ServiceDTO> getMetaService() {
    List<InstanceInfo> instances = discoveryService.getMetaServiceInstances();
    List<ServiceDTO> result = instances.stream().map(instanceInfoToServiceDTOFunc).collect(Collectors.toList());
    return result;
  }

  @RequestMapping("/config")
  public List<ServiceDTO> getConfigService(
      @RequestParam(value = "appId", defaultValue = "") String appId,
      @RequestParam(value = "ip", required = false) String clientIp) {
    List<InstanceInfo> instances = discoveryService.getConfigServiceInstances();
    List<ServiceDTO> result = instances.stream().map(instanceInfoToServiceDTOFunc).collect(Collectors.toList());
    return result;
  }

  @RequestMapping("/admin")
  public List<ServiceDTO> getAdminService() {
    List<InstanceInfo> instances = discoveryService.getAdminServiceInstances();
    List<ServiceDTO> result = instances.stream().map(instanceInfoToServiceDTOFunc).collect(Collectors.toList());
    return result;
  }
}

並且metaservice依賴了eureka的client,向eureka獲取服務列表。

@Service
public class DiscoveryService {

  private final EurekaClient eurekaClient;

  public DiscoveryService(final EurekaClient eurekaClient) {
    this.eurekaClient = eurekaClient;
  }

  public List<InstanceInfo> getConfigServiceInstances() {
    Application application = eurekaClient.getApplication(ServiceNameConsts.APOLLO_CONFIGSERVICE);
    if (application == null) {
      Tracer.logEvent("Apollo.EurekaDiscovery.NotFound", ServiceNameConsts.APOLLO_CONFIGSERVICE);
    }
    return application != null ? application.getInstances() : Collections.emptyList();
  }

  public List<InstanceInfo> getMetaServiceInstances() {
    Application application = eurekaClient.getApplication(ServiceNameConsts.APOLLO_METASERVICE);
    if (application == null) {
      Tracer.logEvent("Apollo.EurekaDiscovery.NotFound", ServiceNameConsts.APOLLO_METASERVICE);
    }
    return application != null ? application.getInstances() : Collections.emptyList();
  }

  public List<InstanceInfo> getAdminServiceInstances() {
    Application application = eurekaClient.getApplication(ServiceNameConsts.APOLLO_ADMINSERVICE);
    if (application == null) {
      Tracer.logEvent("Apollo.EurekaDiscovery.NotFound", ServiceNameConsts.APOLLO_ADMINSERVICE);
    }
    return application != null ? application.getInstances() : Collections.emptyList();
  }
}

跟我們平時寫的業務代碼沒有什麼分別…

也就是說,Config Service和Admin Service的高可用是依賴於eureka來解決的,由於這兩個服務的數據存儲依賴於數據庫,所以無需進行數據同步,只需要保證連接同一數據源即可,對於客戶端而言,無論請求到哪一臺service,最終均會根據同一數據源的數據來進行處理返回,從而解決單點故障的問題。

那麼繼續看,是否完全依賴數據庫?

從configservice包可以看到,其對外提供服務依舊是通過http接口,服務層有一個核心服務類com.ctrip.framework.apollo.configservice.service.config.ConfigService

它有兩種實現類,com.ctrip.framework.apollo.configservice.service.config.ConfigServiceWithCachecom.ctrip.framework.apollo.configservice.service.config.DefaultConfigService

從controller出發

->com.ctrip.framework.apollo.configservice.service.config.ConfigService#loadConfig

->com.ctrip.framework.apollo.configservice.service.config.AbstractConfigService#loadConfig

->com.ctrip.framework.apollo.configservice.service.config.AbstractConfigService#findRelease

->com.ctrip.framework.apollo.configservice.service.config.DefaultConfigService#findActiveOnecom.ctrip.framework.apollo.configservice.service.config.DefaultConfigService#findLatestActiveRelease

最終定位到com.ctrip.framework.apollo.biz.service.ReleaseService

內部持有一個Repository類com.ctrip.framework.apollo.biz.repository.ReleaseRepository

(ps. 這裏依賴了spring data模塊的repository接口)

最終發現,其通過對數據庫操作來獲取配置信息,驗證猜想。

值得一提的是ConfigServiceWithCache,它是有緩存的實現類,它的緩存機制實現的非常簡單,通過在JVM中持有map並定期輪詢數據庫來更新,它通過參數config-service.cache.enabled決定是否開啓。

到這裏,驗證了問題1、6的猜測,並且可以總結出問題1、6的答案:

  1. Apollo的高可用依賴於eureka集羣和數據庫服務,首先要保證數據庫服務(可以爲集羣)穩定運行,一旦數據庫服務出現問題,那麼Apollo上層Config Service和Admin Service均無法正常提供服務,從Config Service的實現可以看到,其通過增加緩存來保護這一情況的發生,一旦數據庫服務出現問題,那麼寫服務將癱瘓但讀服務依舊可以正常使用;其次其依賴eureka集羣來進行服務發現,eureka集羣通過冗餘備份的方式來保證高可用,雖然eureka和數據庫服務的可靠性非常高,但apollo依舊依賴了較多的第三方服務,其可靠性也因此降低。
  2. 目前看來,Apollo的數據存儲(至少配置信息和權限信息)是依賴於數據庫存儲,同時提供了緩存,執行效率是比較高的,但也可能因此成爲性能瓶頸,數據庫的讀寫性能取決於IO性能,可能會因爲索引重建等問題而成爲apollo的性能瓶頸,但目前來說還遇不到像消息系統那樣規模的場景,所以實際上在性能上沒有太大的分別。

繼續瞭解權限部分是如何控制的,首先我們知道apollo是依賴數據庫來進行數據存儲的,那麼權限部分很可能就是通過表設計來控制,首先找到提供權限控制的入口:

com.ctrip.framework.apollo.portal.spi.configuration.AuthConfiguration

com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService

可以發現,它依賴了spring security框架,在其上實現權限控制的效果,權限控制關係由這幾個表表示:

Users Authorities Permission Role RolePermission

這裏就不介紹了。

以上均是apollo服務端的實現,接着進入客戶端的實現部分。

com.ctrip.framework.apollo.internals.DefaultConfigManager

這個類用於與服務端通信來獲取配置信息

個人覺得這部分的實現設計的很好,美中不足在於沒有抽象到接口

例如這裏有RepositoryFactoryManager/FactoryManagerConfig/ConfigFile四個概念

最外層是Service,service對外提供服務

service內部,由Manager管理配置,這一層級存在一級緩存(JVM層)

Manager內部存在FactoryManager用於管理不同namespace的Factory,這樣就很好的隔離了不同namespace

每個Factory則僅負責該namespace下的Config的創建

每個Config內部抽象了自動刷新的功能,監聽了配置變化

而與遠程通信的部分由Repository承擔,Repository有本地實現和遠程實現,這裏使用了裝飾者模式

Config內部持有一個LocalFileConfigRepository

LocalFileConfigRepository內部持有RemoteConfigRepository

這樣又很好的解耦了遠程與本地的配置信息管理

那麼與服務端的通信都封裝在RemoteConfigRepository

感興趣的可以直接看這段邏輯com.ctrip.framework.apollo.internals.RemoteConfigRepository#sync

->com.ctrip.framework.apollo.internals.RemoteConfigRepository#loadApolloConfig

->HttpResponse<ApolloConfig> response = m_httpUtil.doGet(request, ApolloConfig.class);

最終定位到【老套路】,發起http請求,獲取響應,解析…

那麼監聽又是如何實現的?

當本地配置倉庫同步信息成功,感應到配置信息變更後會調用

->com.ctrip.framework.apollo.internals.LocalFileConfigRepository#onRepositoryChange

->com.ctrip.framework.apollo.internals.AbstractConfigRepository#fireRepositoryChange

觸發listeners集合的onRepositoryChange()方法

而listeners集合是在Config被構造出來的時候進行初始化的過程中將自身作爲監聽器add進去,這樣就實現了一個【Config內部抽象了自動刷新的功能,監聽了配置變化】的Config對象

觸發同步的時機有這樣幾種情況:

  1. 初始化時同步一次
  2. 獲取配置時如果沒有命中緩存,則同步一次
  3. 定期同步(默認5秒)

這樣就基本保證了客戶端儘快感知到配置變更情況。(官方稱實時性在1s以內)

而在這段代碼的閱讀中,對於問題5也得到了回答:

以sync過程爲例:

Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");
...
try {
      ...

      transaction.setStatus(Transaction.SUCCESS);
    } catch (Throwable ex) {
      transaction.setStatus(ex);
      throw ex;
    } finally {
      transaction.complete();
    }

由一個Transaction對象來存儲監控信息。

通過反射構造Transaction對象的Factory對象的Method對象,這裏聽起來有點繞,不理解的話最好是追蹤一下源碼。

最終由Method對象來構造Transaction對象。

到這裏,只剩下最後一個client如何進行文件緩存的問題:

前面說到本地配置信息的類是com.ctrip.framework.apollo.internals.LocalFileConfigRepository

直接進入這個類,一切謎底就揭曉了…

-> om.ctrip.framework.apollo.internals.LocalFileConfigRepository#updateFileProperties

-> com.ctrip.framework.apollo.internals.LocalFileConfigRepository#persistLocalCacheFile

->java.util.Properties#store(java.io.OutputStream, java.lang.String)

最終將Properties類信息寫入文件,讀文件反之

->com.ctrip.framework.apollo.internals.LocalFileConfigRepository#loadFromLocalCacheFile

->java.util.Properties#load(java.io.InputStream)

寫的時機:

  1. 嘗試同步上游倉庫(遠程倉庫)的信息,發現信息有變更時,寫入本地緩存
  2. 監聽到倉庫配置信息變化時,寫入本地緩存(可以看出,這樣做的效率很低,如果頻繁更改則會頻繁IO)

讀的時機:

  1. 當讀取本地倉庫的信息時,發現未命中緩存,則發起一次同步,同步後依然沒有命中,則讀本地緩存文件並同步到本地倉庫

總結以上六個問題:

  1. 配置meta server來支持的高可用是如何實現的,可能存在哪些問題
  2. 配置的拉取是通過什麼協議通信,配置監聽又是如何實現的
  3. Apollo client會對配置文件進行本地文件緩存,是如何緩存的呢
  4. 權限控制採用了什麼模式
  5. 客戶端配置信息監控是如何做到的
  6. Apollo server端的配置信息是如何存儲的

答:

  1. Config Service提供配置信息的管理,它的高可用依賴於Eureka集羣和Meta Service,Meta Service起中轉請求的作用,接收發現Config Service和Admin Service服務的請求,並向Eureka請求服務列表並返回給客戶端。
  2. http協議,監聽是通過定時輪詢+主動拉取的方式
  3. 使用java.util.Properties類,直接調用store、load方法來讀寫(每次監聽到變化均會寫,變化頻繁時系統壓力較大)
  4. apollo的權限控制基於Spring Security框架實現,並依賴了數據庫
  5. 通過com.ctrip.framework.apollo.tracer.Tracer類來記錄信息,具體實現可自行跟蹤源碼
  6. 依賴數據庫,目前支持mysql,數據持久層使用了Spring Data的Repository。

ps.吐槽一句,apollo的代碼還是非常適合新手看的,典型的web MVC的風格,大量依賴Spring框架,底層的抽象部分做的還是欠缺了一些,可能最開始設計的時候沒有考慮過多的情況,但從結果上看還是非常成功的。

Nacos

關於Nacos的config server的實現,相對於naming server簡單的多。

目前Nacos已發佈第一個RC版本:1.0.0-RC1

那麼源碼部分的分析以該版本爲準。

要知道,作爲開發者,需要構建一個穩定可靠的微服務環境,對於配置中心這樣的公用基礎設施,最爲重要的則是服務的可用性。

基於以下幾個核心問題的考量來分析:

  1. 高可用如何做?
  2. 接入方式如何?
  3. C/S間採用怎樣的通信協議?
  4. 提供了怎樣的特性?

首先從官網找入口,Config Service的sdk入口:

https://nacos.io/zh-cn/docs/sdk.html

定位到client模塊的com.alibaba.nacos.api.config.ConfigService這個接口

繼續進入它的實現類com.alibaba.nacos.client.config.NacosConfigService

以getConfig爲例追蹤

->com.alibaba.nacos.client.config.NacosConfigService#getConfig

->com.alibaba.nacos.client.config.NacosConfigService#getConfigInner

->(如果在本地緩存沒有找到)com.alibaba.nacos.client.config.impl.ClientWorker#getServerConfig

->result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);

->最終調用HttpSimpleClient.httpGet(),可以發現其底層封裝了HttpURLConnection來進行http請求

那麼可以追蹤到請求的url爲 /v1/cs/configs

通過全局搜索,定位到com.alibaba.nacos.config.server.controller.ConfigController

定位到方法->com.alibaba.nacos.config.server.controller.ConfigController#getConfig

->com.alibaba.nacos.config.server.controller.ConfigServletInner#doGetConfig

->(如果內存緩存中沒有找到)

if (STANDALONE_MODE && !PropertyUtil.isStandaloneUseMysql()) {
    configInfoBase = persistService.findConfigInfo4Beta(dataId, group, tenant);
} else {
    file = DiskUtil.targetBetaFile(dataId, group, tenant);
}

這段代碼告訴我們,當使用單機模式且不使用mysql作爲數據源時,使用persistService獲取,而persisteService則是以debyDB爲數據源來實現的,即依賴debyDB數據庫來進行數據存儲;當處於集羣模式或使用mysql作爲數據源時,從文件中獲取配置信息,並通過transferTo的API將文件中的內容寫入響應outputStream。

(這裏使用了零拷貝提高性能)

到這裏,還是疑惑的,沒有看到有關於可用性的代碼部分,如果有多個節點,文件之間是如何保持同步的呢?還是採用冗餘備份+低一致性的方式呢?

那麼,就要知道這個文件是如何產生的。

筆者這裏直接貼出來入口:com.alibaba.nacos.config.server.service.dump.DumpProcessor#process

定位的過程就不做介紹了,大概想法是筆者認爲既然沒有找到節點間數據同步的部分,那麼很可能是依賴數據庫的可用性,文件只是作爲緩存而存在。

ConfigInfo4Beta cf = dumpService.persistService.findConfigInfo4Beta(dataId, group, tenant);
boolean result;
if (null != cf) {
    result = ConfigService.dumpBeta(dataId, group, tenant, cf.getContent(), lastModified, cf.getBetaIps());
    if (result) {
        ConfigTraceService.logDumpEvent(dataId, group, tenant, null, lastModified, handleIp,
            ConfigTraceService.DUMP_EVENT_OK, System.currentTimeMillis() - lastModified,
            cf.getContent().length());
    }
} else {
    result = ConfigService.removeBeta(dataId, group, tenant);
    if (result) {
        ConfigTraceService.logDumpEvent(dataId, group, tenant, null, lastModified, handleIp,
            ConfigTraceService.DUMP_EVENT_REMOVE_OK, System.currentTimeMillis() - lastModified, 0);
    }
}

最終猜想驗證,Nacos的高可用依賴於數據庫,如果是單機模式且不使用mysql的情況,則無需進行文件緩存,直接從數據源獲取;而如果是集羣模式或使用mysql的情況,則啓動dump線程,定期將數據源中的信息dump到文件(作爲緩存,僅提供讀),值得注意的是,爲了保證數據,不能僅僅通過定期dump來保持文件和數據源之間的數據同步,而是寫入數據庫的同時通過異步通知來令數據寫入文件緩存。

ps.那麼就會存在一個小問題,如果server cpu壓力大或IO壓力大的情況下,可能存在寫入配置後不能第一時間感知到配置的變更。

繼續思考問題2,這個問題其實官網已經給出了答案,前文的https://nacos.io/zh-cn/docs/sdk.html。

Nacos提供了client模塊,由用戶來構建ConfigService對象,並使用其上API來接入。

這樣的方式與Apollo一致,比起直接提供restful api而言更加方便,無需用戶封裝http client請求,對用戶更加友好。

另外,在Spring Cloud Alibaba工程也提供了Spring Cloud Alibaba Config的接入方式,用戶可以通過直接引入jar包,無需配置、直接接入。

指的注意的是,Nacos提供了兩種部署方式:

一、Naming Server和Config Server合併部署

二、Naming Server和Config Server分離部署

於是,Spring Cloud Alibaba依然提供了Spring Cloud Alibaba Config Server的支持,這樣就大大簡化了Spring Cloud工程的接入。

到這裏問題3得到了部分的回答,爲什麼說是部分呢?雖然Config Server對外提供的大部分功能是使用了http協議,但其實現了配置監聽的功能,需要了解其監聽的方式是如何實現的,這樣才能完整的回答問題3。

首先讀完前文,我們知道了Apollo的監聽實現方式採用了定時輪詢+(緩存未命中時)主動同步拉取的方式。

繼續追蹤Nacos監聽配置的過程:

->com.alibaba.nacos.client.config.NacosConfigService#addListener

->com.alibaba.nacos.client.config.impl.ClientWorker#addTenantListeners

->com.alibaba.nacos.client.config.impl.CacheData#addListener

->listeners.addIfAbsent(wrap)

這裏實現的過程比較複雜,就不貼源碼了。

最終將listener包裝後(先不論包裝類起了作用)添加到CacheData持有的listeners中。

(每個CacheData對應了由dataId+group組裝成的key,即某一數據中心下的某個組的配置的數據均存儲在一個CacheData中。)

並開啓多個LongPollingRunnable(一個task對應處理多個CacheData)任務,不斷輪詢,從Config Server拉取信息並寫入其對應的CacheData,一旦檢測到CacheData的內容發生了變化,則回調一次(用戶自定義邏輯的)監聽器,並且Nacos將長輪詢任務使用的線程池和通知任務使用的線程池分離,異步通知。

ps.這裏使用了MD5來區分內容是否發生變化,主要考量是MD5的加密速度較快,而無需使用equals的方式在每次變化時都要解析一次,這樣的加速了比較內容是否發生變化的過程。

可以看到,與Apollo的方式有些不同,Nacos採用長輪詢的方式監聽配置的變化。

可以看到兩者的效果異同之處:

  1. Nacos使用Cache線程池,且無間斷的輪詢,那麼基本可以保證第一時間監聽到配置變化,但如果不同CacheData的監聽器過多的情況下,則會發起多個無間隔長輪詢任務,且會無限制的發起新的輪詢任務,造成客戶端大量時間令CPU空轉,浪費CPU資源。
  2. Apollo則由於使用了同步加鎖的方式,並捨棄了部分響應速度來保證系統的性能,相對而言的問題也很明顯,配置監聽的響應時間不夠快,且如果出現某客戶端不斷獲取服務端不存在的配置信息時,則會加大網絡IO的壓力,另一方面,Apollo比對內容異同的方式是採用equals,相對MD5的方式性能相對較弱。

ps.所以本質上監聽的方式也是使用了http協議,週期性請求。

而對於問題4:

相對Apollo,Nacos沒有配置繼承的概念,從實現上看,後獲取的配置會覆蓋先獲取的配置。

Nacos的接入方式、部署方式相對容易的多。

從代碼上看,Nacos在很多地方運用了異步,看起來能提高系統的吞吐量,但還需要經歷考驗。

所以,似乎也沒有特別亮眼的特性,或許AP/CP/MIX模式是一個比較容易引起注意的功能,但目前看來似乎沒有太大的優勢,這麼做僅僅是爲了舊版本兼容。

不過,Nacos官網提供了多註冊中心的同步組件https://nacos.io/zh-cn/docs/nacos-sync.html,用於遷徙還是非常方便的。

Spring Cloud Config

由於Spring Cloud Config是基於Spring Boot來開發的,源碼分析的部分將會減少,讀者可以根據需要閱讀Spring及Spring Boot源碼。

使用Spring Cloud Config的方式很簡單,在一個Spring Boot應用上使用@EnableConfigServer註解,即激活啓動Spring Cloud Config Server。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigServerConfiguration.class)
public @interface EnableConfigServer {

}

可以看到其導入了ConfigServerConfiguration.class

@Configuration
public class ConfigServerConfiguration {
   class Marker {}

   @Bean
   public Marker enableConfigServerMarker() {
      return new Marker();
   }
}

並通過內部類Marker開啓ConfigServerAutoConfiguration自動配置

@Configuration
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)
@EnableConfigurationProperties(ConfigServerProperties.class)
@Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class,
      ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class })
public class ConfigServerAutoConfiguration {

}

到這裏可以看到,最終進行了一些配置,而主要初始化的內容包括:

  • EnvironmentRepositoryConfiguration:配置中心核心類的配置和注入,包含了各種具體實現,如Git、fileSystem、Db、Consul
  • CompositeConfiguration:如果存在多個配置中心的實現,則進行該配置,將各個實現類綜合到一個類中來管理
  • ResourceRepositoryConfiguration:資源倉庫的配置,用於加載並讀取本地文件
  • ConfigServerEncryptionConfiguration:加密配置,將publicKey等加密信息通過http接口的形式提供給其他客戶端(保證僅有集羣內的節點能對信息進行解讀)
  • ConfigServerMvcConfiguration:MVC配置,將EnvironmentController(提供配置獲取等接口)、ResourceController(提供資源獲取等接口)注入到Spring容器

大致瞭解了其配置的內容後,進入核心的配置類EnvironmentRepositoryConfiguration瞭解一下其實現:

以其默認實現git爲例,從代碼邏輯上看,默認配置下生效並注入的代碼有以下

	@Bean
	@ConditionalOnProperty(value = "spring.cloud.config.server.health.enabled", matchIfMissing = true)
	public ConfigServerHealthIndicator configServerHealthIndicator(
			EnvironmentRepository repository) {
		return new ConfigServerHealthIndicator(repository);
	}

	@Bean
	@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
	public MultipleJGitEnvironmentProperties multipleJGitEnvironmentProperties() {
		return new MultipleJGitEnvironmentProperties();
	}

	@Configuration
	@ConditionalOnMissingBean(EnvironmentWatch.class)
	protected static class DefaultEnvironmentWatch {

		@Bean
		public EnvironmentWatch environmentWatch() {
			return new EnvironmentWatch.Default();
		}
	}

    @Configuration
    @ConditionalOnClass(TransportConfigCallback.class)
    static class JGitFactoryConfig {

        @Bean
        public MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory(
                ConfigurableEnvironment environment, ConfigServerProperties server,
                Optional<ConfigurableHttpConnectionFactory> jgitHttpConnectionFactory,
                Optional<TransportConfigCallback> customTransportConfigCallback) {
            return new MultipleJGitEnvironmentRepositoryFactory(environment, server, jgitHttpConnectionFactory,
					customTransportConfigCallback);
        }
    }

    @Configuration
    @ConditionalOnClass({ HttpClient.class, TransportConfigCallback.class })
    static class JGitHttpClientConfig {

        @Bean
        public ConfigurableHttpConnectionFactory httpClientConnectionFactory() {
            return new HttpClientConfigurableHttpConnectionFactory();
        }
    }

@Configuration
@ConditionalOnMissingBean(value = EnvironmentRepository.class, search = SearchStrategy.CURRENT)
class DefaultRepositoryConfiguration {
	@Autowired
	private ConfigurableEnvironment environment;

	@Autowired
	private ConfigServerProperties server;

	@Autowired(required = false)
	private TransportConfigCallback customTransportConfigCallback;

	@Bean
	public MultipleJGitEnvironmentRepository defaultEnvironmentRepository(
	        MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory,
			MultipleJGitEnvironmentProperties environmentProperties) throws Exception {
		return gitEnvironmentRepositoryFactory.build(environmentProperties);
	}
}

而new EnvironmentWatch.Default()是一個空實現,所以實際上並沒有起作用。

該接口可用於實現配置監聽,可以參考Consul的實現:org.springframework.cloud.config.server.environment.ConsulEnvironmentWatch

最終發現,默認配置下,其最終注入了這樣一個類MultipleJGitEnvironmentRepository.class

且它是由MultipleJGitEnvironmentRepositoryFactory的build方法,並傳入MultipleJGitEnvironmentProperties參數構造出的bean。

org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepositoryFactory#build

從方法中可以看到,對MultipleJGitEnvironmentRepository進行了transport相關配置。

進入org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository這個核心類

首先不關心其實現(無非就是配置的存儲、解析、讀取),作爲Spring家族的成員,最應該學習其思想,先觀察其結構:

![Spring Cloud Config類圖](/img/Spring Cloud Config類圖.jpg)

從名稱可以看出,

Abstract作爲模板抽象類

JGit*作爲git的原生實現類

Multiple*則作爲聚合多個JGit的實現類

且均實現了SearchPathLocator接口,該接口用於定位本地git倉庫的路徑

ResourceLoaderAware則是用於注入ResourceLoader,可以進行資源加載的操作。

現在進入MultipleJGitEnvironmentRepository類,可以發現其僅實現了afterPropertiesSet()、getLocations()、findOne()

從實現上看也可以很容易瞭解JGit實現邏輯,一頓git的命令,拉到本地然後加載到內存(由於利用了git,可以比對版本,所以只有在版本有變更時拉取最新數據並重新加載文件)。

可以發現,用git來進行配置的管理相對來說更加笨重,每次獲取配置時都可能經歷耗時的拉取和加載文件的操作,但通過閱讀這部分源碼,認識到Spring框架的強大之處。

假設我們需要實現帶緩存的git配置中心,僅需要繼承AbstractScmEnvironmentRepository並向Spring容器注入該bean,大部分工作均由Spring框架來完成了。

另外,還有一個需要注意的類,有助於我們基於Spring Cloud Config來實現定製化的配置中心:

ConfigServerHealthIndicator:用於心跳檢測,Spring也做了非常方便的封裝,我們可以通過繼承org.springframework.boot.actuate.health.AbstractHealthIndicator來實現我們自己的心跳檢測器

結合前文提到的另一個org.springframework.cloud.config.server.environment.EnvironmentWatch

就能實現配置中心這兩個重要特性:

  1. 配置監聽
  2. 心跳檢測

另外,Spring環境中的配置信息是以org.springframework.core.env.PropertySources類存在,從Spring Cloud Config環境倉庫獲取到的配置信息會最終轉換爲PropertySource並插入到PropertySources中,形成Spring環境的一部分,由用戶使用Spring 的方式(如@Value等)來使用。

小結

Apollo的實現雖然優雅程度不高,但五臟六腑俱全,成功的經歷了很多考驗,社區也很活躍,易於解決問題。

Nacos的實現從設計上看是可行的,性能等指標相比Apollo應該更具優勢,且提供了方便的接入方案以及Spring Cloud Alibaba的支持,更加方便於Spring Cloud用戶接入。

Spring Cloud Config則顯得跟這兩者沒有可比性,本質上Spring Cloud Config也是提供了幾種配置中心的實現,但無論是git還是svn,其實都是相對來說不太適合作爲配置中心的實現,雖然其也實現了db,與Apollo和Nacos相比,在對db的管控、架構方面的考慮等方面都有所欠缺。

所以筆者認爲Spring Cloud Config應該與Apollo、Nacos這樣的配置中心中間件劃分開,其雖然有默認的實現,本身能作爲一個配置中心中間件存在,但其最大的優勢是具備高度抽象的能力,可以在Spring應用中完美接入其他第三方中間件,例如其就實現了Consul。

ps.對於Spring Cloud應用,Nacos也提供了Spring Cloud Config的實現,感興趣的可以關注https://github.com/spring-cloud-incubator/spring-cloud-alibaba

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章