微服务之配置中心ConfigKeeper
在微服务架构中,配置中心是必不可少的基础服务。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<String> refresh() {
Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
// 加载最新配置到Environment
addConfigFilesToEnvironment();
Set<String> 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 生态功能非常丰富,为我们解决了非常多棘手问题,但很多东西要进行本地化开发后才能更好的使用。配置中心使用了不少开源技术,给我们带来了不少便利,希望通过此开源项目回馈社区,为开源社区贡献绵薄之力。