你好呀,我是歪歪。
周末的时候看到一篇关于配置中心的文章,是真的好。
从分析业界通用方案,到匹配自己的业务场景,最后再到亲自动手造个轮子。而且这个轮子,我去看了代码,代码很简洁,几百行代码就实现了一个配置中心的最核心部分的逻辑。
分享给你,给你提供一个看待“配置中心”的新角度。
原文链接:https://code2life.top
配置中心是微服务系统必不可少的组件之一,乍一看好像没多少技术含量,可是,真的是这样吗?
以Java Spring技术栈为例,主流的配置中心有阿里的Nacos、携程的Apollo、以及Spring Cloud Config Server。我们拆解一下其中共通的技术点:
服务端:
客户端:
看到这里,或许你不会觉得配置中心只是简单的KV存储了。主流的型如Alibaba Nacos,作为一个完善的配置和服务发现组件,已经解决了上述大部分问题。我也曾用Nacos,Nacos非常棒,不过我也逐渐发现了一些局限性:
挥下奥卡姆剃刀吧,或许你不需要如此复杂的方案!
我萌生了一个朴素的想法:
既然配置原本就是单纯的文件,那么文件变化时,重新加载对应的Spring Bean不就行了吗?
于是,我开发了一个Spring Boot的配置热重载库,已发布到Maven中心仓库,Github开源仓库地址:
https://github.com/Code2Life/spring-boot-dynamic-config。
话不多说,先看效果。
这个库的使用方式极其简单:只要在注有@Value/@ConfigurationProperties的类上,加上@DynamicConfig注解即可。
我读了一些Nacos、Spring Boot、Spring Cloud的相关源码后,发现实现热重载配置有两类方案:
我不想依赖Spring Cloud的任何组件,选择了实现难度更大的第一类方案,更难是因为单纯的Spring Boot没有Spring Cloud Starter的父Context和@RefreshScope注解,不能直接destroy原来的Bean,refresh一个新Bean出来,得“飞行中换引擎”。
文件变化监听
第一步,从Environment Bean的PropertySources里,把文件配置的PropertySource给揪出来。
// 需要先实现EnvironmentAware接口或自动装配StandardEnvironment
MutablePropertySources propertySources = environment.getPropertySources();
for (PropertySource<?> ps : propertySources) {
boolean isFilePropSource = isFromConfigFile(ps);
if (isFilePropSource) {
// 找到配置文件的PropertySource
}
}
第二步,用Java NIO的Watch API监听配置目录。
说到这个第二步,有个小坑,可以看看这篇文章《麻了,被JDK的这个BUG秀麻了!》
watchService = FileSystems.getDefault().newWatchService();
Paths.get(configLocation).register(watchService, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE);
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
reloadChangedFile(event)
}
key.reset();
}
第三步,如果配置文件变了,把原来的PropertySource对象replace成重新创建出来的。
propertySources.replace(filePropertySourceName, newPropertySource);
// 上述代码只是一些示意片段,完整的实现参考在 DynamicConfigPropertiesWatcher 类
至此,我们用数十行代码,已经实现了动态的Environment Bean,用getProperty()获得的结果已经是动态的了。
Spring Boot开发者一般是在Bean中使用@Value、@ConfigurationProperties来注入配置内容的,因此,原始的配置值已经分散IoC容器里各个相关的Bean中,我们还需要更进一步,在发生变化的同时,把这些Spring Context碗里相关的 bean 再揪出来,偷梁换柱。
所以还得修改Spring Bean的属性。
遇到的问题
因为要处理许多异常情况、兼容性问题等等,遇到了这两个问题:
读到这里,或许你会质疑,这样做在本地开发没问题,但直接用文件的方式来管理开发/产线环境的配置,不是在开倒车吗?难道部署100个实例,要去100台机器上改配置文件?
当然不是。
对配置文件的修改,一定,一定,一定要在Git仓库中,最好是独立于代码仓库之外的配置仓库。
为什么要用Git管理配置?
我参与了数十个Spring Cloud服务在全球十几个数据中心的容器化部署和运维,深刻体会了配置管理中的痛点。
我们从相对简单的SpringCloud Config,换到功能复杂的Nacos,都没有解决掉本质的问题:
应用配置是DevOps的一环,本应该和其他环节一样,通过GitOps的持续交付流水线实现自动化,不是去登录任何一个系统去输入任何一行配置。
我们设想一个场景,你作为一名开发,现在想更新一行产线配置。
Nacos的工作流是什么样的呢?
Git的工作流是怎么样的呢?
你不需要登录任何“配置管理系统。
你的运维同事不需要敲N下键盘、点N次鼠标。
你不需要发邮件、写文档。
甚至不需要和领导/运维同事发消息,整个过程就如丝般顺滑的在Git上完成了。
如果是开发环境,审批和走查流程灵活一些的话,配置Push到Git的开发分支,自动触发CI Pipeline,喝口水就生效了。
从这个场景可以看出,维护配置的职责交给Git,有很多好处。
读到这里,或许你还有疑问:Git仓库里的配置内容,怎么就通过一个神奇的流水线,“变”到产线的那么多服务器的文件系统里面呢?
还有在Git里面,肯定不能维护产线密钥,怎么办呢?
复杂的分布式系统,离不开基础平台的支持,Kubernetes就是这样一个业界标准级的云原生操作系统。
上一节说的神奇的流水线,并不是把文件拷贝到产线机器上,而仅仅是调用了一个HTTPS请求,来更新Kubernetes ConfigMap资源。
我们先回头看下一开始提的配置中心的技术难点,是怎么被Kubernetes ConfigMap/Secret功能完美解决的。
服务端:
而对于客户端:文件/环境变量就是最原始的配置方式,应用层没有任何额外性能开销和学习使用成本,也天然兼容任何现有的技术栈,只需要在文件变化时在应用层做一次reload即可。
主要特别说明一下的是:每个K8S集群节点上运行的Kubelet,虽然会Watch资源的实时变化,但真正的更新是在1分钟的同步周期做的,也就是说配置修改在1分钟内会在所有运行实例中生效。这个机制是对Kubernetes API Server的请求削峰和保护,对于小型集群可以在Kubelet的启动参数减小“syncFrequency”的默认时间,加速配置生效。
下面是在Kubernetes中使用的Yaml示例片段:
# 1. 前提: 在Jenkins/Argo CD/Github Actions中,
# 创建Config Git Repo到Kubernetes ConfigMap的同步Pipeline
# 2. 在声明的K8S Deployment中,
# 添加环境变量 SPRING_CONFIG_LOCATION
# 指向Config Map mountPath,注意需要'/'结尾
kind: Deployment
apiVersion: apps/v1
metadata:
spec:
template:
spec:
volumes:
- name: conf-path
configMap:
name: your-app-config
containers:
- name: app-container
image: '<your-image-name>'
env:
- name: SPRING_CONFIG_LOCATION
value: /config/
# Spring支持 Relaxing Binding,使用 -jar xxx.jar --spring.config.location 也可以
command: ["java", "-jar", "your-spring-boot-app.jar"]
volumeMounts:
- name: conf-path
mountPath: /config
本文主要介绍了我最近开发的一个实现Spring Boot动态配置的轻量级库:Spring Boot Dynamic Config,以及为什么结合Git + Kubernetes的配置管理模式,优于其他配置管理组件。
技术是解决问题的,脱离问题场景做选择是没有意义的。
开发这个库的动机,是在参与数十个微服务应用的DevOps工作时,看着运维同事深陷大量环境和服务的配置管理泥坑,我开始反思一个问题:
配置管理有必要如此复杂吗?
当我们已经有了Git、有了Kubernetes,那么,Git不就是那个最完美的配置管理系统吗?
Kubernetes不就是那个最完美的配置中心吗?
踏破铁鞋无觅处,得来全不费工夫。
Keep it simple and stupid !
Kubernetes + Git解决了复杂微服务系统的配置管理问题。其实,Kubernetes + X的组合几乎可以解决掉服务治理的所有问题。
而开发人员很少了解Kubernetes这样强大的云原生平台,认为Kubernetes仅仅是部署运维的工具。
我以后还会再写一些文章来说明:为什么在Kubernetes体系下,许多组件和轮子是不必要的,包括主流的Spring Cloud生态的诸多组件。
之前也曾写过一个简单的回答:
https://www.zhihu.com/question/430048535/answer/1582533126
有兴趣的可以去看看,我这里只是截个结论:
为什么造这个轮子?
Kubernetes ConfigMap/Secret已经是主流的云原生配置方案,而Java Spring生态缺少一个动态感知ConfigMap/Secret变化的轻量级库。
Spring Cloud Kubernetes (https://spring.io/projects/spring-cloud-kubernetes) 这个库采用直接调用Kubernetes API的方案,一方面这个库过重了;另一方面与Kubernetes耦合过于紧密,启动服务还需要访问Kubernetes API的权限,不合适。
效果如何?
这个小轮子一共花了一周多的业余时间,一小半的时间在解决疑难杂症,还有一小半的时间在写文档、改进单元测试和代码质量。
最终,精简到仅有600多行实现代码,无任何除了Spring Boot核心库以外的依赖。同时开发了400多行单元测试,测试覆盖率95%,CodeBeat代码质量评分在A/B级之间。
最后,再贴一下项目地址,感兴趣的朋友可以去看看源码:
https://github.com/Code2Life/spring-boot-dynamic-config
2022 年下半年的第一天,我和 MX 同学去听了野孩子乐队的现场。其实还是期待了很长时间的,终于在周五的晚上成功赴约了。
下半年的第二天,回了一趟老家。
在十八线小县城,这次来去匆匆,回去待的时间加起来也没有超过 24 小时。
我以前说离家近的好处就是可以随时回家,但是除了国庆,春节这样的长假外,我好像也没回去过。
虽然这样说,但是“离家近,离家人近”给人的感觉还是很踏实的,有一种归属感。
漂泊的对立面,就是归属。
虽然我老家不是成都,但是我对成都这座城市就非常的有归属感。
这份归属感它来源于离家人很近、和 MX 同学在一起、偶尔临时起意就能和朋友相聚一堂、消费不高、房租平价、饮食相符,还有周围被四川话围绕的感觉。
哦,对了。
因为听了野孩子的现场,心有所感,所以从老家回成都的动车上,发了一篇关于乐队和摇滚的文章《暂时还没想好取什么标题》。
列车上的网络非常不好,写作体验也很不好,磕磕绊绊也算是写完了。写之前先想题目,想了几分钟真的不知道取什么标题,于是就先写下“暂时还没想好取什么标题”。
写完之后还是想不到,于是索性就用这个标题发文了。
现在我想到了一个标题,就是文章里面出现的一句歌词:山川湖海,厨房与爱。
你可以看看。
最近歌荒,也可以把你喜欢的歌留在那篇文章的评论区,推荐给我,谢谢。
··················END················
你好呀,我是歪歪。我没进过一线大厂,没创过业,也没写过书,更不是技术专家,所以也没有什么亮眼的title。
当年高考,随缘调剂到了某二本院校计算机专业。纯属误打误撞,进入程序员的行列,之后开始了运气爆棚的程序员之路。
说起程序员之路还是有点意思,可以点击蓝字,查看我的程序员之路。