在微服务架构中,配置中心是必不可少的基础服务。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的功能差异:
除了上述之外,还有以下其他功能特性:
有史以来最简单的配置中心。使用数据库保存配置是因为微服务拆分粒度相对比较细,使用的配置也会相对比较少,所以使用数据库表就够保存,流程如下:
通过讲解管理后台功能,理解我们当初出于什么原因为什么要这么设计?能解决哪些问题?设计时的考虑点有哪些?通过前面的阅阅读,已知ConfigKeeper有以下核心功能:
为什么要有权限管理?
这个权限系统是我们最初设计的,我们内部现在使用了一个统一的权限系统。为了降低管理成本,我们也开发了微服务管理平台,将配置中心,注册中心,网关管理后台等一系列基础服务都接入到此平台来管理,并通过此平台统一进行权限管理;
我们使用开源系统越多,那么需要管理的账号就会越多,如果团队比较大的话,会增加非常大的管理成本。
配置中心的部署比较灵活,支持多环境集中式管理。但是随行付内部,为了隔离生产环境,我们分开部署了两套配置中心,一套负责开发环境、测试环境、准生产环境的配置管理,另一套负责生产环境的配置管理。当然开发工程师可以选择使用本地配置,不强制开发者环境与配置中心强关联。(只要考虑开发人员众多,需求同步进行)
先回想一下:你有使用jar将配置共享给别人,或别人将提供给你带配置的jar?答案是肯定的,这应该是开发中必须面对的问题,那么使用jar共享配置会带来哪些问题呢?
之前为了统一日志的输出格式,将logback.xml打成一个jar里,让大家使用;而我去年在推新的logback配置规范时,发现与它发生冲突了。为了解决这个冲突,我们在每个项目中增加了个空的logbak.xml文件。
需要与jar包提供方进行协调,还要确认修改是否对其它应用产生影响。
比如有些项目为了复用数据库操作部分代码,将数据库操作以及配置都放到单独的模块,以jar的形式进行复用,如果从复用的角度来看,是非常不错的方法。
但是当系统发展到一定程度后,有些应用的并发量上来了,其数据库连接池的配置就要与其它应用有差别,这时我们还是需要将配置从此模块中拆出来。
通过上面的例子,可以发现配置之所以从代码中提取出,其核心作用就是为了更好适应变化。因为共享配置存在以这些问题,而且微服务架构下,尽量还是以服务的方式来复用业务功能。再者我们一直要将代码进行解偶,那么配置更需要进行解偶。
出于以上种种原因考虑,我们在设计配置中心时,也就没有考虑设计以“组”的形式来共享配置。这也是我们设计时争议比较大的地方。
分为应用配置和全局配置:
为什么还要全局配置?这遇前面讲的组共享配置不是冲突了吗?
全局配置只是用于适应运行环境的变化而设计的,不设计到业务配置。“组”的界限不是很清楚,很容易乱,而全局配置不存在这方面的问题。
为什么单个应用只支持单个配置? 微服务已经拆得比较小了,其配置内容也不会非常多,所以只设计为一个应用只有一个配置。而且经过我们的实践呢,一个配置是可以满足实际需要的。
我们的版本设计相比Git的,要比较简单,但是相应的功能也还有的。主要职责如下:
不管是在内部推广时,还是开源后,都有人问能支持properties吗?其时最初版本是支持的,但我们在前端页面把这个功能屏蔽了,因为我们决定只支持yaml格式。
当Yml也不是完全没有问题的,在实践过程中,偶尔也出现有人把缩进搞错的情况。
使用Yml在线编辑器,可以非常方便编辑,比如:复制粘贴内容,就像在修改配置文件一样,尤其是批量修改时更为方便。不像其它通过key value方便管理的配置中心,每次修改都需要先找到相应的key才能进行一个个修改,非常费时费力;
Yml的JSON预览功能。当用户编辑内容时,会实时检查格式是否符合yaml格式时,如果格式是正确的,右则会正确显示其对应的json内容,如果格式不正确则,右则会提示相应的错误信息,能及时发现错误。
不停机实时刷新配置是配置中心的核心需求之一。比如在生产中运行的应用,突然因需求或性能等原因,需要调整配置,如果我们还需要经过修改代码,重新打包,测试并部署等一系列的操作步骤的话,那效率可想可知,因此带来的损失也可能会非常之大。ConfigKeeper使用Spring Cloud提供的RefreshEndpoint刷新配置,在最初的版本中,我们是通过curl或Postman等工具实现此功能,但这样操作效率比较差,为此在最新版本中增加了如下功能:
在此页面,我们实现如下功能:
因为随行付从Spring boot 1.2.2版本就开始使用Spring boot,到现在已经实现所有应用boot化,所以我们在设计配置中心时,其客户端必须要无缝兼容Spring boot、Spring cloud应用,所以我们就参考Spring cloud config的实现。
为什么ConfigKeeper能实现无缝兼容Spring boot、Spring cloud应用?其原因非常简单,因为核心实现还是由Spring cloud提供的,我们只是在对Spring cloud进行扩展,而不是在其基础上重新造轮子。
要想学习客户端的源码的话,可能以/META-INF/spring.factories文件为入口,此文件中有如下配置:
org.springframework.cloud.bootstrap.BootstrapConfiguration=/ com.suixingpay.config.client.SxfConfigServiceBootstrapConfiguration 复制代码
而SxfConfigServiceBootstrapConfiguration存在如下代码:
[@Bean](https://my.oschina.net/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的实现类,其具体实现请大家查看源码文件。
在我们实践后发现,使用配置中心,还可以很好地对配置进行治理,比如统一使用YAML格式配置,使用配置内容更加清晰;避免了使用jar来共享配置带来的一系列问题等等。但Spring boot、Spring cloud应用可加载的配置源非常之多,还需要注意一些问题。
下面是截取 docs.spring.io/spring-boot… 中的内容:
从上面内容可见,Spring boot是支持非常多种方式加载配置的,而且支持重复配置以及支持覆盖,即相同key的配置,先加载的内容会被后加载的覆盖,为了方便后期维护,尽量遵守以下原则:
相信很多人都会有这样的误区:所有的配置都是可以通过配置中心来实时刷新,不然配置中心的就没有多大意义了。为了解答这个问题,我先来看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获取,比如:
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。生产环境的应用,安全问题不可忽视,所以建议做如下处理:
调整后的配置实例如下:
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 生态功能非常丰富,为我们解决了非常多棘手问题,但很多东西要进行本地化开发后才能更好的使用。配置中心使用了不少开源技术,给我们带来了不少便利,希望通过此开源项目回馈社区,为开源社区贡献绵薄之力。
github.com/sxfad/
gitee.com/sxfad/
ConfigKeeper QQ群:478814745
本分类文章,与「随行付研究院」微信号文章同步,第一时间接收公众号推送,请关注「随行付研究院」公众号。 复制代码