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