[享學Netflix] 十四、Archaius如何對多環境、多區域、多雲部署提供配置支援?
- 2020 年 3 月 18 日
- 筆記
如果你想擁有不平凡的人生,那就請拿出不平凡的努力 程式碼下載地址:https://github.com/f641385712/netflix-learning
目錄
前言
在當下日益複雜的互聯網雲環境中,對應用APP的靈活部署要求越來越高:同樣的一份程式碼在不同環境、不同區域…需要有不同表現(邏輯不同、性能不同…),同時高可用方面的多機房、多雲災備亦體現出了部署的複雜性。
通過前幾篇文章關於Netflix Archaius的學習,相信你已經完全掌握了它是如何處理組合配置、如何讓屬性動態化的。在它的屬性抽象com.netflix.config.Property中,有一個非常重要的子類DynamicContextualProperty –> 根據上下文不同,屬性值也不一樣,它便是今天本文的主要內容。
DynamicContextualProperty里的Contextual便是它的核心:上下文是個泛概念,它可以包括環境、區域、數據中心等等,但卻又不限於此。它是Netflix Archaius拿來應對多環境部署、複雜環境獲取不同屬性值的有效工具,本文將展開對它以及部署上下文DeploymentContext的深入探討和學習。
說明:多環境配置支援 + 動態化,想起來就很激動有木有~
正文
對於多環境部署的,Archaius主要使用兩個核心API來給與支援:DynamicContextualProperty和DeploymentContext,接下來由淺入深,一步步了解其深意。
DeploymentContext
定義應用程式的部署上下文的介面。
public interface DeploymentContext { // 提出一點:命名不規範,應用全大寫 // 這裡幾乎覆蓋了部署所有的參數需求,如環境、數據中心、分區等等都有 // 當然還可能不夠的,下面會教你如何擴展 public enum ContextKey { environment("@environment"), datacenter("@datacenter"), appId("@appId"), serverId("@serverId"), stack("@stack"), region("@region"), zone("@zone"); ... } // ========介面方法========= // For example "test", "dev", "prod"。當然嘍,任意字元串都行 public String getDeploymentEnvironment(); public void setDeploymentEnvironment(String env); // 數據中心 public String getDeploymentDatacenter(); public void setDeploymentDatacenter(String deployedAt); // 對應@appId public String getApplicationId(); public void setApplicationId(String appId); // 對應:@serverId。注意和@appId的區別。 // 一般來說 一個APP都是多實例部署嘛 public void setDeploymentServerId(String serverId); public String getDeploymentServerId(); // 部署此應用程式的堆棧名稱。 // 堆棧的名字可以用來影響應用程式的行為。 public void setDeploymentStack(String stack); public String getDeploymentStack(); // 分區:如東北區、華南區、北美區等等 us-east-1/us-west-1... public String getDeploymentRegion(); public void setDeploymentRegion(String region); // ========通用方法====== // 其實你會發現,還有@zone沒有給特定的方法,所以這裡給了個通用方法 // 這樣是為了避免後續枚舉增加值,弄得不向下兼容了,所以給出一個通用方法嘍 public String getValue(ContextKey key); public void setValue(ContextKey key, String value); }
注意:所有屬性是可選的,如果未設置,可能返回null。
該介面看著方法很多,其實就一句話:內部一個Map,key是ContextKey類型,value是String類型,來表示部署上下文參數。它的直接實現類是:SimpleDeploymentContext。
SimpleDeploymentContext
太簡單了,內部維護一個map(執行緒安全的Map)管理者上下文屬性們。
public class SimpleDeploymentContext implements DeploymentContext { private Map<DeploymentContext.ContextKey, String> map = new ConcurrentHashMap<>(); @Override public String getDeploymentEnvironment() { return map.get(ContextKey.environment); } @Override public void setDeploymentEnvironment(String env) { map.put(ContextKey.environment, env); } ... // 其它方法類似,略 @Override public String getValue(ContextKey key) { return map.get(key); } @Override public void setValue(ContextKey key, String value) { map.put(key, value); } }
它是使用Map維護,是一種通用實現,但是卻還沒有和Configuration掛上鉤。它有個子類ConfigurationBasedDeploymentContext,便是和Configuration有關嘍。
ConfigurationBasedDeploymentContext
從名字就能看出來,它是基於Configuration / ConfigurationManager來實現的。
public class ConfigurationBasedDeploymentContext extends SimpleDeploymentContext { // 這些屬性可以作為key,放在Configuration里,或者系統屬性里均可 // 但是,但是:都標記為過期了,不建議這麼做了 // 現在推薦使用枚舉值來管理 @Deprecated public static final String DEPLOYMENT_ENVIRONMENT_PROPERTY = "archaius.deployment.environment"; @Deprecated public static final String DEPLOYMENT_DATACENTER_PROPERTY = "archaius.deployment.datacenter"; ... @Deprecated public static final String DEPLOYMENT_REGION_PROPERTY = "archaius.deployment.region"; // 唯一構造器 public ConfigurationBasedDeploymentContext() { AbstractConfiguration config = ConfigurationManager.getConfigInstance(); // 只有defaultConfigDisabled = true不允許默認初始化邏輯(只允許自定義) // 但是你又沒自定義的時候,Confiuration是可能為null的 if (config != null) { String contextValue = getValueFromConfig(DEPLOYMENT_APPLICATION_ID_PROPERTY); if (contextValue != null) { // 會放置兩份,key分別為@appId和DEPLOYMENT_APPLICATION_ID_PROPERTY setApplicationId(contextValue); } ... // 把所有的key都放進來(均會放置兩份) } // 添加一個Configuration的監聽器ConfigurationListener config.addConfigurationListener(new ConfigurationListener() { // 只監聽成功後的。EVENT_ADD_PROPERTY和EVENT_SET_PROPERTY兩種事件 // 修改還有一種是clear,此處是沒有監聽的 @Override public void configurationChanged(ConfigurationEvent event) { if (event.isBeforeUpdate() || (event.getType() != AbstractConfiguration.EVENT_ADD_PROPERTY && event.getType() != AbstractConfiguration.EVENT_SET_PROPERTY)) { return; } // value不為null,才需要技術處理 String name = event.getPropertyName(); String value = event.getPropertyValue() == null ? null : String.valueOf(event.getPropertyValue()); if (value == null) { return; } // 把改變之後的值,也設置進去 if (name.equals(DEPLOYMENT_ENVIRONMENT_PROPERTY)) { ConfigurationBasedDeploymentContext.super.setDeploymentRegion(value); setValueInConfig(ContextKey.environment.getKey(), value); } { ... } } }); } ... // 獲取方法也做了增強 // 1、去Configuration或者System里找key為@environment的值 // 2、1若為null。繼續同樣地方找key為DEPLOYMENT_ENVIRONMENT_PROPERTY的 // 3、2若為null。super.getDeploymentEnvironment(),再去當前Map里早 @Override public String getDeploymentEnvironment() { String value = getValueFromConfig(DeploymentContext.ContextKey.environment.getKey()); if (value != null) { return value; } else { value = getValueFromConfig(DEPLOYMENT_ENVIRONMENT_PROPERTY); if (value != null) { return value; } else { return super.getDeploymentEnvironment(); } } } ... }
該類邏輯無非在父類基礎上增加了對Configuration和System的尋找,因此若我們想要設置部署參數,是可以通過兩者來做的,推薦你使用Configuration。
另外,ConfigurationBasedDeploymentContext是Archaius默認使用的部署上下文實現,具體程式碼參考:ConfigurationManager的static程式碼塊部分。
DynamicContextualProperty
它繼承自PropertyWrapper<T>,相較於其它子類來說,它是一個功能強大,理解難度頗高的一個使用類,也是和本文主題:複雜部署相關的API。
它具有多個可能的關聯值,並根據運行時上下文確定該值,其中可以包括部署上下文、其他屬性的值或用戶輸入的屬性,它的Value值用一個JSON表示。
說明:它強依賴於Jackson模組完成操作。若你還不太懂Jackson如何使用,請務必參閱全網最全、最好的Jackson專欄,電梯直達:Jackson專欄
DefaultContextualPredicate
在實地介紹DynamicContextualProperty之前,先看看DefaultContextualPredicate的實現,它代表一種Predicate斷言邏輯的實現,能告訴你什麼叫匹配,什麼叫不匹配。
// 這個泛型類型很複雜哦~~~~ public class DefaultContextualPredicate implements Predicate<Map<String, Collection<String>>> { // 這個欄位命名很奇特:它是一個Function,輸入的是一個值,輸出的被轉為另一個值 private final Function<String, String> getValueFromKeyFunction; public DefaultContextualPredicate(Function<String, String> getValueFromKeyFunction) { this.getValueFromKeyFunction = getValueFromKeyFunction; } // 斷言邏輯 @Override public boolean apply(@Nullable Map<String, Collection<String>> input) { if (null == input) { throw new NullPointerException(); } // 遍歷input的每個entry,必須每一個都是true,最終才返回true for (Map.Entry<String, Collection<String>> entry: input.entrySet()) { String key = entry.getKey(); Collection<String> value = entry.getValue(); // 也就是說key經過Function處理後,得到的value值必須被原value包含才行 // 比如key處理好後值是"a",而原來的value值是["a","b"],那麼就算匹配成功嘍 if (!value.contains(getValueFromKeyFunction.apply(key))) { return false; } } // 必須每一個都是true,最終才返回true return true; } }
從該Predicate實現能總結出如下匹配邏輯:
- input輸入
Map<String, Collection<String>>是個組合邏輯,每個entry都為true最終才為true - 針對input的每個entry,
key經過Function處理後的值必須被value所包含才算此entry為true - 由以上兩步可知,決定此匹配邏輯的核心要素是Function的實現,它由構造器構造的時候必須指定
使用示例
@Test public void fun1() { // Function解釋:過來的name一定叫"YourBatman",age一定是"18"歲 Predicate<Map<String, Collection<String>>> predicate = new DefaultContextualPredicate(key -> { if(key.equals("name")) return "YourBatman"; if(key.equals("age")) return "18"; return null; }); // 輸入:名稱必須是這三個中的一個,而年齡必須是16到20歲之間 Map<String, Collection<String>> input = new HashMap<>(); input.put("name", Arrays.asList("Peter","YourBatman","Tiger")); input.put("age", Arrays.asList("16","17","18","19","20")); System.out.println(predicate.test(input)); // 輸入:名字必須精確的叫Peter input = new HashMap<>(); input.put("name", Arrays.asList("Peter")); input.put("age", Arrays.asList("16","17","18","19","20")); System.out.println(predicate.test(input)); }
運行程式,第一個輸出為true,因為條件都符合。第二個false,因為名字不符合。
內置實現
為了方便使用,Archaius內置了一個深度整合Configuration實現,拿去直接用便可:
DefaultContextualPredicate: public static final DefaultContextualPredicate PROPERTY_BASED = new DefaultContextualPredicate(new Function<String, String>() { @Override public String apply(@Nullable String input) { return DynamicProperty.getInstance(input).getString(); } });
簡單解釋PROPERTY_BASED:key處理後輸出什麼,由Configuration配置來決定,這樣就完美和Configuration集成在了一起。
源碼分析
// T表示最終getValue返回的實際類型,這裡扔不確定,所以可以是任何值 // 因為上下文不同,所以有可能返回任何值 // 它繼承自PropertyWrapper,所以它的屬性值也是具有動態性的哦~~~~~~~ public class DynamicContextualProperty<T> extends PropertyWrapper<T> { // 對value的包裝,該類屬性代表著上下文條件、匹配規則 public static class Value<T> { // 條件們 匹配規則 private Map<String, Collection<String>> dimensions; // 最後實際返回的值,是T類型。可以是任意類型,如String、int,設置可以是POJO private T value; // 注釋 private String comment; private boolean runtimeEval = false; // 請注意:這裡是if哦~~~ @JsonProperty("if") public final Map<String, Collection<String>> getDimensions() { return dimensions; } ... // 省略其它get/set方法 } // 判斷邏輯你可以自定義:比如你可以自定義為只需要有一個為true就為true // 但默認情況下使用的就是PROPERTY_BASED嘍 private final Predicate<Map<String, Collection<String>>> predicate; private static Predicate<Map<String, Collection<String>>> defaultPredicate = DefaultContextualPredicate.PROPERTY_BASED; // JSON字元串,會被返解析為它~~~~ // 這是一個複雜的str -> POJO的反序列化,所以藉助Jackson的ObjectMapper來完成的 volatile List<Value<T>> values; private final ObjectMapper mapper = new ObjectMapper(); // 可以自定義判斷邏輯predicate(一般使用默認的即可) public DynamicContextualProperty(String propName, T defaultValue, Predicate<Map<String, Collection<String>>> predicate) { ... } // 使用默認的判斷邏輯(全都為true才為true,並且和Configuration集成) public DynamicContextualProperty(String propName, T defaultValue) { ...} ... // 在構造器階段:把屬性值value(是個JSON串),轉換為了List<Value<T>> values本地存儲著~~~ // 當屬性發生改變時,List<Value<T>> values也會跟著變化 @Override protected final void propertyChanged() { propertyChangedInternal(); propertyChanged(this.getValue()); } ... }
以上都是初始化階段完成的動作:
- 讀出配置文件的值(它是個JSON串),然後把它反序列化為
List<Value<T>> values放著- 這個values裡面就存著實際value值,以及這個value值生效對應的條件
Map<String, Collection<String>> dimensions哦
- 這個values裡面就存著實際value值,以及這個value值生效對應的條件
- 重寫
propertyChanged()方法,所以每當屬性變化時,便可重新給List<Value<T>> values賦值 - 準備一個判斷邏輯,默認使用的
PROPERTY_BASED:上下文環境屬性從Configuration裡面獲取到,從而進行判斷
準備好了這些能力後,下面就進入到作為一個Property的核心方法:獲取屬性值value
DynamicContextualProperty: @Override public T getValue() { if (values != null) { for (Value<T> v: values) { if (v.getDimensions() == null || v.getDimensions().isEmpty() || predicate.apply(v.getDimensions())) { // 只有條件符合,才拿出其實際值 return v.getValue(); } } } return defaultValue; }
對getValue()獲取步驟做如下描述:
- 若values為null(也就是JSON串為null,或者沒此key),那就使用默認值嘍
- 遍歷values裡面所有的條件,返回首個滿足條件的實際值
- 也就是說若有多組滿足條件,那麼誰在上面就誰的優先順序高唄(一般請避免此種 情況,條件盡量互斥哈)
- 有個小細節:滿足條件的case有如下兩種:
- 沒有條件(為null或者empty),那就是滿足條件(一般作為兜底默認值方案)
- 被
PROPERTY_BASED匹配成功
案例:阿里/騰訊 雙機房、多環境部署
首先,在config.properties「配置」好我們的條件(先用JSON美化表示,後寫進properties文件里):
JSON美化表示:
[ { "if":{ "@region":[ "ali" ], "@environment":[ "prod" ] }, "value":"YourBatman-ali-prod" }, { "if":{ "@region":[ "ten" ], "@environment":[ "test" ] }, "value":"YourBatman-ten-test" }, { "if":{ "@environment":[ "prod" ], "@myDiyParam":[ "China" ] }, "value":"YourBatman-myDiy-pro" }, { "value":"YourBatman" } ]
# 應用名稱:根據機房、環境來拼接生成 applicationName=[{"if":{"@region":["ali"],"@environment":["prod"]},"value":"YourBatman-ali-prod"},{"if":{"@region":["ten"],"@environment":["test"]},"value":"YourBatman-ten-test"},{"if":{"@environment":["prod"],"@myDiyParam":["China"]},"value":"YourBatman-myDiy-prod"},{"value":"YourBatman"}]
說明:一般情況下,
com.netflix.config.DeploymentContext.ContextKey裡面的這些key是默認支援的。此處的@myDiyParam屬性自定義變數名~~~(並不要求你一@開頭,但遵守規範是個好習慣)
1、一個條件都木有的默認值生效:
@Test public void fun2(){ DynamicPropertyFactory factory = DynamicPropertyFactory.getInstance(); DynamicContextualProperty<String> contextualProperty = factory.getContextualProperty("applicationName", "defaultName"); System.out.println(contextualProperty.getValue()); // YourBatman }
2、阿里上的生產環境:
@Test public void fun3(){ // 通過SimpleDeploymentContext手動設置部署環境參數 SimpleDeploymentContext deploymentContext = new SimpleDeploymentContext(); deploymentContext.setDeploymentRegion("ali"); deploymentContext.setDeploymentEnvironment("prod"); ConfigurationManager.setDeploymentContext(deploymentContext); DynamicContextualProperty<String> contextualProperty = new DynamicContextualProperty<>("applicationName", "defaultName"); System.out.println(contextualProperty.getValue()); // YourBatman-ali-prod }
3、騰訊上的測試環境:直接用屬性key完成
@Test public void fun4(){ // 調用一下,讓Configuration完成初始化 AbstractConfiguration configInstance = ConfigurationManager.getConfigInstance(); configInstance.addProperty(DeploymentContext.ContextKey.region.getKey(),"ten"); configInstance.addProperty(DeploymentContext.ContextKey.environment.getKey(),"test"); // 效果同上。但推薦用上者 // System.setProperty(DeploymentContext.ContextKey.region.getKey(),"ten"); // System.setProperty(DeploymentContext.ContextKey.environment.getKey(),"test"); DynamicContextualProperty<String> contextualProperty = new DynamicContextualProperty<>("applicationName", "defaultName"); System.out.println(contextualProperty.getValue()); // YourBatman-ten-test }
4、讓自定義的@myDiyParam條件生效:
@Test public void fun5() { System.setProperty(DeploymentContext.ContextKey.environment.getKey(), "prod"); System.setProperty("@myDiyParam", "China"); DynamicContextualProperty<String> contextualProperty = new DynamicContextualProperty<>("applicationName", "defaultName"); System.out.println(contextualProperty.getValue()); // YourBatman-myDiy-prod }
讓自定義的的屬性生效也是極簡單的有木有,不過據我經驗,生產環境建議你不要亂弄,用枚舉管理起來較好。
這個特性靈活性非常的強,這對於複雜的雲計算環境:多環境、多區域、多機房等等部署,非常非常有用,能夠極大的提升系統的彈性,給了架構師更多的想像空間。
總結
如題:Netflix Archaius如何支援多環境、多區域、多數據中心部署?現在你應該能給出你的答案了~
在微服務、容器化技術、雲源生越來越流行的今天,多環境部署是作為一名架構師、運維人員必備的技能,而Netflix Archaius提供了非常靈活的支援,祝你輕鬆上雲、安全上雲。


