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

原文首發地址:不問沒關係,一問要人命,絕對的面試加分項--配置中心Apollo深度解讀java

爲何要使用配置中心

隨着程序功能的日益複雜,程序的配置日益增多:各類功能的開關、參數的配置、服務器的地址……mysql

對程序配置的指望值也愈來愈高:配置修改後實時生效,灰度發佈,分環境、分集羣管理配置,完善的權限、審覈機制……面試

在這樣的大環境下,傳統的經過配置文件、數據庫等方式已經愈來愈沒法知足開發人員對配置管理的需求。redis

想必你們都深有體會,咱們作的項目都伴隨着各類配置文件,並且老是本地配置一套配置文件,測試服配置一套,正式服配置一套,有時候一不當心就改錯了,捱罵是小事,扣績效那可就鬧大了。spring

並且每當項目發佈的時候,配置文件也會被打包進去,也就是配置文件會跟着項目一塊兒發佈。而後每次出現問題須要咱們修改配置文件的時候,咱們老是得先在本地修改,而後從新發布才能讓新的配置生效。sql

當請求壓力愈來愈大,你的項目也會從 1 個節點變成多個節點,這個時候若是配置須要發生變化,對應的修改操做也是相同的,只須要在項目中修改一次便可,但對於發佈操做工做量就比以前大了不少,由於要發佈多個節點。

修改這些配置,增長的發佈的工做量下降了總體的工做效率,爲了可以提高工做效率,配置中心應運而生了,咱們能夠將配置統一存放在配置中心來進行管理。
數據庫

整體而言在沒有引入配置中心以前,咱們都會面臨如下問題:json

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

  2. 主要採用本地靜態配置,配置修改麻煩,配置修改通常須要通過一個較長的測試發佈週期。在分佈式微服務環境下,當服務實例不少時,修改配置費時費力。緩存

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

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

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

到底什麼是配置中心

配置中心就是把項目中各類個樣的配置、參數、開關,所有都放到一個集中的地方進行統一管理,並提供一套標準的接口。當各個服務須要獲取配置的時候,就來配置中心的接口拉取。當配置中心中的各類參數有更新的時候,也能通知到各個服務實時的過來同步最新的信息,使之動態更新。

Apollo 簡介

Apollo(阿波羅)是攜程框架部門研發的開源配置管理中心,可以集中化管理應用不一樣環境、不一樣集羣的配置,配置修改後可以實時推送到應用端,而且具有規範的權限、流程治理等特性。

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

一、application (應用)
二、environment (環境)

三、cluster (集羣)
四、namespace (命名空間)

什麼是配置

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

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

  • 配置是獨立於程序的只讀變量

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

    • 其次,配置對於程序是隻讀的,程序經過讀取配置來改變本身的行爲,可是程序不該該去改變配置。

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

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

    • 配置貫穿於應用的整個生命週期,應用在啓動時經過讀取配置來初始化,在運行時根據配置調整行爲。

  • 配置能夠有多種加載方式

    • 配置也有不少種加載方式,常見的有程序內部hard code,配置文件,環境變量,啓動參數,基於數據庫等

  • 配置須要治理

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

    • 雖然這類框架類組件是由其餘團隊開發、維護,可是運行時是在業務實際應用內的,因此本質上能夠認爲框架類組件也是應用的一部分。

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

    • 同一份程序在不一樣的環境(開發,測試,生產)、不一樣的集羣(如不一樣的數據中心)常常須要有不一樣的配置,因此須要有完善的環境、集羣配置管理

    • 因爲配置能改變程序的行爲,不正確的配置甚至能引發災難,因此對配置的修改必須有比較完善的權限控制

    • 權限控制

    • 不一樣環境、集羣配置管理

    • 框架類組件配置管理

爲何使用ApolloApollo有哪些特徵

正是基於配置的特殊性,因此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核心概念介紹

一、application

一、Apollo 客戶端在運行時須要知道當前應用是誰,從而能夠根據不一樣的應用來獲取對應應用的配置。

二、每一個應用都須要有惟一的身份標識,能夠在代碼中配置 app.id 參數來標識當前應用,Apollo 會根據此指來辨別當前應用。

二、environment

在實際開發中,咱們的應用常常要部署在不一樣的環境中,通常狀況下分爲 開發、測試、生產 等等不一樣環境,不一樣環境中的配置也是不一樣的,在 Apollo 中默認提供了

四種環境:

FAT:功能測試環境

UAT:集成測試環境

DEV:開發環境

PRO:生產環境

在程序中若是想指定使用哪一個環境,能夠配置變量 env 的值爲對應環境名稱便可。

三、cluster

一、一個應用下不一樣實例的分組,好比典型的能夠按照數據中心分,把上海機房的應用實例分爲一個集羣,把北京機房的應用實例分爲另外一個集羣。

二、對不一樣的集羣,同一個配置能夠有不同的值,好比說上面所指的兩個北京、上海兩個機房設置兩個集羣,都有 mysql 配置參數,其中參數中配置的地址是不同的。

四、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() 方法設置返回結果給客戶端,以上就是當配置發生變化,而後經過消息監聽器通知客戶端的原理,那麼客戶端是在何時接入的呢?具體代碼以下。





@RestControllerpublic class NotificationControllerV2 implements ReleaseMessageListener {// 模擬配置更新, 向其中插入數據表示有更新 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 中的下面這部分代碼實現的,具體代碼以下所示。



private static final long TIMEOUT = 60 * 1000; // 60 secondsprivate 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);}

在建立 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

一、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 { @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中按照以下樣例配置便可。

注入默認application namespace的配置示例

  #will inject 'application' namespace in bootstrap phase  apollo.bootstrap.enabled = true

注入非默認application namespace或多個namespace的配置示例

  apollo.bootstrap.enabled = true  # will inject 'application', 'FX.apollo' and 'application.yml' namespaces in bootstrap phase  apollo.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。參考配置示例以下:

  # will inject 'application' namespace in bootstrap phase  apollo.bootstrap.enabled = true  # put apollo initialization before logging system initialization  apollo.bootstrap.eagerLoad.enabled=true

Spring Placeholder的使用

Spring應用一般會使用Placeholder來注入配置,使用的格式形如${someKey:someDefaultValue},如${timeout:100}。冒號前面的是key,冒號後面的是默認值。

建議在實際使用時儘可能給出默認值,以避免因爲key沒有定義致使運行時錯誤。

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

若是須要關閉placeholder在運行時自動更新功能,能夠經過如下兩種方式關閉:

1. 經過設置System Property apollo.autoUpdateInjectedSpringProperties如啓動時傳入-Dapollo.autoUpdateInjectedSpringProperties=false

2.經過設置META-INF/app.properties中的apollo.autoUpdateInjectedSpringProperties屬性,如​​​​​​​

app.id=SampleAppapollo.autoUpdateInjectedSpringProperties=false

 XML使用方式

假設我有一個TestXmlBean,它有兩個配置項須要注入:​​​​​​​




public class TestXmlBean { private int timeout; private int batch; public void setTimeout(int timeout) { this.timeout = timeout; } public void setBatch(int batch) { this.batch = batch; } public int getTimeout() { return timeout; } public int getBatch() { return batch; }}

那麼,我在XML中會使用以下方式來定義(假設應用默認的application namespace中有timeout和batch的配置項):

<?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/> <bean class="com.ctrip.framework.apollo.spring.TestXmlBean"> <property name="timeout" value="${timeout:100}"/> <property name="batch" value="${batch:200}"/> </bean></beans>

 Java Config使用方式

假設我有一個TestJavaConfigBean,經過Java Config的方式還可使用@Value的方式注入:​​​​​​​



public class TestJavaConfigBean { @Value("${timeout:100}") private int timeout; private int batch; @Value("${batch:200}") public void setBatch(int batch) { this.batch = batch; } public int getTimeout() { return timeout; } public int getBatch() { return batch; }}

在Configuration類中按照下面的方式使用(假設應用默認的application namespace中有timeoutbatch的配置項):​​​​​​​

@Configuration@EnableApolloConfigpublic class AppConfig { @Bean public TestJavaConfigBean javaConfigBean() { return new TestJavaConfigBean(); }}

ConfigurationProperties使用方式

Spring Boot提供了@ConfigurationProperties把配置注入到bean對象中。

Apollo也支持這種方式,下面的例子會把redis.cache.expireSecondsredis.cache.commandTimeout分別注入到SampleRedisConfig的expireSecondscommandTimeout字段中。​​​​​​​


@ConfigurationProperties(prefix = "redis.cache")public class SampleRedisConfig { private int expireSeconds; private int commandTimeout; public void setExpireSeconds(int expireSeconds) { this.expireSeconds = expireSeconds; } public void setCommandTimeout(int commandTimeout) { this.commandTimeout = commandTimeout; }}

在Configuration類中按照下面的方式使用(假設應用默認的application namespace中有redis.cache.expireSecondsredis.cache.commandTimeout的配置項):​​​​​​​

@Configuration@EnableApolloConfigpublic class AppConfig { @Bean public SampleRedisConfig sampleRedisConfig() { return new SampleRedisConfig(); }}

須要注意的是,@ConfigurationProperties若是須要在Apollo配置變化時自動更新注入的值,須要配合使用EnvironmentChangeEvent或RefreshScope。相關代碼實現,能夠參考apollo-use-cases項目中的ZuulPropertiesRefresher.java和apollo-demo項目中的SampleRedisConfig.java以及SpringBootApolloRefreshConfig.java

Spring Annotation支持

Apollo同時還增長了幾個新的Annotation來簡化在Spring環境中的使用。

  1. @ApolloConfig

    • 用來自動注入Config對象

  2. @ApolloConfigChangeListener

    • 用來自動註冊ConfigChangeListener

  3. @ApolloJsonValue

    • 用來把配置的json字符串自動注入爲對象

使用樣例以下:








public class TestApolloAnnotationBean { @ApolloConfig private Config config; //inject config for namespace application @ApolloConfig("application") private Config anotherConfig; //inject config for namespace application @ApolloConfig("FX.apollo") private Config yetAnotherConfig; //inject config for namespace FX.apollo @ApolloConfig("application.yml") private Config ymlConfig; //inject config for namespace application.yml /** * ApolloJsonValue annotated on fields example, the default value is specified as empty list - [] * <br /> * jsonBeanProperty=[{"someString":"hello","someInt":100},{"someString":"world!","someInt":200}] */ @ApolloJsonValue("${jsonBeanProperty:[]}") private List<JsonBean> anotherJsonBeans; @Value("${batch:100}") private int batch; //config change listener for namespace application @ApolloConfigChangeListener private void someOnChange(ConfigChangeEvent changeEvent) { //update injected value of batch if it is changed in Apollo if (changeEvent.isChanged("batch")) { batch = config.getIntProperty("batch", 100); } } //config change listener for namespace application @ApolloConfigChangeListener("application") private void anotherOnChange(ConfigChangeEvent changeEvent) { //do something } //config change listener for namespaces application, FX.apollo and application.yml @ApolloConfigChangeListener({"application", "FX.apollo", "application.yml"}) private void yetAnotherOnChange(ConfigChangeEvent changeEvent) { //do something } //example of getting config from Apollo directly //this will always return the latest value of timeout public int getTimeout() { return config.getIntProperty("timeout", 200); } //example of getting config from injected value //the program needs to update the injected value when batch is changed in Apollo using @ApolloConfigChangeListener shown above public int getBatch() { return this.batch; } private static class JsonBean{ private String someString; private int someInt; }}

在Configuration類中按照下面的方式使用:

​​​​​​​

@Configuration@EnableApolloConfigpublic class AppConfig { @Bean public TestApolloAnnotationBean testApolloAnnotationBean() { return new TestApolloAnnotationBean(); }}

 已有配置遷移

不少狀況下,應用可能已經有很多配置了,好比Spring Boot的應用,就會有bootstrap.properties/yml, application.properties/yml等配置。

在應用接入Apollo以後,這些配置是能夠很是方便的遷移到Apollo的,具體步驟以下:

  1. 在Apollo爲應用新建項目

  2. 在應用中配置好META-INF/app.properties

  3. 建議把原先配置先轉爲properties格式,而後經過Apollo提供的文本編輯模式所有粘帖到應用的application namespace,發佈配置

    • 若是原來格式是yml,可使用YamlPropertiesFactoryBean.getObject轉成properties格式

  4. 若是原來是yml,想繼續使用yml來編輯配置,那麼能夠建立私有的application.yml namespace,把原來的配置所有粘貼進去,發佈配置

    • 須要apollo-client是1.3.0及以上版本

  5. 把原先的配置文件如bootstrap.properties/yml, application.properties/yml從項目中刪除

    • 若是須要保留本地配置文件,須要注意部分配置如server.port必須確保本地文件已經刪除該配置項

如:

​​​​​​​



spring.application.name = reservation-serviceserver.port = 8080logging.level = ERROReureka.client.serviceUrl.defaultZone = http://127.0.0.1:8761/eureka/eureka.client.healthcheck.enabled = trueeureka.client.registerWithEureka = trueeureka.client.fetchRegistry = trueeureka.client.eurekaServiceUrlPollIntervalSeconds = 60eureka.instance.preferIpAddress = true

Apollo的高可用性設計

​​​​​​​
高可用是分佈式系統架構設計中必須考慮的因素之一,它一般是指經過設計減小系統不能提供服務的時間。

Apollo 在高可用設計上下了很大的功夫,下面咱們來簡單的分析下:

1)某臺Config Service 下線

無影響,Config Service 可用部署多個節點。

2)全部 Config Service 下線

全部 Config Service 下線會影響客戶端的使用,沒法讀取最新的配置。可採用讀取本地緩存的配置文件來過渡。

3)某臺 Admin Service 下線

無影響,Admin Service 可用部署多個節點。

4)全部 Admin Service 下線

Admin Service 是服務於 Portal,全部 Admin Service 下線以後只會影響 Portal 的操做,不會影響客戶端,客戶端是依賴 Config Service。

5)某臺 Portal 下線

Portal 可用部署多臺,經過 Nginx 作負載,某臺下線以後不影響使用。

6)所有 Portal 下線

對客戶端讀取配置是沒有影響的,只是不能經過 Portal 去查看,修改配置。

7)數據庫宕機

當配置的數據庫宕機以後,對客戶端是沒有影響的,可是會致使 Portal 中沒法更新配置。當客戶端重啓,這個時候若是須要從新拉取配置,就會有影響,可採起開啓配置緩存的選項來避免數據庫宕機帶來的影響。

總結

本文根據現實開發中的種種複雜狀況,引入配置中心,如今市面上的配置中心不少,本文着重介紹的是攜程開源的Apollo。

本文從對Apollo的簡介到爲何使用Apollo和Apollo的特徵介紹。而後對Apollo的總體架構和Apollo的核心概念進行了詳細介紹。而且重點介紹了Apollo的客戶端設計和用法。

最後對Apollo的高可用設計進行了詳細說明。

本文算是比較全面的介紹了Apollo相關的知識,也是我根據高頻面試題常問的幾個方面對Apollo進行解讀。

總之,走過路過不要錯過,本文很長建議先馬再看。

Spring Cloud 微服務精彩系列

  1. 阿里面試官問我:到底知不知道什麼是Eureka,此次,我沒沉默
  2. 萬字詳解Ribbon架構,針對面試高頻題多角度細說Ribbon
  3. 什麼是Hystrix,阿里技術最終面,遺憾的倒在Hystrix面前!
  4. 2萬字好文全方位深刻學習SpringCloud Fegin,面試不在彷徨
  5. Zuul,據說SpringCloud不許備要我了,但是爲何面試還要每天問我?
  6. 全網最全講解Spring Cloud Gateway,認真看完這一篇就夠了!
  7. 不問沒關係,一問要人命,絕對的面試加分項--配置中心Apollo深度解讀

     

相關文章
相關標籤/搜索