1. 程式人生 > 實用技巧 >攜程架構部開源的配置中心Apollo深度解讀

攜程架構部開源的配置中心Apollo深度解讀

隨著程式功能的日益複雜,程式的配置日益增多:各種功能的開關、引數的配置、伺服器的地址……

對程式配置的期望值也越來越高:配置修改後實時生效,灰度釋出,分環境、分叢集管理配置,完善的許可權、稽核機制……

在這樣的大環境下,傳統的通過配置檔案、資料庫等方式已經越來越無法滿足開發人員對配置管理的需求。

想必大家都深有體會,我們做的專案都伴隨著各種配置檔案,而且總是本地配置一套配置檔案,測試服配置一套,正式服配置一套,有時候一不小心就改錯了,捱罵是小事,扣績效那可就鬧大了。

而且每當專案釋出的時候,配置檔案也會被打包進去,也就是配置檔案會跟著專案一起釋出。然後每次出現問題需要我們修改配置檔案的時候,我們總是得先在本地修改,然後重新發布才能讓新的配置生效。

當請求壓力越來越大,你的專案也會從 1 個節點變成多個節點,這個時候如果配置需要發生變化,對應的修改操作也是相同的,只需要在專案中修改一次即可,但對於釋出操作工作量就比之前大了很多,因為要釋出多個節點。

修改這些配置,增加的釋出的工作量降低了整體的工作效率,為了能夠提升工作效率,配置中心應運而生了,我們可以將配置統一存放在配置中心來進行管理。

總體而言在沒有引入配置中心之前,我們都會面臨以下問題:

  1. 配置散亂格式不標準,有的用 properties 格式,有的用 xml 格式,還有的存 DB,團隊傾向自造輪子,做法五花八門。

  2. 主要採用本地靜態配置,配置修改麻煩,配置修改一般需要經過一個較長的測試釋出週期。在分散式微服務環境下,當服務例項很多時,修改配置費時費力。

  3. 易引發生產事故,這個是我親身經歷,之前在一家網際網路公司,有團隊在釋出的時候將測試環境的配置帶到生產上,引發百萬級資損事故。

  4. 配置缺乏安全審計和版本控制功能,誰改的配置?改了什麼?什麼時候改的?無從追溯,出了問題也無法及時回滾。

  5. 增加了運維小哥哥的工作量,極大的損害了運維小哥哥和開發小哥哥的基情。

到底什麼是配置中心

配置中心就是把專案中各種個樣的配置、引數、開關,全部都放到一個集中的地方進行統一管理,並提供一套標準的介面。當各個服務需要獲取配置的時候,就來配置中心的介面拉取。當配置中心中的各種引數有更新的時候,也能通知到各個服務實時的過來同步最新的資訊,使之動態更新。

Apollo 簡介

Apollo(阿波羅)是攜程框架部門研發的開源配置管理中心,能夠集中化管理應用不同環境、不同叢集的配置,配置修改後能夠實時推送到應用端,並且具備規範的許可權、流程治理等特性。

Apollo支援4個維度管理Key-Value格式的配置(下面會詳細說明):

1、application (應用)
2、environment (環境)

3、cluster (叢集)
4、namespace (名稱空間)

什麼是配置

既然Apollo定位於配置中心,那麼在這裡有必要先簡單介紹一下什麼是配置。

按照我們的理解,配置有以下幾個屬性:

  • 配置是獨立於程式的只讀變數

    • 配置首先是獨立於程式的,同一份程式在不同的配置下會有不同的行為。

    • 其次,配置對於程式是隻讀的,程式通過讀取配置來改變自己的行為,但是程式不應該去改變配置。

    • 常見的配置有:DB Connection Str、Thread Pool Size、Buffer Size、Request Timeout、Feature Switch、Server Urls等。

  • 配置伴隨應用的整個生命週期

    • 配置貫穿於應用的整個生命週期,應用在啟動時通過讀取配置來初始化,在執行時根據配置調整行為。

  • 配置可以有多種載入方式

    • 配置也有很多種載入方式,常見的有程式內部hard code,配置檔案,環境變數,啟動引數,基於資料庫等

  • 配置需要治理

    • 還有一類比較特殊的配置 - 框架類元件配置,比如CAT客戶端的配置。

    • 雖然這類框架類元件是由其他團隊開發、維護,但是執行時是在業務實際應用內的,所以本質上可以認為框架類元件也是應用的一部分。

    • 這類元件對應的配置也需要有比較完善的管理方式。

    • 同一份程式在不同的環境(開發,測試,生產)、不同的叢集(如不同的資料中心)經常需要有不同的配置,所以需要有完善的環境、叢集配置管理

    • 由於配置能改變程式的行為,不正確的配置甚至能引起災難,所以對配置的修改必須有比較完善的許可權控制

    • 許可權控制

    • 不同環境、叢集配置管理

    • 框架類元件配置管理

為什麼使用Apollo,Apollo有哪些特徵

正是基於配置的特殊性,所以Apollo從設計之初就立志於成為一個有治理能力的配置釋出平臺,目前提供了以下的特性:

  • 統一管理不同環境、不同叢集的配置

    • Apollo提供了一個統一介面集中式管理不同環境(environment)、不同叢集(cluster)、不同名稱空間(namespace)的配置。

    • 同一份程式碼部署在不同的叢集,可以有不同的配置,比如zookeeper的地址等

    • 通過名稱空間(namespace)可以很方便地支援多個不同應用共享同一份配置,同時還允許應用對共享的配置進行覆蓋

  • 配置修改實時生效(熱釋出)

    • 使用者在Apollo修改完配置併發布後,客戶端能實時(1秒)接收到最新的配置,並通知到應用程式

  • 版本釋出管理

    • 所有的配置釋出都有版本概念,從而可以方便地支援配置的回滾

  • 灰度釋出

    • 支援配置的灰度釋出,比如點了釋出後,只對部分應用例項生效,等觀察一段時間沒問題後再推給所有應用例項

  • 許可權管理、釋出稽核、操作審計

    • 應用和配置的管理都有完善的許可權管理機制,對配置的管理還分為了編輯和釋出兩個環節,從而減少人為的錯誤。

    • 所有的操作都有審計日誌,可以方便地追蹤問題

  • 客戶端配置資訊監控

    • 可以在介面上方便地看到配置在被哪些例項使用

  • 提供Java和.Net原生客戶端

    • 提供了Java和.Net的原生客戶端,方便應用整合

    • 支援Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便應用使用(需要Spring 3.1.1+)

    • 同時提供了Http介面,非Java和.Net應用也可以方便地使用

  • 提供開放平臺API

    • Apollo自身提供了比較完善的統一配置管理介面,支援多環境、多資料中心配置管理、許可權、流程治理等特性。不過Apollo出於通用性考慮,不會對配置的修改做過多限制,只要符合基本的格式就能儲存,不會針對不同的配置值進行鍼對性的校驗,如資料庫使用者名稱、密碼,Redis服務地址等

    • 對於這類應用配置,Apollo支援應用方通過開放平臺API在Apollo進行配置的修改和釋出,並且具備完善的授權和許可權控制

  • 部署簡單

    • 配置中心作為基礎服務,可用性要求非常高,這就要求Apollo對外部依賴儘可能地少

    • 目前唯一的外部依賴是MySQL,所以部署非常簡單,只要安裝好Java和MySQL就可以讓Apollo跑起來

    • Apollo還提供了打包指令碼,一鍵就可以生成所有需要的安裝包,並且支援自定義執行時引數

Apollo整體架構

首先我們來看看Applo的基本工作流程如下圖所示

1.使用者在配置中心對配置進行修改併發布
2.配置中心通知Apollo客戶端有配置更新
3.Apollo客戶端從配置中心拉取最新的配置、更新本地配置並通知到應用

接下來我們來看看Apollo的整體架構圖

上圖簡要描述了Apollo的總體設計,我們可以從下往上看:

  • Config Service提供配置的讀取、推送等功能,服務物件是Apollo客戶端

  • Admin Service提供配置的修改、釋出等功能,服務物件是Apollo Portal(管理介面)

  • Config Service和Admin Service都是多例項、無狀態部署,所以需要將自己註冊到Eureka中並保持心跳

  • 在Eureka之上我們架了一層Meta Server用於封裝Eureka的服務發現介面

  • Client通過域名訪問Meta Server獲取Config Service服務列表(IP+Port),而後直接通過IP+Port訪問服務,同時在Client側會做load balance、錯誤重試

  • Portal通過域名訪問Meta Server獲取Admin Service服務列表(IP+Port),而後直接通過IP+Port訪問服務,同時在Portal側會做load balance、錯誤重試

  • 為了簡化部署,我們實際上會把Config Service、Eureka和Meta Server三個邏輯角色部署在同一個JVM程序中

Why Eureka

為什麼我們採用Eureka作為服務註冊中心,而不是使用傳統的zk、etcd呢?我大致總結了一下,有以下幾方面的原因:

  • 它提供了完整的Service Registry和Service Discovery實現
    首先是提供了完整的實現,並且也經受住了Netflix自己的生產環境考驗,相對使用起來會比較省心。
    和Spring Cloud無縫整合

  • 的專案本身就使用了Spring Cloud和Spring Boot,同時Spring Cloud還有一套非常完善的開原始碼來整合Eureka,所以使用起來非常方便。

  • 另外,Eureka還支援在我們應用自身的容器中啟動,也就是說我們的應用啟動完之後,既充當了Eureka的角色,同時也是服務的提供者。這樣就極大的提高了服務的可用性。

  • 這一點是我們選擇Eureka而不是zk、etcd等的主要原因,為了提高配置中心的可用性和降低部署複雜度,我們需要儘可能地減少外部依賴。
    Open Source

  • 最後一點是開源,由於程式碼是開源的,所以非常便於我們瞭解它的實現原理和排查問題。

各模組概要介紹

Config Service

  • 提供配置獲取介面

  • 提供配置更新推送介面(基於Http long polling)

  • 服務端使用Spring DeferredResult實現非同步化,從而大大增加長連線數量

  • 目前使用的tomcat embed預設配置是最多10000個連線(可以調整),使用了4C8G的虛擬機器實測- 可以支撐10000個連線,所以滿足需求(一個應用例項只會發起一個長連線)。介面服務物件為Apollo客戶端

Admin Service

  • 提供配置管理介面

  • 提供配置修改、釋出等介面

  • 介面服務物件為Portal

Meta Server

  • Portal通過域名訪問Meta Server獲取Admin Service服務列表(IP+Port)

  • Client通過域名訪問Meta Server獲取Config Service服務列表(IP+Port)

  • Meta Server從Eureka獲取Config Service和Admin Service的服務資訊,相當於是一個Eureka Client
    增設一個Meta Server的角色主要是為了封裝服務發現的細節,對Portal和Client而言,永遠通過一個

  • Http介面獲取Admin Service和Config Service的服務資訊,而不需要關心背後實際的服務註冊和發現元件

  • Meta Server只是一個邏輯角色,在部署時和Config Service是在一個JVM程序中的,所以IP、埠和Config Service一致

Eureka

  • 基於Eureka和Spring Cloud Netflix提供服務註冊和發現

  • Config Service和Admin Service會向Eureka註冊服務,並保持心跳

  • 為了簡單起見,目前Eureka在部署時和Config Service是在一個JVM程序中的(通過Spring Cloud Netflix)

Portal

  • 提供Web介面供使用者管理配置

  • 通過Meta Server獲取Admin Service服務列表(IP+Port),通過IP+Port訪問服務

  • 在Portal側做load balance、錯誤重試

Client

  • Apollo提供的客戶端程式,為應用提供配置獲取、實時更新等功能

  • 通過Meta Server獲取Config Service服務列表(IP+Port),通過IP+Port訪問服務

  • 在Client側做load balance、錯誤重試

Apollo核心概念介紹

1、application

1、Apollo 客戶端在執行時需要知道當前應用是誰,從而可以根據不同的應用來獲取對應應用的配置。

2、每個應用都需要有唯一的身份標識,可以在程式碼中配置 app.id 引數來標識當前應用,Apollo 會根據此指來辨別當前應用。

2、environment

在實際開發中,我們的應用經常要部署在不同的環境中,一般情況下分為 開發、測試、生產 等等不同環境,不同環境中的配置也是不同的,在 Apollo 中預設提供了

四種環境:

FAT:功能測試環境

UAT:整合測試環境

DEV:開發環境

PRO:生產環境

在程式中如果想指定使用哪個環境,可以配置變數 env 的值為對應環境名稱即可。

3、cluster

1、一個應用下不同例項的分組,比如典型的可以按照資料中心分,把上海機房的應用例項分為一個叢集,把北京機房的應用例項分為另一個叢集。

2、對不同的叢集,同一個配置可以有不一樣的值,比如說上面所指的兩個北京、上海兩個機房設定兩個叢集,都有 mysql 配置引數,其中引數中配置的地址是不一樣的。

4、namespace

一個應用中不同配置的分組,可以簡單地把 namespace 類比為不同的配置檔案,不同型別的配置存放在不同的檔案中,如資料庫配置檔案,RPC 配置檔案等。

熟悉 SpringBoot 的都知道,SpringBoot 專案都有一個預設配置檔案 application.yml,如果還想用多個配置,可以建立多個配置檔案來存放不同的配置資訊,通過

指定 spring.profiles.active 引數指定應用不同的配置檔案。這裡的 namespace 概念與其類似,將不同的配置放到不同的配置 namespace 中。

Namespace 分為兩種許可權,分別為:

public(公共的):public許可權的 Namespace,能被任何應用獲取。
private(私有的):只能被所屬的應用獲取到。一個應用嘗試獲取其它應用 private 的 Namespace,Apollo 會報 “404” 異常。

Apollo實時釋出配置

1. 配置釋出後的實時推送設計

配置中心最重要的一個特性就是實時推送,正因為有這個特性,我們才可以依賴配置中心做很多事情。如圖所示。

圖 1 簡要描述了配置釋出的大致過程。

  • 使用者在 Portal 中進行配置的編輯和釋出。

  • Portal 會呼叫 Admin Service 提供的介面進行釋出操作。

  • Admin Service 收到請求後,傳送 ReleaseMessage 給各個 Config Service,通知 Config Service 配置發生變化。

  • Config Service 收到 ReleaseMessage 後,通知對應的客戶端,基於 Http 長連線實現。

2. 傳送 ReleaseMessage 的實現方式

ReleaseMessage 訊息是通過 Mysql 實現了一個簡單的訊息佇列。之所以沒有采用訊息中介軟體,是為了讓 Apollo 在部署的時候儘量簡單,儘可能減少外部依賴,如圖所示。

上圖簡要描述了傳送 ReleaseMessage 的大致過程:

  • Admin Service 在配置釋出後會往 ReleaseMessage 表插入一條訊息記錄。

  • Config Service 會啟動一個執行緒定時掃描 ReleaseMessage 表,來檢視是否有新的訊息記錄。

  • Config Service 發現有新的訊息記錄,就會通知到所有的訊息監聽器。

  • 訊息監聽器得到配置釋出的資訊後,就會通知對應的客戶端。

3. Config Service 通知客戶端的實現方式

通知採用基於 Http 長連線實現,主要分為下面幾個步驟:

  • 客戶端會發起一個 Http 請求到 Config Service 的 notifications/v2 介面。

  • notifications/v2 介面通過 Spring DeferredResult 把請求掛起,不會立即返回。

  • 如果在 60s 內沒有該客戶端關心的配置釋出,那麼會返回 Http 狀態碼 304 給客戶端。

  • 如果發現配置有修改,則會呼叫 DeferredResult 的 setResult 方法,傳入有配置變化的 namespace 資訊,同時該請求會立即返回。

  • 客戶端從返回的結果中獲取到配置變化的 namespace 後,會立即請求 Config Service 獲取該 namespace 的最新配置。

4. 原始碼解析實時推送設計

Apollo 推送涉及的程式碼比較多,本教程就不做詳細分析了,筆者把推送這裡的程式碼稍微簡化了下,給大家進行講解,這樣理解起來會更容易。

當然,這些程式碼比較簡單,很多細節就不做考慮了,只是為了能夠讓大家明白 Apollo 推送的核心原理。

傳送 ReleaseMessage 的邏輯我們就寫一個簡單的介面,用佇列儲存,測試的時候就呼叫這個介面模擬配置有更新,傳送 ReleaseMessage 訊息。具體程式碼如下所示。

@RestControllerpublic class NotificationControllerV2 implements ReleaseMessageListener {// 模擬配置更新, 向其中插入資料表示有更新public static Queue<String> queue = new LinkedBlockingDeque<>();  @GetMapping("/addMsg")  public String addMsg() {    queue.add("xxx");    return "success";  }}

訊息傳送之後,根據前面講過的 Config Service 會啟動一個執行緒定時掃描 ReleaseMessage 表,檢視是否有新的訊息記錄,然後取通知客戶端,在這裡我們也會啟動一個執行緒去掃描,具體程式碼如下所示。

@Componentpublic class ReleaseMessageScanner implements InitializingBean {  @Autowired  private NotificationControllerV2 configController;  @Override  public void afterPropertiesSet() throws Exception {    // 定時任務從資料庫掃描有沒有新的配置釋出    new Thread(() -> {      for (;;) {        String result = NotificationControllerV2.queue.poll();        if (result != null) {        ReleaseMessage message = new ReleaseMessage();        message.setMessage(result);        configController.handleMessage(message);      }    }  }).start();  ;  }}

迴圈讀取 NotificationControllerV2 中的佇列,如果有訊息的話就構造一個 Release-Message 的物件,然後呼叫 NotificationControllerV2 中的 handleMessage() 方法進行訊息的處理。

ReleaseMessage 就一個欄位,模擬訊息內容,具體程式碼如下所示。

public class ReleaseMessage {  private String message;
  public void setMessage(String message) {    this.message = message;  }
  public String getMessage() {    return message;  }}

接下來,我們來看 handleMessage 做了哪些工作。

NotificationControllerV2 實現了 ReleaseMessageListener 介面,ReleaseMessageListener 中定義了 handleMessage() 方法,具體程式碼如下所示。

public interface ReleaseMessageListener {    void handleMessage(ReleaseMessage message);}

handleMessage 就是當配置發生變化的時候,傳送通知的訊息監聽器。訊息監聽器在得到配置釋出的資訊後,會通知對應的客戶端,具體程式碼如下所示。

@RestControllerpublic class NotificationControllerV2 implements ReleaseMessageListener {
  private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps.synchronizedSetMultimap(HashMultimap.create());

  @Override  public void handleMessage(ReleaseMessage message) {    System.err.println("handleMessage:" + message);    List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get("xxxx"));    for (DeferredResultWrapper deferredResultWrapper : results) {      List<ApolloConfigNotification> list = new ArrayList<>();      list.add(new ApolloConfigNotification("application", 1));      deferredResultWrapper.setResult(list);    }  }}

Apollo 的實時推送是基於 Spring DeferredResult 實現的,在 handleMessage() 方法中可以看到是通過 deferredResults 獲取 DeferredResult,deferredResults 就是第一行的 Multimap,Key 其實就是訊息內容,Value 就是 DeferredResult 的業務包裝類 DeferredResultWrapper,我們來看下 DeferredResultWrapper 的程式碼,程式碼如下所示。

public class DeferredResultWrapper {  private static final long TIMEOUT = 60 * 1000;// 60 seconds
  private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST = new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
  private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;
  public DeferredResultWrapper() {    result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);}
  public void onTimeout(Runnable timeoutCallback) {    result.onTimeout(timeoutCallback);  }

  public void onCompletion(Runnable completionCallback) {    result.onCompletion(completionCallback);  }
  public void setResult(ApolloConfigNotification notification) {    setResult(Lists.newArrayList(notification));}
  public void setResult(List<ApolloConfigNotification> notifications) {    result.setResult(new ResponseEntity<>(notifications, HttpStatus.OK));  }
  public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getResult() {    return result;  }}

通過 setResult() 方法設定返回結果給客戶端,以上就是當配置發生變化,然後通過訊息監聽器通知客戶端的原理,那麼客戶端是在什麼時候接入的呢?具體程式碼如下。

@RestControllerpublicclassNotificationControllerV2implementsReleaseMessageListener{
// 模擬配置更新, 向其中插入資料表示有更新  public static Queue<String> queue = new LinkedBlockingDeque<>();  private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps.synchronizedSetMultimap(HashMultimap.create());

  @GetMapping("/getConfig")  public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getConfig() {    DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper();    List<ApolloConfigNotification> newNotifications = getApolloConfigNotifications();    if (!CollectionUtils.isEmpty(newNotifications)) {      deferredResultWrapper.setResult(newNotifications);    } else {      deferredResultWrapper.onTimeout(() -> {    System.err.println("onTimeout");});
  deferredResultWrapper.onCompletion(() -> {    System.err.println("onCompletion");  });  deferredResults.put("xxxx", deferredResultWrapper);  }  return deferredResultWrapper.getResult();  }
  private List<ApolloConfigNotification> getApolloConfigNotifications() {    List<ApolloConfigNotification> list = new ArrayList<>();    String result = queue.poll();    if (result != null) {    list.add(new ApolloConfigNotification("application", 1));  }    return list;  }}

NotificationControllerV2 中提供了一個 /getConfig 的介面,客戶端在啟動的時候會呼叫這個介面,這個時候會執行 getApolloConfigNotifications() 方法去獲取有沒有配置的變更資訊,如果有的話證明配置修改過,直接就通過 deferredResultWrapper.setResult(newNotifications) 返回結果給客戶端,客戶端收到結果後重新拉取配置的資訊覆蓋本地的配置。

如果 getApolloConfigNotifications() 方法沒有返回配置修改的資訊,則證明配置沒有發生修改,那就將 DeferredResultWrapper 物件新增到 deferredResults 中,等待後續配置發生變化時訊息監聽器進行通知。

同時這個請求就會掛起,不會立即返回,掛起是通過 DeferredResultWrapper 中的下面這部分程式碼實現的,具體程式碼如下所示。

privatestaticfinallongTIMEOUT=60*1000;//60seconds
private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST           =newResponseEntity<>(HttpStatus.NOT_MODIFIED);
privateDeferredResult<ResponseEntity<List<ApolloConfigNotification>>>result;
public DeferredResultWrapper() {  result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);}

在建立 DeferredResult 物件的時候指定了超時的時間和超時後返回的響應碼,如果 60s 內沒有訊息監聽器進行通知,那麼這個請求就會超時,超時後客戶端收到的響應碼就是 304。

整個 Config Service 的流程就走完了,接下來我們來看一下客戶端是怎麼實現的,我們簡單地寫一個測試類模擬客戶端註冊,具體程式碼如下所示。

public class ClientTest {    public static void main(String[] args) {        reg();    }    private static void reg() {        System.err.println("註冊");        String result = request("http://localhost:8081/getConfig");        if (result != null) {        // 配置有更新, 重新拉取配置        // ......        }        // 重新註冊        reg();    }    private static String request(String url) {        HttpURLConnection connection = null;        BufferedReader reader = null;        try {            URL getUrl = new URL(url);            connection = (HttpURLConnection) getUrl.openConnection();            connection.setReadTimeout(90000);            connection.setConnectTimeout(3000);            connection.setRequestMethod("GET");            connection.setRequestProperty("Accept-Charset", "utf-8");            connection.setRequestProperty("Content-Type", "application/json");            connection.setRequestProperty("Charset", "UTF-8");            System.out.println(connection.getResponseCode());            if (200 == connection.getResponseCode()) {                reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));                StringBuilder result = new StringBuilder();                String line = null;                while ((line = reader.readLine()) != null) {                    result.append(line);                }                System.out.println("結果 " + result);                return result.toString();            }        } catch (IOException e) {            e.printStackTrace();        } finally {            if (connection != null) {                connection.disconnect();            }        }        return null;    }}

首先啟動 /getConfig 介面所在的服務,然後啟動客戶端,然後客戶端就會發起註冊請求,如果有修改直接獲取到結果,則進行配置的更新操作。如果無修改,請求會掛起,這裡客戶端設定的讀取超時時間是 90s,大於服務端的 60s 超時時間。
每次收到結果後,無論是有修改還是無修改,都必須重新進行註冊,通過這樣的方式就可以達到配置實時推送的效果。
我們可以呼叫之前寫的 /addMsg 介面來模擬配置發生變化,呼叫之後客戶端就能馬上得到返回結果。

Apollo客戶端設計

上圖簡要描述了Apollo客戶端的實現原理:

  1. 客戶端和服務端保持了一個長連線,從而能第一時間獲得配置更新的推送。(通過Http Long Polling實現)

  2. 客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。

    • 這是一個fallback機制,為了防止推送機制失效導致配置不更新

    • 客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified

    • 定時頻率預設為每5分鐘拉取一次,客戶端也可以通過在執行時指定System Property: apollo.refreshInterval來覆蓋,單位為分鐘。

  3. 客戶端從Apollo配置中心服務端獲取到應用的最新配置後,會儲存在記憶體中

  4. 客戶端會把從服務端獲取到的配置在本地檔案系統快取一份(在遇到服務不可用,或網路不通的時候,依然能從本地恢復配置)

  5. 應用程式可以從Apollo客戶端獲取最新的配置、訂閱配置更新通知

Apollo客戶端用法

Apollo支援API方式和Spring整合方式,該怎麼選擇用哪一種方式?

  • API方式靈活,功能完備,配置值實時更新(熱釋出),支援所有Java環境。

  • Spring方式接入簡單,結合Spring有N種酷炫的玩法,如

    • 程式碼中直接使用,如:@Value("${someKeyFromApollo:someDefaultValue}")

    • 配置檔案中使用替換placeholder,如:spring.datasource.url: ${someKeyFromApollo:someDefaultValue}

    • 直接託管spring的配置,如在apollo中直接配置spring.datasource.url=jdbc:mysql://localhost:3306/somedb?characterEncoding=utf8

    • Placeholder方式:

    • Spring boot的@ConfigurationProperties方式

    • 從v0.10.0開始的版本支援placeholder在執行時自動更新,具體參見PR #972。(v0.10.0之前的版本在配置變化後不會重新注入,需要重啟才會更新,如果需要配置值實時更新,可以參考後續3.2.2 Spring Placeholder的使用的說明)

  • Spring方式也可以結合API方式使用,如注入Apollo的Config物件,就可以照常通過API方式獲取配置了:

    @ApolloConfig
    private Config config; //inject config for namespace application
  • 更多有意思的實際使用場景和示例程式碼,請參考apollo-use-cases

1、API使用方式

API方式是最簡單、高效使用Apollo配置的方式,不依賴Spring框架即可使用。

獲取預設namespace的配置(application)

Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never nullString someKey = "someKeyFromDefaultNamespace";String someDefaultValue = "someDefaultValueForTheKey";String value = config.getProperty(someKey, someDefaultValue);

通過上述的config.getProperty可以獲取到someKey對應的實時最新的配置值。

另外,配置值從記憶體中獲取,所以不需要應用自己做快取。

監聽配置變化事件

監聽配置變化事件只在應用真的關心配置變化,需要在配置變化時得到通知時使用,比如:資料庫連線串變化後需要重建連線等。

如果只是希望每次都取到最新的配置的話,只需要按照上面的例子,呼叫config.getProperty即可。

Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never nullconfig.addChangeListener(new ConfigChangeListener() {    @Override    public void onChange(ConfigChangeEvent changeEvent) {        System.out.println("Changes for namespace " + changeEvent.getNamespace());        for (String key : changeEvent.changedKeys()) {            ConfigChange change = changeEvent.getChange(key);            System.out.println(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));        }    }});

獲取公共Namespace的配置

String somePublicNamespace = "CAT";Config config = ConfigService.getConfig(somePublicNamespace); //config instance is singleton for each namespace and is never nullString someKey = "someKeyFromPublicNamespace";String someDefaultValue = "someDefaultValueForTheKey";String value = config.getProperty(someKey, someDefaultValue);

獲取非properties格式namespace的配置

1.yaml/yml格式的namespace

apollo-client 1.3.0版本開始對yaml/yml做了更好的支援,使用起來和properties格式一致。

Config config = ConfigService.getConfig("application.yml");String someKey = "someKeyFromYmlNamespace";String someDefaultValue = "someDefaultValueForTheKey";String value = config.getProperty(someKey, someDefaultValue);

2.非yaml/yml格式的namespace

獲取時需要使用ConfigService.getConfigFile介面並指定Format,如ConfigFileFormat.XML

String someNamespace = "test";ConfigFile configFile = ConfigService.getConfigFile("test", ConfigFileFormat.XML);String content = configFile.getContent();

Spring整合方式

配置

Apollo也支援和Spring整合(Spring 3.1.1+),只需要做一些簡單的配置就可以了。

Apollo目前既支援比較傳統的基於XML的配置,也支援目前比較流行的基於Java(推薦)的配置。

如果是Spring Boot環境,建議參照3.2.1.3 Spring Boot整合方式(推薦)配置。

需要注意的是,如果之前有使用org.springframework.beans.factory.config.PropertyPlaceholderConfigurer的,請替換成org.springframework.context.support.PropertySourcesPlaceholderConfigurer。Spring 3.1以後就不建議使用PropertyPlaceholderConfigurer了,要改用PropertySourcesPlaceholderConfigurer。

如果之前有使用<context:property-placeholder>,請注意xml中引入的spring-context.xsd版本需要是3.1以上(一般只要沒有指定版本會自動升級的),建議使用不帶版本號的形式引入,如:http://www.springframework.org/schema/context/spring-context.xsd

注1:yaml/yml格式的namespace從1.3.0版本開始支援和Spring整合,注入時需要填寫帶字尾的完整名字,比如application.yml

注2:非properties、非yaml/yml格式(如xml,json等)的namespace暫不支援和Spring整合。

基於XML的配置

注:需要把apollo相關的xml namespace加到配置檔案頭上,不然會報xml語法錯誤。

1.注入預設namespace的配置到Spring中

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:apollo="http://www.ctrip.com/schema/apollo"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd       http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">    <!-- 這個是最簡單的配置形式,一般應用用這種形式就可以了,用來指示Apollo注入application namespace的配置到Spring環境中 -->    <apollo:config/>    <bean class="com.ctrip.framework.apollo.spring.TestXmlBean">        <property name="timeout" value="${timeout:100}"/>        <property name="batch" value="${batch:200}"/>    </bean></beans>

2.注入多個namespace的配置到Spring中

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:apollo="http://www.ctrip.com/schema/apollo"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd       http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">    <!-- 這個是最簡單的配置形式,一般應用用這種形式就可以了,用來指示Apollo注入application namespace的配置到Spring環境中 -->    <apollo:config/>    <!-- 這個是稍微複雜一些的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環境中 -->    <apollo:config namespaces="FX.apollo,application.yml"/>    <bean class="com.ctrip.framework.apollo.spring.TestXmlBean">        <property name="timeout" value="${timeout:100}"/>        <property name="batch" value="${batch:200}"/>    </bean></beans>

3.注入多個namespace,並且指定順序

Spring的配置是有順序的,如果多個property source都有同一個key,那麼最終是順序在前的配置生效。

apollo:config如果不指定order,那麼預設是最低優先順序。

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:apollo="http://www.ctrip.com/schema/apollo"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd       http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">    <apollo:config order="2"/>    <!-- 這個是最複雜的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環境中,並且順序在application前面 -->    <apollo:config namespaces="FX.apollo,application.yml" order="1"/>    <bean class="com.ctrip.framework.apollo.spring.TestXmlBean">        <property name="timeout" value="${timeout:100}"/>        <property name="batch" value="${batch:200}"/>    </bean></beans>

基於Java的配置(推薦)

相對於基於XML的配置,基於Java的配置是目前比較流行的方式。

注意@EnableApolloConfig要和@Configuration一起使用,不然不會生效。

1.注入預設namespace的配置到Spring中

//這個是最簡單的配置形式,一般應用用這種形式就可以了,用來指示Apollo注入application namespace的配置到Spring環境中@Configuration@EnableApolloConfigpublic class AppConfig {  @Bean  public TestJavaConfigBean javaConfigBean() {    return new TestJavaConfigBean();  }}

2.注入多個namespace的配置到Spring中

@Configuration@EnableApolloConfigpublic class SomeAppConfig {http://www.jintianxuesha.com/  @Bean  public TestJavaConfigBean javaConfigBean() {    return new TestJavaConfigBean();  }}
//這個是稍微複雜一些的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環境中@Configuration@EnableApolloConfig({"FX.apollo", "application.yml"})public class AnotherAppConfig {}

3.注入多個namespace,並且指定順序

//這個是最複雜的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環境中,並且順序在application前面@Configuration@EnableApolloConfig(order = 2)public class SomeAppConfig {  @Bean  public TestJavaConfigBean javaConfigBean() {    return new TestJavaConfigBean();  }}@Configuration@EnableApolloConfig(value = {"FX.apollo", "application.yml"}, order = 1)public class AnotherAppConfig {}

Spring Boot整合方式(推薦)

Spring Boot除了支援上述兩種整合方式以外,還支援通過application.properties/bootstrap.properties來配置,該方式能使配置在更早的階段注入,比如使用@ConditionalOnProperty的場景或者是有一些spring-boot-starter在啟動階段就需要讀取配置做一些事情(如dubbo-spring-boot-project),所以對於Spring Boot環境建議通過以下方式來接入Apollo(需要0.10.0及以上版本)。

使用方式很簡單,只需要在application.properties/bootstrap.properties中按照如下樣例配置即可。

注入預設applicationnamespace的配置示例

#willinject'application'namespaceinbootstrapphaseapollo.bootstrap.enabled=true

注入非預設applicationnamespace或多個namespace的配置示例

apollo.bootstrap.enabled=true#willinject'application','FX.apollo'and'application.yml'namespacesinbootstrapphaseapollo.bootstrap.namespaces=application,FX.apollo,application.yml

將Apollo配置載入提到初始化日誌系統之前(1.2.0+)

從1.2.0版本開始,如果希望把日誌相關的配置(如logging.level.root=infologback-spring.xml中的引數)也放在Apollo管理,那麼可以額外配置apollo.bootstrap.eagerLoad.enabled=true來使Apollo的載入順序放到日誌系統載入之前,不過這會導致Apollo的啟動過程無法通過日誌的方式輸出(因為執行Apollo載入的時候,日誌系統壓根沒有準備好呢!所以在Apollo程式碼中使用Slf4j的日誌輸出便沒有任何內容),更多資訊可以參考PR 1614。參考配置示例如下:

#willinject'application'namespaceinbootstrapphaseapollo.bootstrap.enabled=true#putapolloinitializationbeforeloggingsysteminitializationapollo.bootstrap.eagerLoad.enabled=true

Spring Placeholder的使用

Spring應用通常會使用Placeholder來注入配置,使用的格式形如{someKey:someDefaultValue},如someKey:someDefaultValue如{timeout:100}。冒號前面的是key,冒號後面的是預設值。

建議在實際使用時儘量給出預設值,以免由於key沒有定義導致執行時錯誤。

從v0.10.0開始的版本支援placeholder在執行時自動更新,具體參見PR #972。

如果需要關閉placeholder在執行時自動更新功能,可以通過以下兩種方式關閉:

1. 通過設定System Propertyapollo.autoUpdateInjectedSpringProperties,如啟動時傳入-Dapollo.autoUpdateInjectedSpringProperties=false

2.通過設定META-INF/app.properties中的apollo.autoUpdateInjectedSpringProperties屬性,如