这个月,我和我的同事们正在开源一个内部的架构治理平台:ArchGuard,我们进行了一系列的遗留系统的迁移工作:
- 从 Maven 到 Gradle。原因是灵活的自定义 task,还有自带的增量构建等。
- 依赖库的更新。
- 系统从微服务到单体。
- 构建规范和对应的规范工具化
- 持续交付。结合 GitHub Action、Docker Hub 等一系列的 DevOps 开源基础设施,进行全自动化的构建。
- ……
其中,最有意思的一个故事莫过于:从微服务到单体架构。因为,它是一种反主流的形式,又或者是反主流的技术架构。
遗留微服务系统的挑战
在我们经过了一系列的内部会议之后,决定了将 ArchGuard 开源。随后,看到了代码库,我们发现了一系列的挑战:
- 过多的服务/模块。这个在内部开发多年的系统,由多个微服务组成 + 代码库组成,
- 知识缺少沉淀。先前并没有留下太多的开发文档,不了解当时做的一系列技术决策,需要从 Git 历史中汲取。
- 复杂的部署架构。同样是工具,对比于 Jenkins/Sonarqube 的部署方式,相对较为复杂。
- 不一定合理的服务划分。我们需要部署一系列的服务,但是只有扫描器(Arch Scanner)才需要弹性伸缩这样的特性。
于是呢,我们重新思考合理的后端服务(微服务)的颗粒度应该是怎样的?所以,参考于过去总结的什么用微服务?以及有多少个微服务更合理?先前的一个结论,类似于:
- 微服务的数量不超过开发人员的数量。
- 满足康威定律。微服务与开发团队对齐。
- 两个比萨团队原则 —— 开发团队的人员数量维护在 3 \~ 12 人。一个微服务只由一个开发团队维护。
- 高内聚,低耦合。单个服务与其它服务依赖少。如:两个服务存在相互调用,耦合度相对较高,可以考虑合为一个服务。
- 收益大于开销。创建服务的开销是否超过了独立成服务的好处。
- 你不一定需要微服务。考虑采用 DDD (领域驱动设计)分层架构来划分,以方便未来拆分为微服务。
在这个场景之下,几乎违反了上面的一系列规则。所以,我就回到了上述的 6 中去,采用 DDD 的分层架构模式。每个资源/聚合/服务在各自的包下管理(common 除外):
├── Application.kt
├── clazz
├── code
├── common
├── config
├── evaluation_bak
├── evolution
├── method
├── metrics
├── module
├── packages
├── qualitygate
├── report
├── report_bak
├── scanner
├── scanner2
└── system_info
由于是合并的代码,所以代码中除在于 _bak 还有 scanner2 这样看似重复,又或者是迁移中的代码。
为什么单体更适合当前?
再回到多年以前, Martin Fowler 写了那篇《Monolithic First》,意在告诉人们在团队微服务能力和技术不够成熟的时候,你不应该采用微服务。这里的场景和上述的这个场景并不是一样的。对于系统的最终形态来说,单体并不一定适合这个系统,但是当于当前的我们来说,单体是最合适的。原因诸如于:
- 单体部署架构决定应用架构。使用 Docker,尽管 Saas 也是更友好的。但是,作为一个刚起步的开源项目,并不会资金来支撑这种规模的 SaaS 服务。
- 最终用户是开发者。软件的使用者本身又可能成为开发者,所以能一次启动就应该一次启动。
- 开发者体验优先。开源与面向开发者决定了 ArchGuard 是一个开发者体验优先的系统。如果一个参与到 ArchGuard 项目的开发者,要在多个项目中切换, 那么这中体验是非常差的。在开源社区里,一直都是单体优先,如 Gradle、Spring 等。
- 首次部署速度。
- setup 速度。
总体来说,作为一个开源应用/工具,软件工程的模式受限于其合作模式。所以,常规的软件开发架构,并不一定适用,我们需要一些更好的模式。
那么,我们还有别的选择吗?
我们的目标架构是单体吗?
从某种意义上,就当前来说,它是的。但是,如果管理有所不善的话,它会变成一个大泥球架构。回顾一下,一个多仓库/多模块的微服务系统,它与一个单体系统在物理形态上的主要区别在于:
- 微服务使用的是进程间调用,单体是进程内调用。
- 微服务最终有多个制品包,而单体只有一个或者是插件化的一带多。
所以,只要我们用相似的形态来构建一个单体应用,那么它在部署形态上就可以变成是微服务架构。简单来说,就是:
- 代码库内,包(package、service)间的调用使用 HTTP 调用,而不是函数调用。
- 通过自定义的构建脚本,在构建时拆分代码库,生成多个服务制品,并进行部署。
从结果来说,便是将系统放置在一种临界状态。以让人们根据自己的需要,做出不同的选择。如在 SaaS 化的时候,这就可以变成微服务的形态,单体部署时,则可以变成单体的状态。唯一麻烦的是,需要开发者对于构建系统有足够的了解,并设计好充足的自动化测试设施。
如何迁移 ?
接着,我们就开始合并多个代码仓库,其中的一些
- 保留历史提交记录的合并。主要是结合 git-filer-repo 来进行过滤和选择路径。
- 构建配置的全集。对 Application.properties 等进行统一。
- 使用相同的依赖版本。由于不同的年代的原因,所以选择的依赖版本也有所不同,需要尝试先统一,才能合并代码。
- 解决冲突。因为,只合并了 src 目录下的内容,如果包名有问题,如冲突了,需要重置。类似的问题,还有:Application 重复、Bean 冲突、Service 冲突。
就迁移过程来说,它并不复杂,就是耗时。
还有其它选项吗?
相似的场景,如果一个开发人员多个微服务,并且在不考虑单机部署的情况下,Monorepo 是一个更好的选择,把所有微服务项目的代码放在一个仓库里。
毕竟,Google 都可以把所有的代码仓库放一起,我们又有什么不可以的。当然了,Google 使用的技术原理是不一样的。不过,它能提供一个足够强壮的理由。
其它
回过头来看,对于小的团队来说,单体会不会是更合适的选择?那么大的团队呢?