您好,登錄后才能下訂單哦!
這篇文章主要講解了“如何理解攜程架構部開源的配置中心Apollo”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何理解攜程架構部開源的配置中心Apollo”吧!
隨著程序功能的日益復雜,程序的配置日益增多:各種功能的開關、參數的配置、服務器的地址……
對程序配置的期望值也越來越高:配置修改后實時生效,灰度發布,分環境、分集群管理配置,完善的權限、審核機制……
在這樣的大環境下,傳統的通過配置文件、數據庫等方式已經越來越無法滿足開發人員對配置管理的需求。
想必大家都深有體會,我們做的項目都伴隨著各種配置文件,而且總是本地配置一套配置文件,測試服配置一套,正式服配置一套,有時候一不小心就改錯了,挨罵是小事,扣績效那可就鬧大了。
而且每當項目發布的時候,配置文件也會被打包進去,也就是配置文件會跟著項目一起發布。然后每次出現問題需要我們修改配置文件的時候,我們總是得先在本地修改,然后重新發布才能讓新的配置生效。
當請求壓力越來越大,你的項目也會從 1 個節點變成多個節點,這個時候如果配置需要發生變化,對應的修改操作也是相同的,只需要在項目中修改一次即可,但對于發布操作工作量就比之前大了很多,因為要發布多個節點。
修改這些配置,增加的發布的工作量降低了整體的工作效率,為了能夠提升工作效率,配置中心應運而生了,我們可以將配置統一存放在配置中心來進行管理。
總體而言在沒有引入配置中心之前,我們都會面臨以下問題:
配置散亂格式不標準,有的用 properties 格式,有的用 xml 格式,還有的存 DB,團隊傾向自造輪子,做法五花八門。
主要采用本地靜態配置,配置修改麻煩,配置修改一般需要經過一個較長的測試發布周期。在分布式微服務環境下,當服務實例很多時,修改配置費時費力。
易引發生產事故,這個是我親身經歷,之前在一家互聯網公司,有團隊在發布的時候將測試環境的配置帶到生產上,引發百萬級資損事故。
配置缺乏安全審計和版本控制功能,誰改的配置?改了什么?什么時候改的?無從追溯,出了問題也無法及時回滾。
增加了運維小哥哥的工作量,極大的損害了運維小哥哥和開發小哥哥的基情。
配置中心就是把項目中各種個樣的配置、參數、開關,全部都放到一個集中的地方進行統一管理,并提供一套標準的接口。當各個服務需要獲取配置的時候,就來配置中心的接口拉取。當配置中心中的各種參數有更新的時候,也能通知到各個服務實時的過來同步最新的信息,使之動態更新。
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提供了一個統一界面集中式管理不同環境(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還提供了打包腳本,一鍵就可以生成所有需要的安裝包,并且支持自定義運行時參數
首先我們來看看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進程中
為什么我們采用Eureka作為服務注冊中心,而不是使用傳統的zk、etcd呢?我大致總結了一下,有以下幾方面的原因:
它提供了完整的Service Registry和Service Discovery實現
首先是提供了完整的實現,并且也經受住了Netflix自己的生產環境考驗,相對使用起來會比較省心。
和Spring Cloud無縫集成
的項目本身就使用了Spring Cloud和Spring Boot,同時Spring Cloud還有一套非常完善的開源代碼來整合Eureka,所以使用起來非常方便。
另外,Eureka還支持在我們應用自身的容器中啟動,也就是說我們的應用啟動完之后,既充當了Eureka的角色,同時也是服務的提供者。這樣就極大的提高了服務的可用性。
這一點是我們選擇Eureka而不是zk、etcd等的主要原因,為了提高配置中心的可用性和降低部署復雜度,我們需要盡可能地減少外部依賴。
Open Source
最后一點是開源,由于代碼是開源的,所以非常便于我們了解它的實現原理和排查問題。
提供配置獲取接口
提供配置更新推送接口(基于Http long polling)
服務端使用Spring DeferredResult實現異步化,從而大大增加長連接數量
目前使用的tomcat embed默認配置是最多10000個連接(可以調整),使用了4C8G的虛擬機實測- 可以支撐10000個連接,所以滿足需求(一個應用實例只會發起一個長連接)。接口服務對象為Apollo客戶端
提供配置修改、發布等接口
接口服務對象為Portal
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和Spring Cloud Netflix提供服務注冊和發現
Config Service和Admin Service會向Eureka注冊服務,并保持心跳
為了簡單起見,目前Eureka在部署時和Config Service是在一個JVM進程中的(通過Spring Cloud Netflix)
提供Web界面供用戶管理配置
通過Meta Server獲取Admin Service服務列表(IP+Port),通過IP+Port訪問服務
在Portal側做load balance、錯誤重試
Apollo提供的客戶端程序,為應用提供配置獲取、實時更新等功能
通過Meta Server獲取Config Service服務列表(IP+Port),通過IP+Port訪問服務
在Client側做load balance、錯誤重試
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” 異常。
配置中心最重要的一個特性就是實時推送,正因為有這個特性,我們才可以依賴配置中心做很多事情。如圖所示。
圖 1 簡要描述了配置發布的大致過程。
用戶在 Portal 中進行配置的編輯和發布。
Portal 會調用 Admin Service 提供的接口進行發布操作。
Admin Service 收到請求后,發送 ReleaseMessage 給各個 Config Service,通知 Config Service 配置發生變化。
Config Service 收到 ReleaseMessage 后,通知對應的客戶端,基于 Http 長連接實現。
ReleaseMessage 消息是通過 Mysql 實現了一個簡單的消息隊列。之所以沒有采用消息中間件,是為了讓 Apollo 在部署的時候盡量簡單,盡可能減少外部依賴,如圖所示。
上圖簡要描述了發送 ReleaseMessage 的大致過程:
Admin Service 在配置發布后會往 ReleaseMessage 表插入一條消息記錄。
Config Service 會啟動一個線程定時掃描 ReleaseMessage 表,來查看是否有新的消息記錄。
Config Service 發現有新的消息記錄,就會通知到所有的消息監聽器。
消息監聽器得到配置發布的信息后,就會通知對應的客戶端。
通知采用基于 Http 長連接實現,主要分為下面幾個步驟:
客戶端會發起一個 Http 請求到 Config Service 的 notifications/v2 接口。
notifications/v2 接口通過 Spring DeferredResult 把請求掛起,不會立即返回。
如果在 60s 內沒有該客戶端關心的配置發布,那么會返回 Http 狀態碼 304 給客戶端。
如果發現配置有修改,則會調用 DeferredResult 的 setResult 方法,傳入有配置變化的 namespace 信息,同時該請求會立即返回。
客戶端從返回的結果中獲取到配置變化的 namespace 后,會立即請求 Config Service 獲取該 namespace 的最新配置。
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客戶端的實現原理:
客戶端和服務端保持了一個長連接,從而能第一時間獲得配置更新的推送。(通過Http Long Polling實現)
客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。
這是一個fallback機制,為了防止推送機制失效導致配置不更新
客戶端定時拉取會上報本地版本,所以一般情況下,對于定時拉取的操作,服務端都會返回304 - Not Modified
定時頻率默認為每5分鐘拉取一次,客戶端也可以通過在運行時指定System Property: apollo.refreshInterval來覆蓋,單位為分鐘。
客戶端從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方式獲取配置了:
@ApolloConfigprivate Config config; //inject config for namespace application
更多有意思的實際使用場景和示例代碼,請參考apollo-use-cases
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的配置
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);
獲取時需要使用ConfigService.getConfigFile
接口并指定Format,如ConfigFileFormat.XML
。
String someNamespace = "test";ConfigFile configFile = ConfigService.getConfigFile("test", ConfigFileFormat.XML);String content = configFile.getContent();
配置
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整合。
注:需要把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>
相對于基于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除了支持上述兩種集成方式以外,還支持通過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=info
或logback-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來注入配置,使用的格式形如${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
假設我有一個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>
假設我有一個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中有timeout
和batch
的配置項):
@Configuration@EnableApolloConfigpublic class AppConfig { @Bean public TestJavaConfigBean javaConfigBean() { return new TestJavaConfigBean(); }}
Spring Boot提供了@ConfigurationProperties把配置注入到bean對象中。
Apollo也支持這種方式,下面的例子會把redis.cache.expireSeconds
和redis.cache.commandTimeout
分別注入到SampleRedisConfig的expireSeconds
和commandTimeout
字段中。
@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.expireSeconds
和redis.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
Apollo同時還增加了幾個新的Annotation來簡化在Spring環境中的使用。
@ApolloConfig
用來自動注入Config對象
@ApolloConfigChangeListener
用來自動注冊ConfigChangeListener
@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的,具體步驟如下:
在Apollo為應用新建項目
在應用中配置好META-INF/app.properties
建議把原先配置先轉為properties格式,然后通過Apollo提供的文本編輯模式全部粘帖到應用的application namespace,發布配置
如果原來格式是yml,可以使用YamlPropertiesFactoryBean.getObject轉成properties格式
如果原來是yml,想繼續使用yml來編輯配置,那么可以創建私有的application.yml namespace,把原來的配置全部粘貼進去,發布配置
需要apollo-client是1.3.0及以上版本
把原先的配置文件如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 在高可用設計上下了很大的功夫,下面我們來簡單的分析下:
無影響,Config Service 可用部署多個節點。
所有 Config Service 下線會影響客戶端的使用,無法讀取最新的配置。可采用讀取本地緩存的配置文件來過渡。
無影響,Admin Service 可用部署多個節點。
Admin Service 是服務于 Portal,所有 Admin Service 下線之后只會影響 Portal 的操作,不會影響客戶端,客戶端是依賴 Config Service。
Portal 可用部署多臺,通過 Nginx 做負載,某臺下線之后不影響使用。
對客戶端讀取配置是沒有影響的,只是不能通過 Portal 去查看,修改配置。
當配置的數據庫宕機之后,對客戶端是沒有影響的,但是會導致 Portal 中無法更新配置。當客戶端重啟,這個時候如果需要重新拉取配置,就會有影響,可采取開啟配置緩存的選項來避免數據庫宕機帶來的影響。
感謝各位的閱讀,以上就是“如何理解攜程架構部開源的配置中心Apollo”的內容了,經過本文的學習后,相信大家對如何理解攜程架構部開源的配置中心Apollo這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。