在微服务的架构下,架构的复杂度以及服务的数量都会比之前单体应用复杂很多,配置的集中管理,以及模块化管理是非常有必要的。
服务对于配置的依赖程度也非常高,配置修改后的实时生效、灰度发布、环境的区分等等。
我们的服务部署在k8s上,打包的方式基于docker所以一次构建所有环境部署这一点是毋庸置疑的.配置外置使容器达到真正的无状态。
在面对这样的大环境,普通的的配置文件管理已经无法满足我们的需求了,所以需要寻找一个解决方案.最终我们选择了使用Apollo来帮助我们完成上述的事情。
接下来就不对Apollo配置中心进行过多的介绍了,有兴趣的小伙伴可以通过官网自行了解。
在我们的平台中所有的服务都是基于Kubernetes(一下简称k8s)进行部署的,所以Apollo配置中心也不例外,我们将admin、config、portal分别打成镜像部署到了k8s上。
上图简要描述了Apollo的总体设计,我们可以从下往上看:
Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳在Eureka之上我们架了一层Meta Server用于封装Eureka的服务发现接口Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Client侧会做load balance、错误重试Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中官方文档提供了k8s部署解决方案,但是其操作过于繁琐,而且定义了大量的yaml文件,这些文件可以给我们提供参考价值,但是再生产过程中还是不要拿来直接使用,最好是使用我们自己构建的镜像.
打开*scripts/build.sh*文件并将数据库以及meta替换成我们指定的mysql以及service-name; # config的数据库配置 apollo_config_db_url=jdbc:mysql://fill-in-the-correct-server:3306/ApolloConfigDB?characterEncoding=utf8 apollo_config_db_username=FillInCorrectUser apollo_config_db_password=FillInCorrectPassword # protal的数据库配置 apollo_portal_db_url=jdbc:mysql://fill-in-the-correct-server:3306/ApolloPortalDB?characterEncoding=utf8 apollo_portal_db_username=FillInCorrectUser apollo_portal_db_password=FillInCorrectPassword # 指定config 的 service-name dev_meta=http://apollo-config:8080执行build.sh文件完成打包;
将apollo-config、apollo-admin、apollo-portal打包成docker镜像;
在对应目录分别执行 mvn docker:build;将镜像推送到自己的docker私仓准备使用;上文已经指出为了简化部署Config Service,Eureka以及Meta Server三个逻辑角色都被放在了apollo-config这一个项目里了,所以使用Client连接Meta Server的时候应该注意实际连接的是apolo-config的service,同理Protal想要连接apollo-admin的时候也需要经过Meta Server去获取admin的地址和端口列表.
配置build.sh时需要注意指定Meta Server的service-name
apollo-admin-7fcd446cf5-hlt7x 1/1 Running 0 20d apollo-config-d46ccdf65-9nr5c 1/1 Running 0 20d apollo-portal-7788fd9f8d-8rgw7 1/1 Running 0 20dApollo支持4个维度去管理Key-Value格式的配置:
application(应用)evironment(环境)cluster(集群)namespace(命名空间)在这四个维度中,前三个维度都是逐层进行区分的,第四个维度是并列的关系.在使用的时候application与evironment都是必须指定的,client会根据指定的信息拉取对应的配置。
基于云原生构建部署的服务会存在多集群多机房的情况,将配置对外暴露可以让我们的服务真正的做到一次构建,多环境部署,不同环境的区分仅通过配置文件就可以实现.
Apollo对应用的划分规则整好符合我们的需求:
application->服务镜像
evironment->部署环境
cluster->多集群的选择
在apollo中对于这三个状态的选择有多种的实现方式,具体就不在这里一一例举了,我们的实现方式是通过JVM参数传入来选择应用所需要的配置
在上文中也提到在我们所有的服务都是基于k8s进行部署的,所以我们线上与预生产放在一个portal中,开发以及测试在其他的集群中.
集群的选择通过k8s的yaml文件进行控制,将JVM参数配置成环境变量传递给Dockerfile
每个应用的配置是由命名空间构成的,在默认的情况下每个应用都会有一个application命名空间,这个命名空间默认是私有的.在应用中还可以创建公共的命名空间提供给其他应用进行关联,在构建微服务时会有连接大量的中间件,在相同的环境下,这些中间件的连接方式基本类似,有的甚至是完全一样.
在这种情况下可以定义一个template将一些基本的配置信息统一定义然后其他的应用去关联这些配置,在使用的时候只对发生变化的内容进行修改即可.
我们在使用Apollo的时候将应用所有的配置文件都保存在Apollo中,但是有的时候为了方便测试本地的配置文件还是会保留大量的配置,这些配置再发布上线的时候会对线上Apollo的配置带来影响么?
答案是不会的,这是由Apollo加载配顺序所导致的,通过观察源码我们可以发现,对于Spring托管的项目而言Apollo会将新获取到的配置文件放在集合的最前面。
@Override public void initialize(ConfigurableApplicationContext context) { //获取已经传入的配置 ConfigurableEnvironment environment = context.getEnvironment(); //初始化Apollo的系统参数,就是我们熟知的'app.id'等 initializeSystemProperty(environment); //解析配置文件获取namespaces String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION); logger.debug("Apollo bootstrap namespaces: {}", namespaces); List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces); ...... //创建一个容器来接收Apollo上的配置 CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME); ...... //这里讲解析的配置文件添加到First(这里说明下,由于Spring在解析配置文件的时候是前面的覆盖后见面的,所以为了让Apollo的配置文件优先级高于本地的配置文件,这里放在最前面) environment.getPropertySources().addFirst(composite); }继续查看PropertySources的getproperties方法可以发现在获取配置的时会按顺序遍历集合一旦获取到对应的配置则会跳出循环
//顺序遍历集合并根据key查找配置 protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) { if (this.propertySources != null) { for (PropertySource<?> propertySource : this.propertySources) { if (logger.isTraceEnabled()) { logger.trace("Searching for key '" + key + "' in PropertySource '" + propertySource.getName() + "'"); } //查找配置 Object value = propertySource.getProperty(key); //找到配置跳出循环 if (value != null) { if (resolveNestedPlaceholders && value instanceof String) { value = resolveNestedPlaceholders((String) value); } logKeyFound(key, propertySource, value); return convertValueIfNecessary(value, targetValueType); } } } if (logger.isDebugEnabled()) { logger.debug("Could not find key '" + key + "' in any property source"); } return null; }在Apollo-Client内部会维护一个“长连接”,这是一个Long Polling(长轮询),通过长轮询来保证配置更新的实时性。
客户端发起一个Http请求到服务端服务端会保持住这个连接60秒 如果在60秒内有客户端关心的配置变化,被保持住的客户端请求会立即返回,并告知客户端有配置变化的namespace信息,客户端会据此拉取对应namespace的最新配置如果在60秒内没有客户端关心的配置变化,那么会返回Http状态码304给客户端 客户端在收到服务端请求后会立即重新发起连接,回到第一步在PaaS项目中我们的动态路由就是通过开放平台API以及热部署实现的,操作流程如下图所示:
在监听到配置变更事件之后调用RouteDefinitionRepository所提供的方法对路由信息进行更改。
@ApolloConfigChangeListener(interestedKeyPrefixes = "spring.cloud.gateway.") public void onChange(ConfigChangeEvent changeEvent) { //清理被覆盖的路由信息 preDestroyGatewayProperties(changeEvent); //刷新配置 this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); //更新Gateway路由列表 refreshGatewayRouteDefinition();本文开篇讲述了我们为什么选择Apollo来作为配置管理中心,接着对Apollo的作用进行了简要的分析后开始介绍Apollo在PaaS中的应用:
k8s的部署方式;多环境的区分;配置属性的模板化以及关联引用;Spring中的配置管理方式;运行时监听配置变化。实际上Apollo还有很多特性等待着我们去开发:比如使用namespace结合Spring的Start后者是ConfigurationProperties对配置进行模板化,使开发人员在引入新的中间件或者是三方服务使只需要引入一个namespace以及dependency就可以直接开始工作。
由于篇幅的限制介绍的内容不是很直观还请见谅,如果有疑问非常欢迎在品论区进行交流.