溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何理解攜程架構(gòu)部開源的配置中心Apollo

發(fā)布時(shí)間:2021-10-20 17:08:37 來源:億速云 閱讀:186 作者:iii 欄目:編程語言

這篇文章主要講解了“如何理解攜程架構(gòu)部開源的配置中心Apollo”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“如何理解攜程架構(gòu)部開源的配置中心Apollo”吧!

為什么要使用配置中心

隨著程序功能的日益復(fù)雜,程序的配置日益增多:各種功能的開關(guān)、參數(shù)的配置、服務(wù)器的地址……

對程序配置的期望值也越來越高:配置修改后實(shí)時(shí)生效,灰度發(fā)布,分環(huán)境、分集群管理配置,完善的權(quán)限、審核機(jī)制……

在這樣的大環(huán)境下,傳統(tǒng)的通過配置文件、數(shù)據(jù)庫等方式已經(jīng)越來越無法滿足開發(fā)人員對配置管理的需求。

想必大家都深有體會(huì),我們做的項(xiàng)目都伴隨著各種配置文件,而且總是本地配置一套配置文件,測試服配置一套,正式服配置一套,有時(shí)候一不小心就改錯(cuò)了,挨罵是小事,扣績效那可就鬧大了。

而且每當(dāng)項(xiàng)目發(fā)布的時(shí)候,配置文件也會(huì)被打包進(jìn)去,也就是配置文件會(huì)跟著項(xiàng)目一起發(fā)布。然后每次出現(xiàn)問題需要我們修改配置文件的時(shí)候,我們總是得先在本地修改,然后重新發(fā)布才能讓新的配置生效。

當(dāng)請求壓力越來越大,你的項(xiàng)目也會(huì)從 1 個(gè)節(jié)點(diǎn)變成多個(gè)節(jié)點(diǎn),這個(gè)時(shí)候如果配置需要發(fā)生變化,對應(yīng)的修改操作也是相同的,只需要在項(xiàng)目中修改一次即可,但對于發(fā)布操作工作量就比之前大了很多,因?yàn)橐l(fā)布多個(gè)節(jié)點(diǎn)。

修改這些配置,增加的發(fā)布的工作量降低了整體的工作效率,為了能夠提升工作效率,配置中心應(yīng)運(yùn)而生了,我們可以將配置統(tǒng)一存放在配置中心來進(jìn)行管理。

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

  1. 配置散亂格式不標(biāo)準(zhǔn),有的用 properties 格式,有的用 xml 格式,還有的存 DB,團(tuán)隊(duì)傾向自造輪子,做法五花八門。

  2. 主要采用本地靜態(tài)配置,配置修改麻煩,配置修改一般需要經(jīng)過一個(gè)較長的測試發(fā)布周期。在分布式微服務(wù)環(huán)境下,當(dāng)服務(wù)實(shí)例很多時(shí),修改配置費(fèi)時(shí)費(fèi)力。

  3. 易引發(fā)生產(chǎn)事故,這個(gè)是我親身經(jīng)歷,之前在一家互聯(lián)網(wǎng)公司,有團(tuán)隊(duì)在發(fā)布的時(shí)候?qū)y試環(huán)境的配置帶到生產(chǎn)上,引發(fā)百萬級資損事故。

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

  5. 增加了運(yùn)維小哥哥的工作量,極大的損害了運(yùn)維小哥哥和開發(fā)小哥哥的基情。

到底什么是配置中心

配置中心就是把項(xiàng)目中各種個(gè)樣的配置、參數(shù)、開關(guān),全部都放到一個(gè)集中的地方進(jìn)行統(tǒng)一管理,并提供一套標(biāo)準(zhǔn)的接口。當(dāng)各個(gè)服務(wù)需要獲取配置的時(shí)候,就來配置中心的接口拉取。當(dāng)配置中心中的各種參數(shù)有更新的時(shí)候,也能通知到各個(gè)服務(wù)實(shí)時(shí)的過來同步最新的信息,使之動(dòng)態(tài)更新。

Apollo 簡介

Apollo(阿波羅)是攜程框架部門研發(fā)的開源配置管理中心,能夠集中化管理應(yīng)用不同環(huán)境、不同集群的配置,配置修改后能夠?qū)崟r(shí)推送到應(yīng)用端,并且具備規(guī)范的權(quán)限、流程治理等特性。

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

1、application (應(yīng)用)
2、environment (環(huán)境)

3、cluster (集群)
4、namespace (命名空間)

什么是配置

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

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

  • 配置是獨(dú)立于程序的只讀變量

    • 配置首先是獨(dú)立于程序的,同一份程序在不同的配置下會(huì)有不同的行為。

    • 其次,配置對于程序是只讀的,程序通過讀取配置來改變自己的行為,但是程序不應(yīng)該去改變配置。

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

  • 配置伴隨應(yīng)用的整個(gè)生命周期

    • 配置貫穿于應(yīng)用的整個(gè)生命周期,應(yīng)用在啟動(dòng)時(shí)通過讀取配置來初始化,在運(yùn)行時(shí)根據(jù)配置調(diào)整行為。

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

    • 配置也有很多種加載方式,常見的有程序內(nèi)部hard code,配置文件,環(huán)境變量,啟動(dòng)參數(shù),基于數(shù)據(jù)庫等

  • 配置需要治理

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

    • 雖然這類框架類組件是由其他團(tuán)隊(duì)開發(fā)、維護(hù),但是運(yùn)行時(shí)是在業(yè)務(wù)實(shí)際應(yīng)用內(nèi)的,所以本質(zhì)上可以認(rèn)為框架類組件也是應(yīng)用的一部分。

    • 這類組件對應(yīng)的配置也需要有比較完善的管理方式。

    • 同一份程序在不同的環(huán)境(開發(fā),測試,生產(chǎn))、不同的集群(如不同的數(shù)據(jù)中心)經(jīng)常需要有不同的配置,所以需要有完善的環(huán)境、集群配置管理

    • 由于配置能改變程序的行為,不正確的配置甚至能引起災(zāi)難,所以對配置的修改必須有比較完善的權(quán)限控制

    • 權(quán)限控制

    • 不同環(huán)境、集群配置管理

    • 框架類組件配置管理

為什么使用Apollo,Apollo有哪些特征

正是基于配置的特殊性,所以Apollo從設(shè)計(jì)之初就立志于成為一個(gè)有治理能力的配置發(fā)布平臺(tái),目前提供了以下的特性:

  • 統(tǒng)一管理不同環(huán)境、不同集群的配置

    • Apollo提供了一個(gè)統(tǒng)一界面集中式管理不同環(huán)境(environment)、不同集群(cluster)、不同命名空間(namespace)的配置。

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

    • 通過命名空間(namespace)可以很方便地支持多個(gè)不同應(yīng)用共享同一份配置,同時(shí)還允許應(yīng)用對共享的配置進(jìn)行覆蓋

  • 配置修改實(shí)時(shí)生效(熱發(fā)布)

    • 用戶在Apollo修改完配置并發(fā)布后,客戶端能實(shí)時(shí)(1秒)接收到最新的配置,并通知到應(yīng)用程序

  • 版本發(fā)布管理

    • 所有的配置發(fā)布都有版本概念,從而可以方便地支持配置的回滾

  • 灰度發(fā)布

    • 支持配置的灰度發(fā)布,比如點(diǎn)了發(fā)布后,只對部分應(yīng)用實(shí)例生效,等觀察一段時(shí)間沒問題后再推給所有應(yīng)用實(shí)例

  • 權(quán)限管理、發(fā)布審核、操作審計(jì)

    • 應(yīng)用和配置的管理都有完善的權(quán)限管理機(jī)制,對配置的管理還分為了編輯和發(fā)布兩個(gè)環(huán)節(jié),從而減少人為的錯(cuò)誤。

    • 所有的操作都有審計(jì)日志,可以方便地追蹤問題

  • 客戶端配置信息監(jiān)控

    • 可以在界面上方便地看到配置在被哪些實(shí)例使用

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

    • 提供了Java和.Net的原生客戶端,方便應(yīng)用集成

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

    • 同時(shí)提供了Http接口,非Java和.Net應(yīng)用也可以方便地使用

  • 提供開放平臺(tái)API

    • Apollo自身提供了比較完善的統(tǒng)一配置管理界面,支持多環(huán)境、多數(shù)據(jù)中心配置管理、權(quán)限、流程治理等特性。不過Apollo出于通用性考慮,不會(huì)對配置的修改做過多限制,只要符合基本的格式就能保存,不會(huì)針對不同的配置值進(jìn)行針對性的校驗(yàn),如數(shù)據(jù)庫用戶名、密碼,Redis服務(wù)地址等

    • 對于這類應(yīng)用配置,Apollo支持應(yīng)用方通過開放平臺(tái)API在Apollo進(jìn)行配置的修改和發(fā)布,并且具備完善的授權(quán)和權(quán)限控制

  • 部署簡單

    • 配置中心作為基礎(chǔ)服務(wù),可用性要求非常高,這就要求Apollo對外部依賴盡可能地少

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

    • Apollo還提供了打包腳本,一鍵就可以生成所有需要的安裝包,并且支持自定義運(yùn)行時(shí)參數(shù)

Apollo整體架構(gòu)

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

如何理解攜程架構(gòu)部開源的配置中心Apollo

1.用戶在配置中心對配置進(jìn)行修改并發(fā)布
2.配置中心通知Apollo客戶端有配置更新
3.Apollo客戶端從配置中心拉取最新的配置、更新本地配置并通知到應(yīng)用

接下來我們來看看Apollo的整體架構(gòu)圖

如何理解攜程架構(gòu)部開源的配置中心Apollo

上圖簡要描述了Apollo的總體設(shè)計(jì),我們可以從下往上看:

  • Config Service提供配置的讀取、推送等功能,服務(wù)對象是Apollo客戶端

  • Admin Service提供配置的修改、發(fā)布等功能,服務(wù)對象是Apollo Portal(管理界面)

  • Config Service和Admin Service都是多實(shí)例、無狀態(tài)部署,所以需要將自己注冊到Eureka中并保持心跳

  • 在Eureka之上我們架了一層Meta Server用于封裝Eureka的服務(wù)發(fā)現(xiàn)接口

  • Client通過域名訪問Meta Server獲取Config Service服務(wù)列表(IP+Port),而后直接通過IP+Port訪問服務(wù),同時(shí)在Client側(cè)會(huì)做load balance、錯(cuò)誤重試

  • Portal通過域名訪問Meta Server獲取Admin Service服務(wù)列表(IP+Port),而后直接通過IP+Port訪問服務(wù),同時(shí)在Portal側(cè)會(huì)做load balance、錯(cuò)誤重試

  • 為了簡化部署,我們實(shí)際上會(huì)把Config Service、Eureka和Meta Server三個(gè)邏輯角色部署在同一個(gè)JVM進(jìn)程中

Why Eureka

為什么我們采用Eureka作為服務(wù)注冊中心,而不是使用傳統(tǒng)的zk、etcd呢?我大致總結(jié)了一下,有以下幾方面的原因:

  • 它提供了完整的Service Registry和Service Discovery實(shí)現(xiàn)
    首先是提供了完整的實(shí)現(xiàn),并且也經(jīng)受住了Netflix自己的生產(chǎn)環(huán)境考驗(yàn),相對使用起來會(huì)比較省心。
    和Spring Cloud無縫集成

  • 的項(xiàng)目本身就使用了Spring Cloud和Spring Boot,同時(shí)Spring Cloud還有一套非常完善的開源代碼來整合Eureka,所以使用起來非常方便。

  • 另外,Eureka還支持在我們應(yīng)用自身的容器中啟動(dòng),也就是說我們的應(yīng)用啟動(dòng)完之后,既充當(dāng)了Eureka的角色,同時(shí)也是服務(wù)的提供者。這樣就極大的提高了服務(wù)的可用性。

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

  • 最后一點(diǎn)是開源,由于代碼是開源的,所以非常便于我們了解它的實(shí)現(xiàn)原理和排查問題。

各模塊概要介紹

Config Service

  • 提供配置獲取接口

  • 提供配置更新推送接口(基于Http long polling)

  • 服務(wù)端使用Spring DeferredResult實(shí)現(xiàn)異步化,從而大大增加長連接數(shù)量

  • 目前使用的tomcat embed默認(rèn)配置是最多10000個(gè)連接(可以調(diào)整),使用了4C8G的虛擬機(jī)實(shí)測- 可以支撐10000個(gè)連接,所以滿足需求(一個(gè)應(yīng)用實(shí)例只會(huì)發(fā)起一個(gè)長連接)。接口服務(wù)對象為Apollo客戶端

Admin Service

  • 提供配置管理接口

  • 提供配置修改、發(fā)布等接口

  • 接口服務(wù)對象為Portal

Meta Server

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

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

  • Meta Server從Eureka獲取Config Service和Admin Service的服務(wù)信息,相當(dāng)于是一個(gè)Eureka Client
    增設(shè)一個(gè)Meta Server的角色主要是為了封裝服務(wù)發(fā)現(xiàn)的細(xì)節(jié),對Portal和Client而言,永遠(yuǎn)通過一個(gè)

  • Http接口獲取Admin Service和Config Service的服務(wù)信息,而不需要關(guān)心背后實(shí)際的服務(wù)注冊和發(fā)現(xiàn)組件

  • Meta Server只是一個(gè)邏輯角色,在部署時(shí)和Config Service是在一個(gè)JVM進(jìn)程中的,所以IP、端口和Config Service一致

Eureka

  • 基于Eureka和Spring Cloud Netflix提供服務(wù)注冊和發(fā)現(xiàn)

  • Config Service和Admin Service會(huì)向Eureka注冊服務(wù),并保持心跳

  • 為了簡單起見,目前Eureka在部署時(shí)和Config Service是在一個(gè)JVM進(jìn)程中的(通過Spring Cloud Netflix)

Portal

  • 提供Web界面供用戶管理配置

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

  • 在Portal側(cè)做load balance、錯(cuò)誤重試

Client

  • Apollo提供的客戶端程序,為應(yīng)用提供配置獲取、實(shí)時(shí)更新等功能

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

  • 在Client側(cè)做load balance、錯(cuò)誤重試

Apollo核心概念介紹

1、application

1、Apollo 客戶端在運(yùn)行時(shí)需要知道當(dāng)前應(yīng)用是誰,從而可以根據(jù)不同的應(yīng)用來獲取對應(yīng)應(yīng)用的配置。

2、每個(gè)應(yīng)用都需要有唯一的身份標(biāo)識,可以在代碼中配置 app.id 參數(shù)來標(biāo)識當(dāng)前應(yīng)用,Apollo 會(huì)根據(jù)此指來辨別當(dāng)前應(yīng)用。

2、environment

在實(shí)際開發(fā)中,我們的應(yīng)用經(jīng)常要部署在不同的環(huán)境中,一般情況下分為 開發(fā)、測試、生產(chǎn) 等等不同環(huán)境,不同環(huán)境中的配置也是不同的,在 Apollo 中默認(rèn)提供了

四種環(huán)境:

FAT:功能測試環(huán)境

UAT:集成測試環(huán)境

DEV:開發(fā)環(huán)境

PRO:生產(chǎn)環(huán)境

在程序中如果想指定使用哪個(gè)環(huán)境,可以配置變量 env 的值為對應(yīng)環(huán)境名稱即可。

3、cluster

1、一個(gè)應(yīng)用下不同實(shí)例的分組,比如典型的可以按照數(shù)據(jù)中心分,把上海機(jī)房的應(yīng)用實(shí)例分為一個(gè)集群,把北京機(jī)房的應(yīng)用實(shí)例分為另一個(gè)集群。

2、對不同的集群,同一個(gè)配置可以有不一樣的值,比如說上面所指的兩個(gè)北京、上海兩個(gè)機(jī)房設(shè)置兩個(gè)集群,都有 mysql 配置參數(shù),其中參數(shù)中配置的地址是不一樣的。

4、namespace

一個(gè)應(yīng)用中不同配置的分組,可以簡單地把 namespace 類比為不同的配置文件,不同類型的配置存放在不同的文件中,如數(shù)據(jù)庫配置文件,RPC 配置文件等。

熟悉 SpringBoot 的都知道,SpringBoot 項(xiàng)目都有一個(gè)默認(rèn)配置文件 application.yml,如果還想用多個(gè)配置,可以創(chuàng)建多個(gè)配置文件來存放不同的配置信息,通過

指定 spring.profiles.active 參數(shù)指定應(yīng)用不同的配置文件。這里的 namespace 概念與其類似,將不同的配置放到不同的配置 namespace 中。

Namespace 分為兩種權(quán)限,分別為:

public(公共的):public權(quán)限的 Namespace,能被任何應(yīng)用獲取。
private(私有的):只能被所屬的應(yīng)用獲取到。一個(gè)應(yīng)用嘗試獲取其它應(yīng)用 private 的 Namespace,Apollo 會(huì)報(bào) “404” 異常。

Apollo實(shí)時(shí)發(fā)布配置

1. 配置發(fā)布后的實(shí)時(shí)推送設(shè)計(jì)

配置中心最重要的一個(gè)特性就是實(shí)時(shí)推送,正因?yàn)橛羞@個(gè)特性,我們才可以依賴配置中心做很多事情。如圖所示。

如何理解攜程架構(gòu)部開源的配置中心Apollo

如何理解攜程架構(gòu)部開源的配置中心Apollo

圖 1 簡要描述了配置發(fā)布的大致過程。

  • 用戶在 Portal 中進(jìn)行配置的編輯和發(fā)布。

  • Portal 會(huì)調(diào)用 Admin Service 提供的接口進(jìn)行發(fā)布操作。

  • Admin Service 收到請求后,發(fā)送 ReleaseMessage 給各個(gè) Config Service,通知 Config Service 配置發(fā)生變化。

  • Config Service 收到 ReleaseMessage 后,通知對應(yīng)的客戶端,基于 Http 長連接實(shí)現(xiàn)。

2. 發(fā)送 ReleaseMessage 的實(shí)現(xiàn)方式

ReleaseMessage 消息是通過 Mysql 實(shí)現(xiàn)了一個(gè)簡單的消息隊(duì)列。之所以沒有采用消息中間件,是為了讓 Apollo 在部署的時(shí)候盡量簡單,盡可能減少外部依賴,如圖所示。

如何理解攜程架構(gòu)部開源的配置中心Apollo

上圖簡要描述了發(fā)送 ReleaseMessage 的大致過程:

  • Admin Service 在配置發(fā)布后會(huì)往 ReleaseMessage 表插入一條消息記錄。

  • Config Service 會(huì)啟動(dòng)一個(gè)線程定時(shí)掃描 ReleaseMessage 表,來查看是否有新的消息記錄。

  • Config Service 發(fā)現(xiàn)有新的消息記錄,就會(huì)通知到所有的消息監(jiān)聽器。

  • 消息監(jiān)聽器得到配置發(fā)布的信息后,就會(huì)通知對應(yīng)的客戶端。

3. Config Service 通知客戶端的實(shí)現(xiàn)方式

通知采用基于 Http 長連接實(shí)現(xiàn),主要分為下面幾個(gè)步驟:

  • 客戶端會(huì)發(fā)起一個(gè) Http 請求到 Config Service 的 notifications/v2 接口。

  • notifications/v2 接口通過 Spring DeferredResult 把請求掛起,不會(huì)立即返回。

  • 如果在 60s 內(nèi)沒有該客戶端關(guān)心的配置發(fā)布,那么會(huì)返回 Http 狀態(tài)碼 304 給客戶端。

  • 如果發(fā)現(xiàn)配置有修改,則會(huì)調(diào)用 DeferredResult 的 setResult 方法,傳入有配置變化的 namespace 信息,同時(shí)該請求會(huì)立即返回。

  • 客戶端從返回的結(jié)果中獲取到配置變化的 namespace 后,會(huì)立即請求 Config Service 獲取該 namespace 的最新配置。

4. 源碼解析實(shí)時(shí)推送設(shè)計(jì)

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

當(dāng)然,這些代碼比較簡單,很多細(xì)節(jié)就不做考慮了,只是為了能夠讓大家明白 Apollo 推送的核心原理。

發(fā)送 ReleaseMessage 的邏輯我們就寫一個(gè)簡單的接口,用隊(duì)列存儲(chǔ),測試的時(shí)候就調(diào)用這個(gè)接口模擬配置有更新,發(fā)送 ReleaseMessage 消息。具體代碼如下所示。

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

消息發(fā)送之后,根據(jù)前面講過的 Config Service 會(huì)啟動(dòng)一個(gè)線程定時(shí)掃描 ReleaseMessage 表,查看是否有新的消息記錄,然后取通知客戶端,在這里我們也會(huì)啟動(dòng)一個(gè)線程去掃描,具體代碼如下所示。

@Componentpublic class ReleaseMessageScanner implements InitializingBean {  @Autowired  private NotificationControllerV2 configController;  @Override  public void afterPropertiesSet() throws Exception {    // 定時(shí)任務(wù)從數(shù)據(jù)庫掃描有沒有新的配置發(fā)布    new Thread(() -> {      for (;;) {        String result = NotificationControllerV2.queue.poll();        if (result != null) {        ReleaseMessage message = new ReleaseMessage();        message.setMessage(result);        configController.handleMessage(message);      }    }  }).start();  ;  }}

循環(huán)讀取 NotificationControllerV2 中的隊(duì)列,如果有消息的話就構(gòu)造一個(gè) Release-Message 的對象,然后調(diào)用 NotificationControllerV2 中的 handleMessage() 方法進(jìn)行消息的處理。

ReleaseMessage 就一個(gè)字段,模擬消息內(nèi)容,具體代碼如下所示。

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

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

NotificationControllerV2 實(shí)現(xiàn)了 ReleaseMessageListener 接口,ReleaseMessageListener 中定義了 handleMessage() 方法,具體代碼如下所示。

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

handleMessage 就是當(dāng)配置發(fā)生變化的時(shí)候,發(fā)送通知的消息監(jiān)聽器。消息監(jiān)聽器在得到配置發(fā)布的信息后,會(huì)通知對應(yīng)的客戶端,具體代碼如下所示。

@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 的實(shí)時(shí)推送是基于 Spring DeferredResult 實(shí)現(xiàn)的,在 handleMessage() 方法中可以看到是通過 deferredResults 獲取 DeferredResult,deferredResults 就是第一行的 Multimap,Key 其實(shí)就是消息內(nèi)容,Value 就是 DeferredResult 的業(yè)務(wù)包裝類 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() 方法設(shè)置返回結(jié)果給客戶端,以上就是當(dāng)配置發(fā)生變化,然后通過消息監(jiān)聽器通知客戶端的原理,那么客戶端是在什么時(shí)候接入的呢?具體代碼如下。

@RestControllerpublic class NotificationControllerV2 implements ReleaseMessageListener {// 模擬配置更新, 向其中插入數(shù)據(jù)表示有更新  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 中提供了一個(gè) /getConfig 的接口,客戶端在啟動(dòng)的時(shí)候會(huì)調(diào)用這個(gè)接口,這個(gè)時(shí)候會(huì)執(zhí)行 getApolloConfigNotifications() 方法去獲取有沒有配置的變更信息,如果有的話證明配置修改過,直接就通過 deferredResultWrapper.setResult(newNotifications) 返回結(jié)果給客戶端,客戶端收到結(jié)果后重新拉取配置的信息覆蓋本地的配置。

如果 getApolloConfigNotifications() 方法沒有返回配置修改的信息,則證明配置沒有發(fā)生修改,那就將 DeferredResultWrapper 對象添加到 deferredResults 中,等待后續(xù)配置發(fā)生變化時(shí)消息監(jiān)聽器進(jìn)行通知。

同時(shí)這個(gè)請求就會(huì)掛起,不會(huì)立即返回,掛起是通過 DeferredResultWrapper 中的下面這部分代碼實(shí)現(xiàn)的,具體代碼如下所示。

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);}

在創(chuàng)建 DeferredResult 對象的時(shí)候指定了超時(shí)的時(shí)間和超時(shí)后返回的響應(yīng)碼,如果 60s 內(nèi)沒有消息監(jiān)聽器進(jìn)行通知,那么這個(gè)請求就會(huì)超時(shí),超時(shí)后客戶端收到的響應(yīng)碼就是 304。

整個(gè) Config Service 的流程就走完了,接下來我們來看一下客戶端是怎么實(shí)現(xiàn)的,我們簡單地寫一個(gè)測試類模擬客戶端注冊,具體代碼如下所示。

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("結(jié)果 " + result);                return result.toString();            }        } catch (IOException e) {            e.printStackTrace();        } finally {            if (connection != null) {                connection.disconnect();            }        }        return null;    }}

首先啟動(dòng) /getConfig 接口所在的服務(wù),然后啟動(dòng)客戶端,然后客戶端就會(huì)發(fā)起注冊請求,如果有修改直接獲取到結(jié)果,則進(jìn)行配置的更新操作。如果無修改,請求會(huì)掛起,這里客戶端設(shè)置的讀取超時(shí)時(shí)間是 90s,大于服務(wù)端的 60s 超時(shí)時(shí)間。
每次收到結(jié)果后,無論是有修改還是無修改,都必須重新進(jìn)行注冊,通過這樣的方式就可以達(dá)到配置實(shí)時(shí)推送的效果。
我們可以調(diào)用之前寫的 /addMsg 接口來模擬配置發(fā)生變化,調(diào)用之后客戶端就能馬上得到返回結(jié)果。

Apollo客戶端設(shè)計(jì)

如何理解攜程架構(gòu)部開源的配置中心Apollo

上圖簡要描述了Apollo客戶端的實(shí)現(xiàn)原理:

  1. 客戶端和服務(wù)端保持了一個(gè)長連接,從而能第一時(shí)間獲得配置更新的推送。(通過Http Long Polling實(shí)現(xiàn))

  2. 客戶端還會(huì)定時(shí)從Apollo配置中心服務(wù)端拉取應(yīng)用的最新配置。

    • 這是一個(gè)fallback機(jī)制,為了防止推送機(jī)制失效導(dǎo)致配置不更新

    • 客戶端定時(shí)拉取會(huì)上報(bào)本地版本,所以一般情況下,對于定時(shí)拉取的操作,服務(wù)端都會(huì)返回304 - Not Modified

    • 定時(shí)頻率默認(rèn)為每5分鐘拉取一次,客戶端也可以通過在運(yùn)行時(shí)指定System Property: apollo.refreshInterval來覆蓋,單位為分鐘。

  3. 客戶端從Apollo配置中心服務(wù)端獲取到應(yīng)用的最新配置后,會(huì)保存在內(nèi)存中

  4. 客戶端會(huì)把從服務(wù)端獲取到的配置在本地文件系統(tǒng)緩存一份(在遇到服務(wù)不可用,或網(wǎng)絡(luò)不通的時(shí)候,依然能從本地恢復(fù)配置)

  5. 應(yīng)用程序可以從Apollo客戶端獲取最新的配置、訂閱配置更新通知

Apollo客戶端用法

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

  • API方式靈活,功能完備,配置值實(shí)時(shí)更新(熱發(fā)布),支持所有Java環(huán)境。

  • Spring方式接入簡單,結(jié)合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在運(yùn)行時(shí)自動(dòng)更新,具體參見PR #972。(v0.10.0之前的版本在配置變化后不會(huì)重新注入,需要重啟才會(huì)更新,如果需要配置值實(shí)時(shí)更新,可以參考后續(xù)3.2.2 Spring Placeholder的使用的說明)

  • Spring方式也可以結(jié)合API方式使用,如注入Apollo的Config對象,就可以照常通過API方式獲取配置了:

    @ApolloConfigprivate Config config; //inject config for namespace application


  • 更多有意思的實(shí)際使用場景和示例代碼,請參考apollo-use-cases

1、API使用方式

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

獲取默認(rèn)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對應(yīng)的實(shí)時(shí)最新的配置值。

另外,配置值從內(nèi)存中獲取,所以不需要應(yīng)用自己做緩存。

監(jiān)聽配置變化事件

監(jiān)聽配置變化事件只在應(yīng)用真的關(guān)心配置變化,需要在配置變化時(shí)得到通知時(shí)使用,比如:數(shù)據(jù)庫連接串變化后需要重建連接等。

如果只是希望每次都取到最新的配置的話,只需要按照上面的例子,調(diào)用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

獲取時(shí)需要使用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目前既支持比較傳統(tǒng)的基于XML的配置,也支持目前比較流行的基于Java(推薦)的配置。

如果是Spring Boot環(huán)境,建議參照3.2.1.3 Spring Boot集成方式(推薦)配置。

需要注意的是,如果之前有使用org.springframework.beans.factory.config.PropertyPlaceholderConfigurer的,請?zhí)鎿Q成org.springframework.context.support.PropertySourcesPlaceholderConfigurer。Spring 3.1以后就不建議使用PropertyPlaceholderConfigurer了,要改用PropertySourcesPlaceholderConfigurer。

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

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

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

基于XML的配置

注:需要把a(bǔ)pollo相關(guān)的xml namespace加到配置文件頭上,不然會(huì)報(bào)xml語法錯(cuò)誤。

1.注入默認(rèn)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">    <!-- 這個(gè)是最簡單的配置形式,一般應(yīng)用用這種形式就可以了,用來指示Apollo注入application namespace的配置到Spring環(huán)境中 -->    <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.注入多個(gè)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">    <!-- 這個(gè)是最簡單的配置形式,一般應(yīng)用用這種形式就可以了,用來指示Apollo注入application namespace的配置到Spring環(huán)境中 -->    <apollo:config/>    <!-- 這個(gè)是稍微復(fù)雜一些的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環(huán)境中 -->    <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.注入多個(gè)namespace,并且指定順序

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

apollo:config如果不指定order,那么默認(rèn)是最低優(yōu)先級。

<?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"/>    <!-- 這個(gè)是最復(fù)雜的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環(huán)境中,并且順序在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一起使用,不然不會(huì)生效。

1.注入默認(rèn)namespace的配置到Spring中

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

2.注入多個(gè)namespace的配置到Spring中

@Configuration@EnableApolloConfigpublic class SomeAppConfig {  @Bean  public TestJavaConfigBean javaConfigBean() {    return new TestJavaConfigBean();  }}//這個(gè)是稍微復(fù)雜一些的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環(huán)境中@Configuration@EnableApolloConfig({"FX.apollo", "application.yml"})public class AnotherAppConfig {}

3.注入多個(gè)namespace,并且指定順序

//這個(gè)是最復(fù)雜的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環(huán)境中,并且順序在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在啟動(dòng)階段就需要讀取配置做一些事情(如dubbo-spring-boot-project),所以對于Spring Boot環(huán)境建議通過以下方式來接入Apollo(需要0.10.0及以上版本)。

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

注入默認(rèn)application namespace的配置示例

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

注入非默認(rèn)application namespace或多個(gè)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配置加載提到初始化日志系統(tǒng)之前(1.2.0+)

從1.2.0版本開始,如果希望把日志相關(guān)的配置(如logging.level.root=infologback-spring.xml中的參數(shù))也放在Apollo管理,那么可以額外配置apollo.bootstrap.eagerLoad.enabled=true來使Apollo的加載順序放到日志系統(tǒng)加載之前,不過這會(huì)導(dǎo)致Apollo的啟動(dòng)過程無法通過日志的方式輸出(因?yàn)閳?zhí)行Apollo加載的時(shí)候,日志系統(tǒng)壓根沒有準(zhǔn)備好呢!所以在Apollo代碼中使用Slf4j的日志輸出便沒有任何內(nèi)容),更多信息可以參考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應(yīng)用通常會(huì)使用Placeholder來注入配置,使用的格式形如${someKey:someDefaultValue},如${timeout:100}。冒號前面的是key,冒號后面的是默認(rèn)值。

建議在實(shí)際使用時(shí)盡量給出默認(rèn)值,以免由于key沒有定義導(dǎo)致運(yùn)行時(shí)錯(cuò)誤。

從v0.10.0開始的版本支持placeholder在運(yùn)行時(shí)自動(dòng)更新,具體參見PR #972。

如果需要關(guān)閉placeholder在運(yùn)行時(shí)自動(dòng)更新功能,可以通過以下兩種方式關(guān)閉:

1. 通過設(shè)置System Property apollo.autoUpdateInjectedSpringProperties,如啟動(dòng)時(shí)傳入-Dapollo.autoUpdateInjectedSpringProperties=false

2.通過設(shè)置META-INF/app.properties中的apollo.autoUpdateInjectedSpringProperties屬性,如

app.id=SampleAppapollo.autoUpdateInjectedSpringProperties=false

 XML使用方式

假設(shè)我有一個(gè)TestXmlBean,它有兩個(gè)配置項(xiàng)需要注入:

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中會(huì)使用如下方式來定義(假設(shè)應(yīng)用默認(rèn)的application namespace中有timeout和batch的配置項(xiàng)):

<?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使用方式

假設(shè)我有一個(gè)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類中按照下面的方式使用(假設(shè)應(yīng)用默認(rèn)的application namespace中有timeoutbatch的配置項(xiàng)):

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

ConfigurationProperties使用方式

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

Apollo也支持這種方式,下面的例子會(huì)把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類中按照下面的方式使用(假設(shè)應(yīng)用默認(rèn)的application namespace中有redis.cache.expireSecondsredis.cache.commandTimeout的配置項(xiàng)):

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

需要注意的是,@ConfigurationProperties如果需要在Apollo配置變化時(shí)自動(dòng)更新注入的值,需要配合使用EnvironmentChangeEvent或RefreshScope。相關(guān)代碼實(shí)現(xiàn),可以參考apollo-use-cases項(xiàng)目中的ZuulPropertiesRefresher.java和apollo-demo項(xiàng)目中的SampleRedisConfig.java以及SpringBootApolloRefreshConfig.java

Spring Annotation支持

Apollo同時(shí)還增加了幾個(gè)新的Annotation來簡化在Spring環(huán)境中的使用。

  1. @ApolloConfig

    • 用來自動(dòng)注入Config對象

  2. @ApolloConfigChangeListener

    • 用來自動(dòng)注冊ConfigChangeListener

  3. @ApolloJsonValue

    • 用來把配置的json字符串自動(dòng)注入為對象

使用樣例如下:

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();  }}

 已有配置遷移

很多情況下,應(yīng)用可能已經(jīng)有不少配置了,比如Spring Boot的應(yīng)用,就會(huì)有bootstrap.properties/yml, application.properties/yml等配置。

在應(yīng)用接入Apollo之后,這些配置是可以非常方便的遷移到Apollo的,具體步驟如下:

  1. 在Apollo為應(yīng)用新建項(xiàng)目

  2. 在應(yīng)用中配置好META-INF/app.properties

  3. 建議把原先配置先轉(zhuǎn)為properties格式,然后通過Apollo提供的文本編輯模式全部粘帖到應(yīng)用的application namespace,發(fā)布配置

    • 如果原來格式是yml,可以使用YamlPropertiesFactoryBean.getObject轉(zhuǎn)成properties格式

  4. 如果原來是yml,想繼續(xù)使用yml來編輯配置,那么可以創(chuàng)建私有的application.yml namespace,把原來的配置全部粘貼進(jìn)去,發(fā)布配置

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

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

    • 如果需要保留本地配置文件,需要注意部分配置如server.port必須確保本地文件已經(jīng)刪除該配置項(xiàng)

如:

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的高可用性設(shè)計(jì)


高可用是分布式系統(tǒng)架構(gòu)設(shè)計(jì)中必須考慮的因素之一,它通常是指通過設(shè)計(jì)減少系統(tǒng)不能提供服務(wù)的時(shí)間。

Apollo 在高可用設(shè)計(jì)上下了很大的功夫,下面我們來簡單的分析下:

1)某臺(tái)Config Service 下線

無影響,Config Service 可用部署多個(gè)節(jié)點(diǎn)。

2)所有 Config Service 下線

所有 Config Service 下線會(huì)影響客戶端的使用,無法讀取最新的配置??刹捎米x取本地緩存的配置文件來過渡。

3)某臺(tái) Admin Service 下線

無影響,Admin Service 可用部署多個(gè)節(jié)點(diǎn)。

4)所有 Admin Service 下線

Admin Service 是服務(wù)于 Portal,所有 Admin Service 下線之后只會(huì)影響 Portal 的操作,不會(huì)影響客戶端,客戶端是依賴 Config Service。

5)某臺(tái) Portal 下線

Portal 可用部署多臺(tái),通過 Nginx 做負(fù)載,某臺(tái)下線之后不影響使用。

6)全部 Portal 下線

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

7)數(shù)據(jù)庫宕機(jī)

當(dāng)配置的數(shù)據(jù)庫宕機(jī)之后,對客戶端是沒有影響的,但是會(huì)導(dǎo)致 Portal 中無法更新配置。當(dāng)客戶端重啟,這個(gè)時(shí)候如果需要重新拉取配置,就會(huì)有影響,可采取開啟配置緩存的選項(xiàng)來避免數(shù)據(jù)庫宕機(jī)帶來的影響。

感謝各位的閱讀,以上就是“如何理解攜程架構(gòu)部開源的配置中心Apollo”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對如何理解攜程架構(gòu)部開源的配置中心Apollo這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI