[享學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來給與支援:DynamicContextualPropertyDeploymentContext,接下來由淺入深,一步步了解其深意。


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

該類邏輯無非在父類基礎上增加了對ConfigurationSystem的尋找,因此若我們想要設置部署參數,是可以通過兩者來做的,推薦你使用Configuration

另外,ConfigurationBasedDeploymentContextArchaius默認使用的部署上下文實現,具體程式碼參考: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實現能總結出如下匹配邏輯:

  1. input輸入Map<String, Collection<String>>是個組合邏輯,每個entry都為true最終才為true
  2. 針對input的每個entry,key經過Function處理後的值必須被value所包含才算此entry為true
  3. 由以上兩步可知,決定此匹配邏輯的核心要素是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());      }    	...  }

以上都是初始化階段完成的動作:

  1. 讀出配置文件的值(它是個JSON串),然後把它反序列化為List<Value<T>> values放著
    1. 這個values裡面就存著實際value值,以及這個value值生效對應的條件Map<String, Collection<String>> dimensions
  2. 重寫propertyChanged()方法,所以每當屬性變化時,便可重新給 List<Value<T>> values賦值
  3. 準備一個判斷邏輯,默認使用的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()獲取步驟做如下描述:

  1. 若values為null(也就是JSON串為null,或者沒此key),那就使用默認值嘍
  2. 遍歷values裡面所有的條件,返回首個滿足條件的實際值
    1. 也就是說若有多組滿足條件,那麼誰在上面就誰的優先順序高唄(一般請避免此種 情況,條件盡量互斥哈)
  3. 有個小細節:滿足條件的case有如下兩種:
    1. 沒有條件(為null或者empty),那就是滿足條件(一般作為兜底默認值方案)
    2. 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提供了非常靈活的支援,祝你輕鬆上雲、安全上雲。