微服务下的配置管理—Apollo

为什么需要统一管理配置

在微服务的架构下,架构的复杂度以及服务的数量都会比之前单体应用复杂很多,配置的集中管理,以及模块化管理是非常有必要的。

服务对于配置的依赖程度也非常高,配置修改后的实时生效、灰度发布、环境的区分等等。

我们的服务部署在k8s上,打包的方式基于docker所以一次构建所有环境部署这一点是毋庸置疑的.配置外置使容器达到真正的无状态。

在面对这样的大环境,普通的的配置文件管理已经无法满足我们的需求了,所以需要寻找一个解决方案.最终我们选择了使用Apollo来帮助我们完成上述的事情。

为什么选择Apollo

  • Apollo所包含的功能符合上文我所提到的需求
    • 统一管理不同环境、不同集群的配置;
    • 配置修改实时生效;
    • 发布版本管理;
    • 灰度发布;
  • 提供了权限管理、发布审核、操作审计的功能,很好地帮助我们记录配置的变化方便回滚和追踪问题;
  • 提供了良好的UI操作页面操作上手非常容易;
  • Java原生
    • Apollo使用Java+Spring全家桶进行开发技术栈与我们完全符合可以通过阅读源码去快速定位问题以及自由的定制化开发;
    • 提供了非常好的Java-Client可以快速继承到Spring框架中;
  • 提供开发平台API
    • Apollo对外提供API可以发布和修改已经发布的配置,这样我们就可以根据自己的需求对Apollo的API进行封装;
  • 部署简单
    • 配置中心作为基础服务,对可用性要求非常高,这一点其自身就提供高可用的部署方式;
    • Apollo的部署只需要依赖mysql,只需要安装Java和Mysql就可以让Apollo跑起来;
    • 提供打包脚本,方便我们自定义镜像;

接下来就不对Apollo配置中心进行过多的介绍了,有兴趣的小伙伴可以通过官网自行了解。

Apollo在PaaS中的应用

  • Apollo在k8s上的部署;
  • Apollo的配置管理;

Apollo的部署

在我们的平台中所有的服务都是基于Kubernetes(一下简称k8s)进行部署的,所以Apollo配置中心也不例外,我们将adminconfigportal分别打成镜像部署到了k8s上。

Apollo架构设计

Apollo架构设计

上图简要描述了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部署

使用k8s部署首先需要制作对应的镜像

官方文档提供了k8s部署解决方案,但是其操作过于繁琐,而且定义了大量的yaml文件,这些文件可以给我们提供参考价值,但是再生产过程中还是不要拿来直接使用,最好是使用我们自己构建的镜像.

  1. 打开*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
  1. 执行build.sh文件完成打包;

  2. apollo-configapollo-adminapollo-portal打包成docker镜像;

    1. 在对应目录分别执行 mvn docker:build;
    2. 将镜像推送到自己的docker私仓准备使用;
在k8s上完成部署

上文已经指出为了简化部署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          20d

Apollo的配置托管

Apollo支持4个维度去管理Key-Value格式的配置:

  • application(应用)
  • evironment(环境)
  • cluster(集群)
  • namespace(命名空间)

在这四个维度中,前三个维度都是逐层进行区分的,第四个维度是并列的关系.在使用的时候application与evironment都是必须指定的,client会根据指定的信息拉取对应的配置。

Apollo中多环境配置

基于云原生构建部署的服务会存在多集群多机房的情况,将配置对外暴露可以让我们的服务真正的做到一次构建,多环境部署,不同环境的区分仅通过配置文件就可以实现.

Apollo对应用的划分规则整好符合我们的需求:

application->服务镜像

evironment->部署环境

cluster->多集群的选择

在apollo中对于这三个状态的选择有多种的实现方式,具体就不在这里一一例举了,我们的实现方式是通过JVM参数传入来选择应用所需要的配置

在上文中也提到在我们所有的服务都是基于k8s进行部署的,所以我们线上与预生产放在一个portal中,开发以及测试在其他的集群中.

集群的选择通过k8s的yaml文件进行控制,将JVM参数配置成环境变量传递给Dockerfile

Apollo中配置的关联

每个应用的配置是由命名空间构成的,在默认的情况下每个应用都会有一个application命名空间,这个命名空间默认是私有的.在应用中还可以创建公共的命名空间提供给其他应用进行关联,在构建微服务时会有连接大量的中间件,在相同的环境下,这些中间件的连接方式基本类似,有的甚至是完全一样.

在这种情况下可以定义一个template将一些基本的配置信息统一定义然后其他的应用去关联这些配置,在使用的时候只对发生变化的内容进行修改即可.

Apollo在Spring中管理配置的方式

我们在使用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给客户端
  • 客户端在收到服务端请求后会立即重新发起连接,回到第一步
关键代码分析

        //单线程维持调用,在该线程内会维护Long polling的相关操作
        static {
            m_executorService = Executors.newScheduledThreadPool(1,
                    ApolloThreadFactory.create("RemoteConfigRepository", true));
        }
        ......
        //设置了5秒的等待时间,防止频繁的刷新配置
        if (!m_loadConfigRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {
            //wait at most 5 seconds
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
            }
        }
        ......   
        //assembleQueryConfigUrl()==组装请求的URL(根据appid,cluster,namespace)
        Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "queryConfig");
        transaction.addData("Url", url);
				......
  			//设置了请求超时时间,这里指定的是90秒,而服务端的断开时间是60秒
        request.setReadTimeout(LONG_POLLING_READ_TIMEOUT);     
				......
				//和远程建立连接
				private void scheduleLongPollingRefresh() {
 					 remoteConfigLongPollService.submit(m_namespace, this);
				}
				//处理结果之后继续循环

Gateway动态路由实现

在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就可以直接开始工作。

由于篇幅的限制介绍的内容不是很直观还请见谅,如果有疑问非常欢迎在品论区进行交流.

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐