[享学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提供了非常灵活的支持,祝你轻松上云、安全上云。