分布式配置中心--Apollo

Apollo(阿波羅)是攜程開源的分布式配置中心蹭越,能夠集中化管理應(yīng)用不同環(huán)境汁汗、不同集群的配置,支持配置熱發(fā)布并實(shí)時(shí)推送到應(yīng)用端振劳,并且具備規(guī)范的權(quán)限及流程治理等特性,適用于分布式微服務(wù)配置管理場(chǎng)景

Apollo配置中心介紹

程序功能日益復(fù)雜油狂,程序配置日益增多:各種功能開關(guān)历恐、參數(shù)配置寸癌、服務(wù)器地址...
對(duì)程序配置的期望也越來越高:熱部署并實(shí)時(shí)生效、灰度發(fā)布弱贼、分環(huán)境分集群管理配置蒸苇、完善的權(quán)限審核機(jī)制...
在這樣的背景下,Apollo配置中心應(yīng)運(yùn)而生吮旅。Apollo支持四個(gè)維度Key-Value格式的配置
*** Application(應(yīng)用)** 實(shí)際使用配置的應(yīng)用溪烤,Apollo客戶端在運(yùn)行時(shí)需要知道當(dāng)前應(yīng)用是誰,從而可以去獲取對(duì)應(yīng)的配置庇勃。每個(gè)應(yīng)用都有對(duì)應(yīng)的身份標(biāo)識(shí)--appId檬嘀,需要在代碼中配置

  • **Environment(環(huán)境) ** 配置對(duì)應(yīng)的環(huán)境,Apollo客戶端需要知道當(dāng)前應(yīng)用出于哪個(gè)環(huán)境责嚷,鸳兽,從而可以去獲取應(yīng)用的配置;環(huán)境和代碼無關(guān)罕拂,同一份代碼部署在不同的環(huán)境就應(yīng)該獲取不同環(huán)境的配置揍异;環(huán)境默認(rèn)是通過讀取機(jī)器上的配置(server.properties的env屬性)指定的
  • Cluster(集群) 一個(gè)應(yīng)用下不同實(shí)例的分組,例如按照不同數(shù)據(jù)中心劃分爆班,把上海機(jī)房的實(shí)例分為一個(gè)集群衷掷、把深圳機(jī)房的實(shí)例分為一個(gè)集群;對(duì)于不同的Cluster蛋济,同一個(gè)配置可以有不一樣的值棍鳖;集群默認(rèn)是通過讀取機(jī)器上的配置指定的(server.properties的idc屬性)
  • **Namespace(命名空間) ** 一個(gè)應(yīng)用下不同配置的分組,是配置項(xiàng)的集合碗旅,可以簡(jiǎn)單地把Namespace類別為(配置)文件渡处,不同類型的配置存放在不同的文件中,例如數(shù)據(jù)庫配置文件祟辟、RPC配置文件医瘫、應(yīng)用自身的配置文件等;應(yīng)用可以直接讀取到公共組件的配置namespace旧困,例如DAL醇份、RPC等;應(yīng)用也可以通過繼承公共組件的配置namespace來對(duì)公共組件的配置做調(diào)整吼具,如DAL的初始數(shù)據(jù)庫連接數(shù)

Apollo在創(chuàng)建項(xiàng)目的時(shí)候僚纷,都會(huì)默認(rèn)創(chuàng)建一個(gè)"application"的Namespace,"application"是個(gè)應(yīng)用自身使用的拗盒。例如Spring Boot中項(xiàng)目的默認(rèn)配置文件application.yaml怖竭,這里application.yaml就等同于"application"的Namespace。對(duì)于大多數(shù)應(yīng)用來說,"application"Namespace已經(jīng)能滿足日常配置使用場(chǎng)景

客戶端獲取"application"Namespace的代碼如下

Config config = ConfigService.getAppConfig()

客戶端獲取非"application"Namespace的代碼如下

Config config = ConfigService.getConfig(namespaceName)

Namespace的格式 配置文件有多種格式陡蝇,properties痊臭、xml哮肚、yml、yaml广匙、json等允趟,同樣Namespace也具有這些格式
tips: 非properties格式的namespace,在客戶端使用時(shí)需要調(diào)用ConfigService.getConfigFile(String namespace, ConfigFileFormat configFileFormat)來獲取鸦致,如果使用Htpp接口直接調(diào)用時(shí)潮剪,對(duì)應(yīng)的namespace參數(shù)需要傳入namespace的名字加上后綴名,如datasource.json
Namespace的獲取權(quán)限分類 此處權(quán)限相是對(duì)于Apollo客戶端來說的
private(私有的)權(quán)限 private權(quán)限的Namespace蹋凝,只能被所屬的應(yīng)用獲取到鲁纠。一個(gè)應(yīng)用嘗試獲取其他應(yīng)用private的Namespace,Apollo客戶端會(huì)報(bào)"404"異常

public(公共的)權(quán)限 具有public權(quán)限的Namespace鳍寂,能被任何應(yīng)用獲取

Namespace的類型

  • 私有類型 具有private權(quán)限改含,例如上文中提到的"application"Namespace就是私有類型

  • 公共類型 具有public權(quán)限,公共類型的Namespace相當(dāng)于游離于應(yīng)用之外的配置迄汛,且通過Namespace的名稱去標(biāo)識(shí)公共Namespace捍壤,所以公共Namespace的名稱必須全局唯一

使用場(chǎng)景 部門級(jí)別共享的配置、小組級(jí)別共享的配置鞍爱、幾個(gè)項(xiàng)目之間共享的配置鹃觉、中間件客戶端的配置

  • 關(guān)聯(lián)類型(繼承類型) 具有private權(quán)限,關(guān)聯(lián)類型的Namespace繼承于公共類型的Namespace睹逃,用于覆蓋公共Namespace的某些配置盗扇。例如公共Namespace有兩個(gè)配置項(xiàng)

      k1 = v1
      k2 = v2
    

    然后應(yīng)用A有一個(gè)關(guān)聯(lián)類型的Namespace關(guān)聯(lián)此公共Namespace,且以新值v3覆蓋配置項(xiàng)k1沉填。那么在應(yīng)用A實(shí)際運(yùn)行時(shí)疗隶,獲取到的公共Namespace的配置為

      k1 = v3
      k2 = v2
    

    使用場(chǎng)景 假設(shè)RPC框架的配置(如:timeout)有以下要求

  • 提供一份全公司默認(rèn)的配置,且可動(dòng)態(tài)調(diào)整

  • RPC客戶端項(xiàng)目可以自定義某些配置項(xiàng)且可動(dòng)態(tài)調(diào)整
    結(jié)合Apollo的公共類型的Namespace和關(guān)聯(lián)類型的Namespace翼闹。RPC團(tuán)隊(duì)在Apollo上維護(hù)一個(gè)叫“rpc-client”的公共Namespace斑鼻,在"rpc-client"Namespace上配置默認(rèn)的參數(shù)值。rpc-client.jar里的代碼讀取"rpc-client"Namespace的配置即可猎荠;如需要調(diào)整默認(rèn)的配置坚弱,只需要修改公共類型"rpc-client"Namespace的配置;如果客戶端項(xiàng)目想要自定義或動(dòng)態(tài)修改某些配置項(xiàng)关摇,只需要在Apollo自己項(xiàng)目下關(guān)聯(lián)"rpc-client"荒叶,就能創(chuàng)建關(guān)聯(lián)類型"rpc-client"的Namespace,然后在關(guān)聯(lián)類型下修改配置項(xiàng)即可输虱。這里rpc-client.jar是在應(yīng)用容器里運(yùn)行的些楣,所以rpc-client獲取到"rpc-client"Namespace的配置是應(yīng)用的關(guān)聯(lián)類型的Namespace加上公共類型的Namespace
    例子 如下圖,有三個(gè)應(yīng)用:應(yīng)用A、應(yīng)用B戈毒、應(yīng)用C
    應(yīng)用A有兩個(gè)私有類型的Namespace:application和NS-Private,以及一個(gè)關(guān)聯(lián)類型的Namespace:NS-Public
    應(yīng)用B有一個(gè)私有類型的Namespace:application横堡,以及一個(gè)公共類型的Namespace:NS-Public
    應(yīng)用C只有一個(gè)私有類型的Namespace:application


      應(yīng)用A獲取Apollo配置
      //application 
      Config appConfig = ConfigService.getAppConfig();
      appConfig.getProperty("k1", null); // k1 = v11
      appConfig.getProperty("k2", null); // k2 = v21
      //NS-Private
      Config privateConfig = ConfigService.getConfig("NS-Private");
      privateConfig.getProperty("k1", null); // k1 = v3
      privateConfig.getProperty("k3", null); // k3 = v4
      //NS-Public埋市,覆蓋公共類型配置的情況,k4被覆蓋
      Config publicConfig = ConfigService.getConfig("NS-Public");
      publicConfig.getProperty("k4", null); // k4 = v6 cover
      publicConfig.getProperty("k6", null); // k6 = v6
      publicConfig.getProperty("k7", null); // k7 = v7
      
      應(yīng)用B獲取Apollo配置
      //application
      Config appConfig = ConfigService.getAppConfig();
      appConfig.getProperty("k1", null); // k1 = v12
      appConfig.getProperty("k2", null); // k2 = null
      appConfig.getProperty("k3", null); // k3 = v32
      
      //NS-Private命贴,由于沒有NS-Private Namespace 所以獲取到default value
      Config privateConfig = ConfigService.getConfig("NS-Private");
      privateConfig.getProperty("k1", "default value"); 
      //NS-Public
      Config publicConfig = ConfigService.getConfig("NS-Public");
      publicConfig.getProperty("k4", null); // k4 = v5
      publicConfig.getProperty("k6", null); // k6 = v6
      publicConfig.getProperty("k7", null); // k7 = v7
      
      應(yīng)用C獲取Apollo配置
      //application
      Config appConfig = ConfigService.getAppConfig();
      appConfig.getProperty("k1", null); // k1 = v12
      appConfig.getProperty("k2", null); // k2 = null
      appConfig.getProperty("k3", null); // k3 = v33
      //NS-Private道宅,由于沒有NS-Private Namespace 所以獲取到default value
      Config privateConfig = ConfigService.getConfig("NS-Private");
      privateConfig.getProperty("k1", "default value"); 
      //NS-Public,公共類型的Namespace胸蛛,任何項(xiàng)目都可以獲取到
      Config publicConfig = ConfigService.getConfig("NS-Public");
      publicConfig.getProperty("k4", null); // k4 = v5
      publicConfig.getProperty("k6", null); // k6 = v6
      publicConfig.getProperty("k7", null); // k7 = v7
    

ChangeListener 以上代碼可以看出污茵,在客戶端Namespace映射成一個(gè)Config對(duì)象,Namespace配置變更的監(jiān)聽器是注冊(cè)在Config對(duì)象上
## 在應(yīng)用A中監(jiān)聽application的Namespace代碼如下
Config appConfig = ConfigService.getAppConfig();
appConfig.addChangeListener(new ConfigChangeListener() {
public void onChange(ConfigChangeEvent changeEvent) {
//do something
}
})

    在應(yīng)用A中監(jiān)聽 NS-Private 的 Namespace代碼如下
    Config privateConfig = ConfigService.getConfig("NS-Private");
    privateConfig.addChangeListener(new ConfigChangeListener() {
    public void onChange(ConfigChangeEvent changeEvent) {
    //do something
    }
    })
    ## 在應(yīng)用A葬项、應(yīng)用B和應(yīng)用C中監(jiān)聽NS-Public Namespace代碼如下
    Config publicConfig = ConfigService.getConfig("NS-Public");
    publicConfig.addChangeListener(new ConfigChangeListener() {
        public void onChange(ConfigChangeEvent changeEvent) {
            //do something
            }
    })

配置的幾大屬性

  • 配置是獨(dú)立于程序的只讀變量
    • 配置首先是獨(dú)立于程序的泞当,同一份程序在不同的配置下會(huì)有不同的行為
    • 配置對(duì)于程序是只讀的,程序通過讀取配置來改變自己的行為民珍,程序不應(yīng)該去改變配置
  • 配置伴隨應(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ù)庫等
  • 配置需要治理
    • 權(quán)限控制 由于配置能改變程序行為嗜历,不正確的配置甚至能引起災(zāi)難,所以對(duì)配置的修改必須有比較完善的權(quán)限控制
    • 不同環(huán)境抖所、集群配置管理 同一份程序在不同的環(huán)境(開發(fā)梨州、測(cè)試、生產(chǎn))部蛇、不同的集群(如不同的數(shù)據(jù)中心)可能有不同的配置摊唇,所以需要有完善的環(huán)境、集群配置管理
    • 框架類組件配置管理 一類比較特殊的配置涯鲁,通常是由其他團(tuán)隊(duì)開發(fā)巷查、維護(hù),但是運(yùn)行時(shí)是在業(yè)務(wù)實(shí)際應(yīng)用內(nèi)的抹腿,所以本質(zhì)上可以認(rèn)為框架類組件也是應(yīng)用的一部分岛请,也需要比較完善的管理方式
      基于配置的特殊性,Apollo從設(shè)計(jì)之初就立志于成為一個(gè)有治理能力的配置發(fā)布平臺(tái)警绩,目前提供了以下的特性
  • 統(tǒng)一管理不同環(huán)境崇败、不同集群的配置
    • Apollo提供了一個(gè)統(tǒng)一界面集中式管理不同環(huán)境(Environment)、不同集群(Cluster)、不同命名空間(Namespace)的配置
    • 同一份代碼部署在不同的集群后室,可以有不同的配置
    • 通過命名空間可以很方便地支持多個(gè)不同應(yīng)用共享同一份配置缩膝,同時(shí)還允許應(yīng)用對(duì)共享的配置進(jìn)行覆蓋
  • 熱發(fā)布--配置修改實(shí)時(shí)生效 用戶在Apollo修改完配置并發(fā)布后,客戶端能實(shí)時(shí)(1秒)接收到最新的配置岸霹,并通知到應(yīng)用程序
  • 版本發(fā)布管理 所有的配置發(fā)布都有版本概念疾层,從而可以方便地支持配置的回滾
  • 灰度發(fā)布 支持配置的灰度發(fā)布,比如點(diǎn)了發(fā)布后贡避,只對(duì)部分應(yīng)用實(shí)例生效痛黎,等觀察一段時(shí)間沒問題后再推給所有應(yīng)用實(shí)例
  • 權(quán)限管理、發(fā)布審核刮吧、操作審計(jì)
    • 應(yīng)用和配置的管理都有完善的權(quán)限管理機(jī)制湖饱,對(duì)配置的管理還分為了編輯和發(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
  • 部署簡(jiǎn)單

配置獲取規(guī)則 僅當(dāng)應(yīng)用自定義了集群或namespace才需要旗笔。有了cluster概念后,配置的規(guī)則就顯得重要了拄踪,比如應(yīng)用部署在A機(jī)房蝇恶,但是并沒有在Apollo新建cluster或者在運(yùn)行時(shí)指定了cluster=SomeCluster,但是并沒有在Apollo新建cluster惶桐,這時(shí)候Apollo的行為是怎樣的撮弧?下面介紹配置獲取的規(guī)則
應(yīng)用自身配置的獲取規(guī)則
當(dāng)應(yīng)用使用下面的語句獲取配置時(shí),稱之為獲取應(yīng)用自身的配置姚糊,也就是應(yīng)用自身的application namespace的配置

    Config config = ConfigService.getAppConfig();

這種情況的配置獲取規(guī)則簡(jiǎn)而言之如下

  1. 首先查找運(yùn)行時(shí)cluster的配置(通過apollo.cluster指定)

  2. 如果沒有找到贿衍,則查找數(shù)據(jù)中心cluster的配置

  3. 如果還是沒有找到,則返回默認(rèn)cluster的配置
    圖示如下


    配置查找順序

    所以救恨,如果應(yīng)用部署在A數(shù)據(jù)中心贸辈,但是用戶沒有在Apollo創(chuàng)建cluster,那么獲取的配置就是默認(rèn)cluster(default)的肠槽;如果應(yīng)用部署在A數(shù)據(jù)中心擎淤,同時(shí)在運(yùn)行時(shí)指定了apollo.cluster=SomeCluster,但是沒有在Apollo創(chuàng)建cluster秸仙,那么獲取的配置就是A數(shù)據(jù)中心cluster的配置嘴拢,如果A數(shù)據(jù)中心cluster沒有配置的話,那么獲取的配置就是默認(rèn)cluster(default)的

  • 公共組件配置的獲取規(guī)則
    FX.Hermes.Producer為例寂纪,hermes producer是hermes發(fā)布的公共組件席吴。當(dāng)使用下面的語句獲取配置時(shí)赌结,稱之為獲取公共組件的配置

          Config config = ConfigService.getConfig("FX.Hermes.Producer")
    

對(duì)于這種情況獲取配置規(guī)則,簡(jiǎn)而言之如下

  1. 首先獲取當(dāng)前應(yīng)用下的FX.Hermes.Producernamespace的配置
  2. 然后獲取hermes應(yīng)用下FX.Hermes.Producernamespace的配置
  3. 上面兩部分配置的并集就是最終使用的配置孝冒,如有key一樣的部分柬姚,應(yīng)當(dāng)以應(yīng)用優(yōu)先
    圖示如下


    公共組件配置獲取規(guī)則

通過這種方式實(shí)現(xiàn)對(duì)框架組件的配置管理,框架組件提供方提供配置的默認(rèn)值庄涡,應(yīng)用如果有特殊需求可以自行覆蓋

Apollo配置中心的設(shè)計(jì)

總體設(shè)計(jì)

  • 基礎(chǔ)模型
    • 用戶在配置中心對(duì)配置進(jìn)行修改并發(fā)布
    • 配置中心通知Apollo客戶端有配置更新
    • Apollo客戶端從配置中心拉取最新的配置伤靠、更新本地配置并通知到應(yīng)用
基礎(chǔ)模型
  • 架構(gòu)模塊

    • Apollo包含七個(gè)模塊:四個(gè)功能相關(guān)的核心模塊和三個(gè)輔助服務(wù)發(fā)現(xiàn)的模塊
    • 四個(gè)核心模塊及其主要功能
      • ConfigService 提供配置獲取接口、提供配置推送接口啼染、服務(wù)于Apollo客戶端
      • AdminService 提供配置管理接口、提供配置修改發(fā)布接口焕梅、服務(wù)于管理界面Portal
      • Client 為應(yīng)用獲取配置迹鹅,支持實(shí)時(shí)更新、通過MetaServer獲取ConfigService的服務(wù)列表贞言、使用客戶端軟負(fù)載SLB方式調(diào)用ConfigService
      • Portal 配置管理界面斜棚、通過MetaServer獲取AdminService的服務(wù)列表、使用客戶端軟負(fù)載SLB的方式調(diào)用AdminService
    • 三個(gè)輔助服務(wù)發(fā)現(xiàn)模塊
      • Eureka 用于服務(wù)發(fā)現(xiàn)和注冊(cè)该窗、ConfigService和AdminService注冊(cè)實(shí)例并定期上報(bào)心跳弟蚀、和ConfigService部署于同一個(gè)進(jìn)程
      • MetaServer Portal通過域名訪問MetaServer獲取AdminService的地址列表、Client通過域名訪問MetaServer獲取ConfigService的地址列表酗失、相當(dāng)于一個(gè)EurekaProxy义钉、是一個(gè)邏輯角色和ConfigService部署于同一個(gè)進(jìn)程
      • NginxLB 和域名系統(tǒng)配合,協(xié)助Portal訪問MetaServer獲取AdminService地址列表规肴、和域名系統(tǒng)配合捶闸,協(xié)助Client訪問MetaServer獲取ConfigService地址列表、和域名系統(tǒng)配合拖刃,協(xié)助用戶訪問Portal進(jìn)行配置管理


        Apollo架構(gòu)
  • 架構(gòu)剖析

    Apollo架構(gòu)V1 如果不考慮分布式微服務(wù)架構(gòu)中的服務(wù)發(fā)現(xiàn)問題删壮,Apollo的最簡(jiǎn)架構(gòu)如下圖所示

    Apollo架構(gòu)V1

    要點(diǎn)

    • ConfigService是一個(gè)獨(dú)立的微服務(wù),服務(wù)于Client進(jìn)行配置獲取
    • Client和ConfigService保持長(zhǎng)連接兑牡,通過一種推拉結(jié)合(push & pull)的模式央碟,在實(shí)現(xiàn)配置實(shí)時(shí)更新的同時(shí),保證配置更新不丟失
    • AdminService是一個(gè)獨(dú)立的微服務(wù)均函,服務(wù)于Portal進(jìn)行配置管理亿虽。Portal通過調(diào)用AdminService進(jìn)行配置管理和發(fā)布
    • ConfigService和AdminService共享ConfigDB,ConfigDB中存放項(xiàng)目在某個(gè)環(huán)境中的配置信息苞也。ConfigService/AdminService/ConfigDB三者在每個(gè)環(huán)境(DEV/FAT/UAT/PRO)中都要部署一份
    • Protal有一個(gè)獨(dú)立的PortalDB经柴,存放用戶權(quán)限、項(xiàng)目和配置的元數(shù)據(jù)信息墩朦。Protal只需部署一份坯认,它可以管理多套環(huán)境

    Apollo架構(gòu) V2 為了保證高可用,ConfigService和AdminService都是無狀態(tài)以集群方式部署的,這時(shí)候就存在一個(gè)服務(wù)發(fā)現(xiàn)的問題:Client怎么找到ConfigService牛哺?Portal怎么找到AdminService陋气?為了解決這個(gè)問題,Apollo在其架構(gòu)中引入Eureka服務(wù)注冊(cè)中心組件引润,實(shí)現(xiàn)微服務(wù)間的服務(wù)注冊(cè)和發(fā)現(xiàn)巩趁,更新后的架構(gòu)如下圖所示

    Apollo架構(gòu)V2

    要點(diǎn)

    • ConfigService和AdminService啟動(dòng)后都會(huì)注冊(cè)到Eureka服務(wù)注冊(cè)中心,并定期發(fā)送存活心跳
    • Eureka采用集群方式部署淳附,使用分布式一致性協(xié)議保證每個(gè)實(shí)例的狀態(tài)最終一致

Apollo架構(gòu)V3 Eureka是自帶服務(wù)發(fā)現(xiàn)的Java客戶端的议慰,如果Apollo只支持Java客戶端接入,不支持其它語言客戶端接入的話奴曙,那么Client和Portal只需要引入Eureka的Java客戶端别凹,就可以實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)功能。發(fā)現(xiàn)目標(biāo)服務(wù)后洽糟,通過客戶端軟負(fù)載(SLB炉菲,例如Ribbon)就可以路由到目標(biāo)服務(wù)實(shí)例。這是一個(gè)經(jīng)典的微服務(wù)架構(gòu)坤溃,基于Eureka實(shí)現(xiàn)服務(wù)注冊(cè)發(fā)現(xiàn)+客戶端Ribbon配合實(shí)現(xiàn)軟路由拍霜,如下圖所示

Apollo架構(gòu)V3

Apollo架構(gòu)V4
為支持多語言客戶端接入,Apollo引入MetaServer角色薪介,它其實(shí)是一個(gè)Eureka的Proxy祠饺,將Eureka的服務(wù)發(fā)現(xiàn)接口以更簡(jiǎn)單明確的HTTP接口的形式暴露出來,方便Client/Protal通過簡(jiǎn)單的HTTPClient就可以查詢到ConfigService/AdminService的地址列表汁政。獲取到服務(wù)實(shí)例地址列表之后吠裆,再以簡(jiǎn)單的客戶端軟負(fù)載(Client SLB)策略路由定位到目標(biāo)實(shí)例,并發(fā)起調(diào)用

另一個(gè)問題烂完,MetaServer本身也是無狀態(tài)以集群方式部署的试疙,那么Client/Protal該如何發(fā)現(xiàn)MetaServer呢?一種傳統(tǒng)的做法是借助硬件或者軟件負(fù)載均衡器抠蚣,在攜程采用的是擴(kuò)展后的NginxLB(Software Load Balancer)祝旷,由運(yùn)維為MetaServer集群配置一個(gè)域名,指向NginxLB集群嘶窄,NginxLB再對(duì)MetaServer進(jìn)行負(fù)載均衡和流量轉(zhuǎn)發(fā)怀跛。Client/Portal通過域名+NginxLB間接訪問MetaServer集群

引入MetaServer和NginxLB之后的架構(gòu)如下圖


Apollo架構(gòu)V4

Apollo架構(gòu)V5
還剩下最后一個(gè)環(huán)節(jié),Portal也是無狀態(tài)的以集群方式部署的柄冲,用戶如何發(fā)現(xiàn)和訪問Portal吻谋?答案也是簡(jiǎn)單的傳統(tǒng)做法,用戶通過域名+NginxLB間接訪問Portal集群现横。所以V5版本是包括用戶端的最終的Apollo架構(gòu)全貌漓拾,如下圖所示

Apollo架構(gòu)V5

  • 服務(wù)端設(shè)計(jì)

    配置發(fā)布后的實(shí)時(shí)推送設(shè)計(jì) 在配置中心中阁最,一個(gè)重要的功能就是配置發(fā)布后實(shí)時(shí)推送到客戶端。下面我們簡(jiǎn)要看一下這塊是怎么設(shè)計(jì)實(shí)現(xiàn)的


    服務(wù)端設(shè)計(jì)
      上圖簡(jiǎn)要描述了配置發(fā)布的大致過程
      1. 用戶在Portal操作發(fā)布配置
      2. Portal調(diào)用Admin Service的接口操作發(fā)布
      3. Admin Service發(fā)布配置后骇两,發(fā)送ReleaseMessage給各Config Service
      4. Config Service收到ReleaseMessage后通知對(duì)應(yīng)的客戶端
    

    發(fā)送ReleaseMessage的實(shí)現(xiàn)方式 Admin Service在配置發(fā)布后速种,需要通知所有的Config Service有配置發(fā)布,從而Config Service可以通知對(duì)應(yīng)的客戶端來拉取最新的配置低千。從概念上看配阵,這是一個(gè)典型的消息使用場(chǎng)景,Admin Service作為Producer發(fā)出消息示血,各個(gè)Config Service作為consumer消費(fèi)消息棋傍。通過一個(gè)消息組件(Message Queue)就能很好地實(shí)現(xiàn)Admin Service和Config Service的解耦。在實(shí)現(xiàn)上难审,Apollo為盡量減少外部依賴瘫拣,沒有采用外部的消息中間件,而是通過數(shù)據(jù)庫實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的消息隊(duì)列

    實(shí)現(xiàn)方式如下
    1. Admin Service在配置發(fā)布后會(huì)往ReleaseMessage表插入一條消息記錄剔宪,消息內(nèi)容就是配置發(fā)布的AppId+Cluster+Namespace
    2. Config Service有一個(gè)線程會(huì)每秒掃描一次ReleaseMessage表,看是否有新的消息記
    3. Config Service如果發(fā)現(xiàn)有新的消息記錄壹无,那么會(huì)通知到所有的消息監(jiān)聽器(ReleaseMessageListener)葱绒,例如NotificationControllerV2
    4. 消息監(jiān)聽器得到配置發(fā)布的AppId+Cluster+Namespace后,會(huì)通知對(duì)應(yīng)的客戶端
    示意圖如下


    配置更新通知

Config Service通知客戶端的實(shí)現(xiàn)方式 消息監(jiān)聽器在得知有新的配置發(fā)布后是如何通知到客戶端的呢斗锭?其實(shí)現(xiàn)方式如下

  1. 客戶端會(huì)發(fā)起一個(gè)Http請(qǐng)求到Config Service的notifications/v2接口地淀,也就是NotificationControllerV2
  2. NotificationControllerV2不會(huì)立即返回結(jié)果,而是通過Spring DeferredResult把請(qǐng)求掛起
  3. 如果在60秒內(nèi)沒有該客戶端關(guān)心的配置發(fā)布岖是,那么會(huì)返回Http狀態(tài)碼304給客戶端
  4. 如果有該客戶端關(guān)心的配置發(fā)布帮毁,NotificationControllerV2會(huì)調(diào)用DeferredResult的setResult方法,傳入有配置變化的namespace信息豺撑,同時(shí)該請(qǐng)求會(huì)立即返回烈疚。客戶端從返回的結(jié)果中獲取到配置變化的namespace后聪轿,會(huì)立即請(qǐng)求Config Service獲取該namespace的最新配置

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


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

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

  1. 客戶端和服務(wù)端保持了一個(gè)長(zhǎng)連接(通過Http Long Polling實(shí)現(xiàn))爷肝,從而能第一時(shí)間獲得配置更新的推送
  2. 客戶端還會(huì)定期從Apollo配置中心服務(wù)端拉取應(yīng)用的最新配置
    • 這是一個(gè)fallback機(jī)制,為了防止推送機(jī)制失效導(dǎo)致配置不更新
    • 客戶端定時(shí)拉取會(huì)上報(bào)本地版本陆错,所有一般情況下灯抛,對(duì)于定時(shí)拉取的操作,服務(wù)端都會(huì)返回304-Not Modified
    • 定時(shí)頻率默認(rèn)為每5分鐘拉取一次音瓷,客戶端也可以通過在運(yùn)行時(shí)指定System Property: apollo.refreshInterval來覆蓋对嚼,單位為分鐘
    • 客戶端從Apollo配置中心服務(wù)端獲取到應(yīng)用的最新配置后,會(huì)保存在內(nèi)存中
    • 客戶端會(huì)把從服務(wù)端獲取到的配置在本地文件系統(tǒng)緩存一份绳慎,在遇到服務(wù)不可以或網(wǎng)絡(luò)不通時(shí)纵竖,依然能從本地恢復(fù)配置
    • 應(yīng)用程序可以從Apollo客戶端獲取最新的配置漠烧、訂閱配置更新通知

Apollo使用指南

名稱解析

  • 普通應(yīng)用 獨(dú)立運(yùn)行的程序,如Web應(yīng)用程序磨确、帶有main函數(shù)的程序
  • 公共組件 指發(fā)布的類庫沽甥、客戶端程序,不會(huì)自己獨(dú)立運(yùn)行乏奥,如Java的jar包

普通應(yīng)用接入指南

  • 創(chuàng)建項(xiàng)目

    • 1.進(jìn)入apollo-portal主頁
    • 2.點(diǎn)擊"創(chuàng)建項(xiàng)目"
    • 3.輸入項(xiàng)目信息
      部門 選擇應(yīng)用所在的部門
      應(yīng)用AppId 用來表示應(yīng)用身份的唯一id摆舟,格式為string,需要和客戶端app.properties中配置的app.id對(duì)應(yīng)
      應(yīng)用名稱 應(yīng)用名邓了,僅用于界面展示
      應(yīng)用負(fù)責(zé)人 選擇的人默認(rèn)會(huì)成為該項(xiàng)目的管理員恨诱,具備項(xiàng)目權(quán)限管理、集群創(chuàng)建骗炉、Namespace創(chuàng)建等權(quán)限
    • 4.點(diǎn)擊提交 創(chuàng)建成功后默認(rèn)會(huì)跳轉(zhuǎn)到照宝,項(xiàng)目首頁
  • 項(xiàng)目權(quán)限分配
    項(xiàng)目管理員權(quán)限 項(xiàng)目管理員可以管理項(xiàng)目的權(quán)限分配、可以創(chuàng)建集群句葵、可以創(chuàng)建Namespace

  • 配置編輯發(fā)布權(quán)限
    編輯權(quán)限允許用戶在Apollo界面上創(chuàng)建厕鹃、修改、刪除配置
    發(fā)布權(quán)限允許用戶在Apollo界面上發(fā)布乍丈、回滾配置

  • 添加配置項(xiàng) 編輯配置需要擁有這個(gè)Namespace的編輯權(quán)限剂碴,如果發(fā)現(xiàn)沒有新增配置按鈕,可以找項(xiàng)目管理員授權(quán)

  • 發(fā)布配置 配置只有在發(fā)布后才會(huì)真的被應(yīng)用使用到轻专,所以在編輯完配置后忆矛,需要發(fā)布配置。發(fā)布配置需要擁有這個(gè)Namespace的發(fā)布權(quán)限请垛,如果發(fā)現(xiàn)沒有發(fā)布按鈕催训,可以找項(xiàng)目管理員授權(quán)

  • 應(yīng)用讀取配置 配置發(fā)布成功后就可以通過Apollo客戶端讀取到配置了。Apollo目前提供Java客戶端宗收,如果使用其他語言漫拭,也可以通過直接訪問Http接口獲取配置

  • 回滾已發(fā)布配置 如果發(fā)現(xiàn)已發(fā)布的配置有問題,可以通過點(diǎn)擊"回滾"按鈕來將客戶端讀取到的配置回滾到上一個(gè)發(fā)布版本混稽。這里的回滾機(jī)制類似于發(fā)布系統(tǒng)嫂侍,發(fā)布系統(tǒng)中的回滾操作是將部署到機(jī)器上的安裝包回滾到上一個(gè)部署的版本,但代碼倉庫中的代碼是不會(huì)回滾的荚坞,從而開發(fā)可以在修復(fù)代碼后重新發(fā)布挑宠。Apollo中的回滾也是類似的機(jī)制,點(diǎn)擊回滾后是將發(fā)布到客戶端的配置回滾到上一個(gè)已發(fā)布版本颓影,也就是說客戶端讀取到的配置會(huì)恢復(fù)到上一個(gè)版本各淀,但頁面上編輯狀態(tài)的配置是不會(huì)回滾的,從而開發(fā)可以在修復(fù)配置后重新發(fā)布

公共組件接入步驟
公共組件接入步驟幾乎與普通應(yīng)用接入一致诡挂,唯一的區(qū)別是公共組件需要建立自己的唯一Namespace

    1.創(chuàng)建項(xiàng)目
    2.項(xiàng)目管理員權(quán)限
    3.創(chuàng)建Namespace
    4.添加配置項(xiàng)
    5.發(fā)布配置
    6.應(yīng)用讀取配置

應(yīng)用覆蓋公共組件配置步驟

1.關(guān)聯(lián)公共組件Namespace
2.覆蓋公共組件配置
3.發(fā)布配置

多個(gè)AppId共享同一份配置
在一些情況下碎浇,盡管應(yīng)用本身不是公共組件临谱,但還是需要在多個(gè)AppId之間共用同一份配置,這種情況下如果希望實(shí)現(xiàn)多個(gè)AppId使用同一份配置的話奴璃,基本概念和公共組件的配置是一致的悉默。具體來說,就是在其中一個(gè)AppId下創(chuàng)建一個(gè)namespace苟穆,寫入公共的配置信息抄课,然后在各個(gè)項(xiàng)目中讀取該namespace的配置即可;如果某個(gè)AppId需要覆蓋公共的配置信息雳旅,那么在該AppId下關(guān)聯(lián)公共的namespace并寫入需要覆蓋的配置即可

應(yīng)用接入策略
這里考慮非Java語言客戶端接入--直接通過Http接口獲取配置

  • 通過帶緩存的HTTP接口從Apollo讀取配置
    該接口會(huì)從緩存中獲取配置跟磨,適合頻率較高的配置拉取請(qǐng)求,如簡(jiǎn)單的30秒輪詢一次配置攒盈。由于緩存最多會(huì)有一秒的延遲抵拘,所以如果需要配合配置推送通知實(shí)現(xiàn)實(shí)時(shí)更新配置的話,請(qǐng)參考不帶緩存的HTTP接口從Apollo讀取配置

    HTTP接口說明

    URL {config_server_url}/configfiles/json/{appId}/{clusterName}/{namespaceName}?ip={clientIp}

    Method GET

    **參數(shù)說明 **


    file
      **HTTP接口返回格式**  該HTTP接口返回的是JSON格式型豁、UTF-8編碼僵蛛,包含了對(duì)應(yīng)namespace中所有的配置項(xiàng)。返回內(nèi)容Sample如下
      {
      "portal.elastic.document.type":"biz",
      "portal.elastic.cluster.name":"hermes-es-fws"
      }
      *TIPS 通過{configserverurl}/configfiles/{appId}/{clusterName}/{namespaceName}?ip={clientIp}可以獲取到properties形式的配置*
    
  • 不帶緩存的HTTP接口從Apollo讀取配置
    該接口會(huì)直接從數(shù)據(jù)庫中獲取配置迎变,可以配合配置推送通知實(shí)現(xiàn)實(shí)時(shí)更新配置

    URL {config_server_url}/configs/{appId}/{clusterName}/{namespaceName}?releaseKey={releaseKey}&ip={clientIp}

    Method GET

    參數(shù)說明

    file

該HTTP接口返回的是JSON格式充尉、UTF-8編碼。如果配置沒有變化(傳入的releaseKey和服務(wù)端的相等)氏豌,則返回HttpStatus 304喉酌,Response Body為空热凹;如果配置有變化泵喘,則會(huì)返回HttpStatus 200,Response Body為對(duì)應(yīng)namespace的meta信息以及其中所有的配置項(xiàng)般妙。返回內(nèi)容Sample如下

        {
        "appId": "100004458",
        "cluster": "default",
        "namespaceName": "application",
        "configurations": {
        "portal.elastic.document.type":"biz",
        "portal.elastic.cluster.name":"hermes-es-fws"
        },
        "releaseKey": "20170430092936-dee2d58e74515ff3"
        }
  • 應(yīng)用感知配置更新
    Apollo提供了基于Http long polling的配置更新推送通知纪铺,第三方客戶端可以看自己實(shí)際的需求決定是否需要使用這個(gè)功能。如果對(duì)配置更新時(shí)間不是那么敏感的話碟渺,可以通過定時(shí)刷新來感知配置更新鲜锚,刷新頻率可以視應(yīng)用自身情況來定,建議在30秒以上苫拍。如果需要做到實(shí)時(shí)感知配置更新(1秒)的話芜繁,可以參考下面的文檔實(shí)現(xiàn)配置更新推送的功能

    配置更新推送實(shí)現(xiàn)思路 建議參考Apollo的Java實(shí)現(xiàn)RemoteConfigLongPollService.java

初始化 首先需要確定哪些namespace需要配置更新推送,Apollo的實(shí)現(xiàn)方式是程序第一次獲取某個(gè)namespace的配置時(shí)就會(huì)來注冊(cè)一下绒极,我們就知道有哪些namespace需要配置更新推送了骏令。初始化后的結(jié)果就是得到一個(gè)notifications的Map,內(nèi)容是namespaceName -> notificationId(初始值為-1)垄提。運(yùn)行過程中如果發(fā)現(xiàn)有新的namespace需要配置更新推送榔袋,直接塞到notifications這個(gè)Map里面即可

請(qǐng)求服務(wù) 有了notifications這個(gè)Map之后周拐,就可以請(qǐng)求服務(wù)了。這里先描述一下請(qǐng)求服務(wù)的邏輯凰兑,具體的URL參數(shù)和說明請(qǐng)參見后面的接口說明

    1.請(qǐng)求遠(yuǎn)端服務(wù)妥粟,帶上自己的應(yīng)用信息以及notifications信息

    2.服務(wù)端針對(duì)傳過來的每一個(gè)namespace和對(duì)應(yīng)的notificationId,檢查notificationId是否是最新的

    3.如果都是最新的吏够,則保持住請(qǐng)求60秒勾给,如果60秒內(nèi)沒有配置變化,則返回HttpStatus 304稿饰。如果60秒內(nèi)有配置變化锦秒,則返回對(duì)應(yīng)namespace的最新notificationId, HttpStatus 200

    4.如果傳過來的notifications信息中發(fā)現(xiàn)有notificationId比服務(wù)端老,則直接返回對(duì)應(yīng)namespace的最新notificationId, HttpStatus 200

    5.客戶端拿到服務(wù)端返回后喉镰,判斷返回的HttpStatus

    6.如果返回的HttpStatus是304旅择,說明配置沒有變化,重新執(zhí)行第1步

    7.如果返回的HttpStauts是200侣姆,說明配置有變化生真,針對(duì)變化的namespace重新去服務(wù)端拉取配置,參見1.3 通過不帶緩存的Http接口從Apollo讀取配置捺宗。同時(shí)更新notifications map中的notificationId柱蟀。重新執(zhí)行第1步

HTTP接口說明

URL {config_server_url}/notifications/v2?appId={appId}&cluster={clusterName}&notifications={notifications}

Method GET

參數(shù)說明
file

TIPS 由于服務(wù)端會(huì)hold住60秒,所以請(qǐng)確毖晾鳎客戶端訪問服務(wù)端的超時(shí)時(shí)間要大于60秒长已;記得對(duì)參數(shù)進(jìn)行URL Encode

HTTP返回格式  該Http接口返回的是JSON格式、UTF-8編碼昼牛,包含了有變化的namespace和最新的notificationId术瓮。返回內(nèi)容Sample如下
    [{
        "namespaceName": "application",
        "notificationId": 101
    }]

分布式部署指南

官方展示的部署策略,生產(chǎn)環(huán)境部署一套Apollo-Portal+ApolloPortalDB贰健,其他環(huán)境(PRO胞四、UAT、FAT伶椿、DEV)單獨(dú)部署MetaServer+AdminService+ConfigService辜伟,使用獨(dú)立數(shù)據(jù)庫ApolloConfigDB及應(yīng)用服務(wù);MetaServer和Config Service部署在同一個(gè)JVM進(jìn)程內(nèi)脊另,Admin Service部署在同一臺(tái)服務(wù)器的另一個(gè)JVM進(jìn)程內(nèi)导狡。部署示例如下圖


file

網(wǎng)絡(luò)策略 分布式部署的時(shí)候,apollo-configservice和apollo-adminservice需要把自己的IP和端口注冊(cè)到Meta Server(apollo-configservice本身)偎痛。Apollo客戶端和Portal會(huì)從Meta Server獲取服務(wù)的地址(IP+PORT)旱捧,然后通過服務(wù)地址直接訪問。apollo-configservice和apollo-adminservice是基于內(nèi)網(wǎng)可信網(wǎng)絡(luò)設(shè)計(jì)的看彼,所以出于安全考慮廊佩,請(qǐng)不要將apollo-configservice和apollo-adminservice直接暴露在公網(wǎng)

部署步驟
    創(chuàng)建數(shù)據(jù)庫  Apollo服務(wù)端依賴于MYSQL數(shù)據(jù)庫囚聚,所以需要事先創(chuàng)建并完成初始化
    獲取安裝包  Apollo服務(wù)端安裝包共3個(gè): Apollo-AdminService、Apollo-ConfigService标锄、Apollo-Portal
    部署Apollo服務(wù)端  獲取安裝包后就可以部署到測(cè)試和生產(chǎn)環(huán)境

小結(jié)

文章較為全面介紹開源分布式配置中心Apollo的設(shè)計(jì)顽铸、使用、應(yīng)用接入及部署方法料皇,目前客戶端只有Java和.Net版本谓松,其他語言客戶端的接入可以通過HTTP接口的方式定時(shí)拉取更新配置或通過Http Long Polling機(jī)制實(shí)時(shí)推送挟裂,實(shí)現(xiàn)應(yīng)用感知配置更新

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末募逞,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子腕够,更是在濱河造成了極大的恐慌逊脯,老刑警劉巖优质,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異军洼,居然都是意外死亡巩螃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門匕争,熙熙樓的掌柜王于貴愁眉苦臉地迎上來避乏,“玉大人,你說我怎么就攤上這事甘桑∨钠ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵跑杭,是天一觀的道長(zhǎng)铆帽。 經(jīng)常有香客問我,道長(zhǎng)艘蹋,這世上最難降的妖魔是什么锄贼? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任票灰,我火速辦了婚禮女阀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屑迂。我一直安慰自己浸策,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布惹盼。 她就那樣靜靜地躺著庸汗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪手报。 梳的紋絲不亂的頭發(fā)上蚯舱,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天改化,我揣著相機(jī)與錄音,去河邊找鬼枉昏。 笑死陈肛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的兄裂。 我是一名探鬼主播句旱,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼晰奖!你這毒婦竟也來了谈撒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤匾南,失蹤者是張志新(化名)和其女友劉穎啃匿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛆楞,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡立宜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了臊岸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片橙数。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖帅戒,靈堂內(nèi)的尸體忽然破棺而出灯帮,到底是詐尸還是另有隱情,我是刑警寧澤逻住,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布钟哥,位于F島的核電站,受9級(jí)特大地震影響瞎访,放射性物質(zhì)發(fā)生泄漏腻贰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一扒秸、第九天 我趴在偏房一處隱蔽的房頂上張望播演。 院中可真熱鬧,春花似錦伴奥、人聲如沸写烤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽洲炊。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間暂衡,已是汗流浹背询微。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狂巢,地道東北人拓提。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像隧膘,于是被迫代替她去往敵國(guó)和親代态。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 在Spring Boot 2.0 整合攜程Apollo配置中心一文中疹吃,我們?cè)诒镜乜焖俨渴鹪囉昧薃pollo蹦疑。本文將...
    AaronSimon閱讀 17,599評(píng)論 1 24
  • Apollo介紹 Apollo(阿波羅)是攜程框架部門研發(fā)的開源配置管理中心,能夠集中化管理應(yīng)用不同環(huán)境萨驶、不同集群...
    dreamguys閱讀 126,411評(píng)論 7 37
  • Apollo(阿波羅)是攜程框架部門研發(fā)的分布式配置中心歉摧,能夠集中化管理應(yīng)用不同環(huán)境、不同集群的配置腔呜,配置修改后能...
    BeckJin閱讀 3,739評(píng)論 0 1
  • 看到 InfoQ 的一篇文章:GitHub 9K Star叁温!Apollo作者手把手教你微服務(wù)配置中心之道 關(guān)于為什...
    專職跑龍?zhí)?/span>閱讀 3,613評(píng)論 0 17
  • 三十二年回隴縣, 滿頭白發(fā)花甲年核畴, 部隊(duì)生活三年多膝但, 歷歷浮現(xiàn)在眼前。 當(dāng)年胸懷國(guó)防夢(mèng)谤草, 軍校畢業(yè)到隴州跟束, 部隊(duì)熔...
    精忠報(bào)國(guó)_fdf3閱讀 331評(píng)論 0 0