在微服务架构中,配置中心是必不可少的基础服务。ConfigKeeper已开源,本文将深度分析配置中心的核心内容,错过「Spring Cloud中国社区北京沙龙-2018.10.28 」的同学将从本篇文章中收获现场的分享内容。

背景

微服务+容器架构后,为了方便动态更新应用配置,需要把配置文件放到应用执行包之外的配置中心,这样一来,一个可执行包就可以在不同的环境下运行,大幅度降低包的版本管理成本,也可以有效控制docker镜像的版本管理成本。传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求。对程序配置的期望值也越来越高:配置修改后实时生效,分环境、分集群管理配置,完善的权限、审核机制等等。于是便诞生了ConfigKeeper。

ConfigKeeper是随行付架构部基于Spring Cloud研发的分布式配置中心,与Spring Boot、Spring Cloud应用无缝兼容。

虽然Spring Cloud 已经为我们提供了基于git或mongodb等实现的配置中心,但是这些方案实现都过于简单,没有达到实际可用的标准。比如:没有提供统一的管理页面,不便于操作和使用;没有权限管理功能;没有数据验证功能等等。但Spring Cloud Config的核心技术还是可以为我们所用,没有必要重新造轮子。

定制的原因

市面上已经有几款比较成型的配置中心,大家耳熟能详的携程Apollo和百度Disconf,而我们的配置中心底层是基于Spring Cloud Config模块进行扩展的,首先来看看Apollo、Spring Cloud Config、ConfigKeeper的功能差异:

功能点

Apollo

Spring Cloud Config

ConfigKeeper

配置界面

一个界面管理不同环境、不同集群配置

无,需要通过git操作

配置信息落入数据库中,友好页面管理

配置生效时间

实时

重启生效或者手动refresh生效

实时推送、重启生效、手动refresh生效

版本管理

界面上直接提供发布历史和回滚按钮

无,需要通过git操作

管理页面一键回滚

灰度发布

支持

不支持

支持,与Spring Cloud其他组件打通

授权、审核、审计

界面上直接支持,而且支持修改、发布权限分离

需要通过git仓库设置,且不支持修改、发布权限分离

应用分配制权限管理

实例配置监控

可以方便的看到当前哪些客户端在使用哪些配置

不支持

心跳推送,一目了然

配置获取性能

快,通过数据库访问,还有缓存支持

较慢,需要从git clone repository,然后从文件系统读取

本地式缓存文件,配置增量推送

客户端支持

原生支持所有Java和.Net应用

支持Spring应用,提供annotation获取配置

Spring、Spring Boot、Spring Cloud

支持YAML格式

不支持

支持

支持

除了上述之外,还有以下其他功能特性:

开发人员最习惯的就是在文件中修改配置,管理页面上提供「舒适」的富文本编辑框;

全局配置约定,比如多个项目共享的配置,比如短信地址等采取约定大于配置。全局配置

配置校验,文本修改高亮对比修改内容,防止低级错误等;

架构设计

有史以来最简单的配置中心。使用数据库保存配置是因为微服务拆分粒度相对比较细,使用的配置也会相对比较少,所以使用数据库表就够保存,流程如下:

用户先去配置中心 添加、修改配置;

应用启动时:(Spring boot应用向配置中心客户端获取配置、然后缓存配置到本地内存及本地文件缓存、应用根据配置进行启动;)

不停机更新配置(调用Spring Cloud的RefreshEndpoint、通过RefreshEndpoint刷新配置)

使用前后端分离架构,如果需要重新设计管理界面,也可以使用自己习惯的技术实现


设计的初衷

通过讲解管理后台功能,理解我们当初出于什么原因为什么要这么设计?能解决哪些问题?设计时的考虑点有哪些?通过前面的阅阅读,已知ConfigKeeper有以下核心功能:


权限管理

为什么要有权限管理?

1.对于企业级应用来说,权限管理是必不可以一个需求;

2.通过权限管理隔离数据,保证数据的安全性,避免误操作;

3.在微服务比较多情况下,也可以通过权限自动过滤出我们所关心的服务,不需要再自己手动过滤,减少不必要的操作,可以提高工作效率;


这个权限系统是我们最初设计的,我们内部现在使用了一个统一的权限系统。为了降低管理成本,我们也开发了微服务管理平台,将配置中心,注册中心,网关管理后台等一系列基础服务都接入到此平台来管理,并通过此平台统一进行权限管理;

我们使用开源系统越多,那么需要管理的账号就会越多,如果团队比较大的话,会增加非常大的管理成本。

多环境管理

配置中心的部署比较灵活,支持多环境集中式管理。但是随行付内部,为了隔离生产环境,我们分开部署了两套配置中心,一套负责开发环境、测试环境、准生产环境的配置管理,另一套负责生产环境的配置管理。当然开发工程师可以选择使用本地配置,不强制开发者环境与配置中心强关联。(只要考虑开发人员众多,需求同步进行)


配置设计

先回想一下:你有使用jar将配置共享给别人,或别人将提供给你带配置的jar?答案是肯定的,这应该是开发中必须面对的问题,那么使用jar共享配置会带来哪些问题呢?

容易造成冲突

之前为了统一日志的输出格式,将logback.xml打成一个jar里,让大家使用;而我去年在推新的logback配置规范时,发现与它发生冲突了。为了解决这个冲突,我们在每个项目中增加了个空的logbak.xml文件。

不方便修改。

需要与jar包提供方进行协调,还要确认修改是否对其它应用产生影响。

不能做差异化配置

比如有些项目为了复用数据库操作部分代码,将数据库操作以及配置都放到单独的模块,以jar的形式进行复用,如果从复用的角度来看,是非常不错的方法。

但是当系统发展到一定程度后,有些应用的并发量上来了,其数据库连接池的配置就要与其它应用有差别,这时我们还是需要将配置从此模块中拆出来。

通过上面的例子,可以发现配置之所以从代码中提取出,其核心作用就是为了更好适应变化。因为共享配置存在以这些问题,而且微服务架构下,尽量还是以服务的方式来复用业务功能。再者我们一直要将代码进行解偶,那么配置更需要进行解偶。

出于以上种种原因考虑,我们在设计配置中心时,也就没有考虑设计以“组”的形式来共享配置。这也是我们设计时争议比较大的地方。

配置内容

分为应用配置和全局配置:

全局配置:是某一环境下所有应用共享的配置,比如公司的邮件服务配置;注册中心地址、公司名称、公司地址等,可能会变化,但普遍性非常高的配置。

应用配置:每个应用个性化的配置;

为什么还要全局配置?这遇前面讲的组共享配置不是冲突了吗?

全局配置只是用于适应运行环境的变化而设计的,不设计到业务配置。“组”的界限不是很清楚,很容易乱,而全局配置不存在这方面的问题。

为什么单个应用只支持单个配置?

微服务已经拆得比较小了,其配置内容也不会非常多,所以只设计为一个应用只有一个配置。而且经过我们的实践呢,一个配置是可以满足实际需要的。

支持版本控制

我们的版本设计相比Git的,要比较简单,但是相应的功能也还有的。主要职责如下:

配置每被修改一次,会将旧数据及版本号保存到日志表中,更新配置内容的同量,将版本号加一

支持版本比较功能:方便查看与最新版本的差异;检查在哪天做了什么调整;

支持回退功能:如果配置出现问题,可以快速回退;

修改配置

不管是在内部推广时,还是开源后,都有人问能支持properties吗?其时最初版本是支持的,但我们在前端页面把这个功能屏蔽了,因为我们决定只支持yaml格式。

1.properties 对中文支持不是好,而yml却没有这个问题;

2.yaml能很好管理同类项配置,避免配置重复key。看过不少properties文件,配置杂乱以及同一个文件出重相同的key,不同value的情况;不是所有的开发都是有强迫症;

3.统一大家的习惯;


当Yml也不是完全没有问题的,在实践过程中,偶尔也出现有人把缩进搞错的情况。

使用Yml在线编辑器,可以非常方便编辑,比如:复制粘贴内容,就像在修改配置文件一样,尤其是批量修改时更为方便。不像其它通过key value方便管理的配置中心,每次修改都需要先找到相应的key才能进行一个个修改,非常费时费力;

Yml的JSON预览功能。当用户编辑内容时,会实时检查格式是否符合yaml格式时,如果格式是正确的,右则会正确显示其对应的json内容,如果格式不正确则,右则会提示相应的错误信息,能及时发现错误。


实例基本信息及批量刷新

不停机实时刷新配置是配置中心的核心需求之一。比如在生产中运行的应用,突然因需求或性能等原因,需要调整配置,如果我们还需要经过修改代码,重新打包,测试并部署等一系列的操作步骤的话,那效率可想可知,因此带来的损失也可能会非常之大。ConfigKeeper使用Spring Cloud提供的RefreshEndpoint刷新配置,在最初的版本中,我们是通过curl或Postman等工具实现此功能,但这样操作效率比较差,为此在最新版本中增加了如下功能:


在此页面,我们实现如下功能:

1.列出所有应用实例的IP、管理端口等信息

2.查看应用中配置的版本是否是最新的;(非常方便核对应用版本是否是最新的;避免漏操作等问题;)

3.实现灰度发布;(可以手动刷新选中的一个或多个实例的配置;)

客户端实现

因为随行付从Spring boot 1.2.2版本就开始使用Spring boot,到现在已经实现所有应用boot化,所以我们在设计配置中心时,其客户端必须要无缝兼容Spring boot、Spring cloud应用,所以我们就参考Spring cloud config的实现。

无缝兼容Spring boot、Spring cloud应用

为什么ConfigKeeper能实现无缝兼容Spring boot、Spring cloud应用?其原因非常简单,因为核心实现还是由Spring cloud提供的,我们只是在对Spring cloud进行扩展,而不是在其基础上重新造轮子。

只依赖 spring-cloud-context 和 spring-cloud-commons 两个jar;

Spring cloud 提供PropertySourceLocator接口,方便我们去加载外部配置,ConfigKeeper的客户端核心代码就是实现此接口;

客户端源码解析

要想学习客户端的源码的话,可能以/META-INF/spring.factories文件为入口,此文件中有如下配置:

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.suixingpay.config.client.SxfConfigServiceBootstrapConfiguration
而SxfConfigServiceBootstrapConfiguration存在如下代码:
@Bean
@ConditionalOnMissingBean(SxfConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "suixingpay.config.enabled", matchIfMissing = true)
public SxfConfigServicePropertySourceLocator sxfConfigServicePropertySource(ApplicationContext context) {
SxfConfigClientProperties configClientProperties = sxfConfigClientProperties(context);
ConfigDAO configDAO = sxfConfigDAO(configClientProperties);
return new SxfConfigServicePropertySourceLocator(configDAO, configClientProperties);
}

而SxfConfigServicePropertySourceLocator其实就是PropertySourceLocator的实现类,其具体实现请大家查看源码文件。

客户端特性

支持客户端负载:如果有多个配置中心服务器实例,可以通过简单的轮询实现客户端负载,达到高可能的效果。当然也可以使用nginx 反向代理实现服务端负载。

支持失败后重试功能;

支持本地缓存

客户端从配置中心拉取最新配置后,会缓存到本地磁盘。每次去拉取配置之前,会加载本地缓存配置的版本信息,前传到服务端,如果服务端与客户端的版本一致时,接口会返回304状态,并使用本地缓存进行启动应用,当服务端与客户端的版本不一致时,会返回最新版本,并缓存到本地磁盘中。通过此缓存机制,一方面可以降低网络带宽,二是即使配置中心不可用,也不会影响应用的启动。

上报应用实例信息

使用建议

配置治理

在我们实践后发现,使用配置中心,还可以很好地对配置进行治理,比如统一使用YAML格式配置,使用配置内容更加清晰;避免了使用jar来共享配置带来的一系列问题等等。但Spring boot、Spring cloud应用可加载的配置源非常之多,还需要注意一些问题。

Command line arguments.
Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property)
ServletConfig init parameters.
ServletContext init parameters.
JNDI attributes from java:comp/env.
Java System properties (System.getProperties()).
OS environment variables.
A RandomValuePropertySource that only has properties in random.*.
Profile-specific bootstrap properties outside of your packaged jar (bootstrap-{profile}.properties and YAML variants)
Profile-specific bootstrap properties packaged inside your jar (bootstrap-{profile}.properties and YAML variants)
Bootstrap properties outside of your packaged jar (bootstrap.properties and YAML variants).
Bootstrap properties packaged inside your jar (bootstrap.properties and YAML variants).
Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants)
Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants)
Application properties outside of your packaged jar (application.properties and YAML variants).
Application properties packaged inside your jar (application.properties and YAML variants).

通过 PropertySourceLocator 加载配置(应用配置优先级要高于全局配置)

@PropertySource annotations on your @Configuration classes.
Default properties (specified using SpringApplication.setDefaultProperties).

从上面内容可见,Spring boot是支持非常多种方式加载配置的,而且支持重复配置以及支持覆盖,即相同key的配置,先加载的内容会被后加载的覆盖,为了方便后期维护,尽量遵守以下原则:

尽量避免同一key在多个地方配置的情况;

如果第1种情况不可避免,那么要注意各个配置中的优化级,比如ConfigKeeper中全局配置的优先级要低于应用配置;

约定配置位置

可配置的比较那么多,在团队中每个人使用的方法不一样,抛必造成混乱,所以需要大家提前做好约定,比如:哪些配置通过命令行来配置,那些配置放到bootstrap 文件中,那些放到application 文件中。

拒绝使用jar共享配置

是不是所有的配置都可以通过配置中心来实时刷新?

相信很多人都会有这样的误区:所有的配置都是可以通过配置中心来实时刷新,不然配置中心的就没有多大意义了。为了解答这个问题,我先来看RefreshEndpoint都做了哪些事情:

public synchronized Set refresh() {
Map before = extract(this.context.getEnvironment().getPropertySources());
// 加载最新配置到Environment
addConfigFilesToEnvironment();
Set keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
// 发送EnvironmentChangeEvent
this.context.publishEvent(new EnvironmentChangeEvent(context, keys));
// 清空RefreshScope缓存
this.scope.refreshAll();
return keys;
}

通过上面的源码,我们可以看出其RefreshEndpoint主要做了三件事情:

加载最新配置到Environment

发送EnvironmentChangeEvent

清空RefreshScope缓存

所以我们要想获取最新配置配置,可以通过以下途径:

直接通过Environment获取,比如:

String applicationName = environment.getProperty("spring.application.name");

处理EnvironmentChangeEvent,比如对于线程池大小的调整,我们可以监听EnvironmentChangeEvent,当接收到EnvironmentChangeEvent时,关闭原来的线程池,前重新实例化新的线程池;

Spring boot官方建议我们尽量我们使用@ConfigurationProperties管理配置,那么它是否能自动刷新配置呢?其实它是可以的,因为在ConfigurationPropertiesRebinder中会监听EnvironmentChangeEvent,详细内容请查看org.springframework.cloud.context.properties. ConfigurationPropertiesRebinder。

在实例化bean时增加@RefreshScope, 比如:

@Autowired
private DefaultUserProperties userProperties;
@RefreshScope // 支持动态刷新
@Bean(name="defaultUser")
public UserDO defaultUser() {
UserDO userDO=new UserDO();
userDO.setId(userProperties.getId());
userDO.setName(userProperties.getName());
return userDO;
}

Spring cloud 为了实现运行时动态刷新,增加了RefreshScope(org.springframework.cloud.context.scope.refresh.RefreshScope类),会将加了@RefreshScope的bean放入RefreshScope中,当刷新RefreshScope时,会清空缓存,当下次使用这些bean时会重新实例些这些bean。

安全提示

通过RefreshEndpoint 刷新的话,就需要开启Spring boot Endpoint相关功能,而Spring boot Endpoint如果不做特殊处理的话,很容易被探测到,引发一些安全问题。比如:

server:
port: 8080
management:
security:
enabled: false

那么很容易去调用Spring boot Endpoint。生产环境的应用,安全问题不可忽视,所以建议做如下处理:

management.port 与 server.port 设置不同的值,并且此端口不允许外网访问;

增加安全验证;

修改management.context-path

生产环境的management相关配置,尽量与其它环境的配置要有差异,不能完全一样。

调整后的配置实例如下:

server:
port: 8080
management:
security:
enabled: true
context-path: /_ops
port: 9098
security:
basic:
enabled: true
path: ${management.context-path}/**, /swagger-ui.html, /v2/api-docs, /druid/**
user:
name: ma
password: xxxxxx

开源地址

Spring 生态功能非常丰富,为我们解决了非常多棘手问题,但很多东西要进行本地化开发后才能更好的使用。配置中心使用了不少开源技术,给我们带来了不少便利,希望通过此开源项目回馈社区,为开源社区贡献绵薄之力。