[享学Netflix] 十四、Netflix Archaius如何对多环境、多区域、多云部署提供配置支持?

如果你想拥有不平凡的人生,那就请拿出不平凡的努力

–> 返回专栏总目录 <–
代码下载地址: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提供了非常灵活的支持,祝你轻松上云、安全上云。

分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了309 篇原创文章 · 获赞 466 · 访问量 40万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/104469838