Blog

Blog

PHODAL

回到单体架构:一个开源项目的重构

这个月,我和我的同事们开源了一个内部的架构治理平台:ArchGuard,我们进行了一系列的遗留系统的迁移工作:

  • 从 Maven 到 Gradle。原因是灵活的自定义 task,还有自带的增量构建等。
  • 依赖库的更新。
  • 系统从微服务到单体。
  • 构建规范和对应的规范工具化
  • 持续交付。结合 GitHub Action、Docker Hub 等一系列的 DevOps 开源基础设施,进行全自动化的构建。
  • ……

其中,最有意思的一个故事莫过于:从微服务到单体架构。因为,它是一种反主流的形式,又或者是反主流的技术架构。

遗留微服务系统的挑战

在我们经过了一系列的内部会议之后,决定了将 ArchGuard 开源。随后,看到了代码库,我们发现了一系列的挑战:

  • 过多的服务/模块。这个在内部开发多年的系统,由多个微服务组成 + 代码库组成,
  • 知识缺少沉淀。先前并没有留下太多的开发文档,不了解当时做的一系列技术决策,需要从 Git 历史中汲取。
  • 复杂的部署架构。同样是工具,对比于 Jenkins/Sonarqube 的部署方式,相对较为复杂。
  • 不一定合理的服务划分。我们需要部署一系列的服务,但是只有扫描器(Arch Scanner)才需要弹性伸缩这样的特性。

于是呢,我们重新思考合理的后端服务(微服务)的颗粒度应该是怎样的?所以,参考于过去总结的什么用微服务?以及有多少个微服务更合理?先前的一个结论,类似于:

  1. 微服务的数量不超过开发人员的数量。
  2. 满足康威定律。微服务与开发团队对齐。
  3. 两个比萨团队原则  —— 开发团队的人员数量维护在 3 \~ 12 人。一个微服务只由一个开发团队维护。
  4. 高内聚,低耦合。单个服务与其它服务依赖少。如:两个服务存在相互调用,耦合度相对较高,可以考虑合为一个服务。
  5. 收益大于开销。创建服务的开销是否超过了独立成服务的好处。
  6. 你不一定需要微服务。考虑采用 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 化的时候,这就可以变成微服务的形态,单体部署时,则可以变成单体的状态。唯一麻烦的是,需要开发者对于构建系统有足够的了解,并设计好充足的自动化测试设施。

如何迁移 ?

接着,我们就开始合并多个代码仓库,其中的一些

  1. 保留历史提交记录的合并。主要是结合 git-filer-repo 来进行过滤和选择路径。
  2. 构建配置的全集。对 Application.properties 等进行统一。
  3. 使用相同的依赖版本。由于不同的年代的原因,所以选择的依赖版本也有所不同,需要尝试先统一,才能合并代码。
  4. 解决冲突。因为,只合并了 src 目录下的内容,如果包名有问题,如冲突了,需要重置。类似的问题,还有:Application 重复、Bean 冲突、Service 冲突。

就迁移过程来说,它并不复杂,就是耗时。

还有其它选项吗?

相似的场景,如果一个开发人员多个微服务,并且在不考虑单机部署的情况下,Monorepo 是一个更好的选择,把所有微服务项目的代码放在一个仓库里。

毕竟,Google 都可以把所有的代码仓库放一起,我们又有什么不可以的。当然了,Google 使用的技术原理是不一样的。不过,它能提供一个足够强壮的理由。

其它

回过头来看,对于小的团队来说,单体会不会是更合适的选择?那么大的团队呢?

关于我

Github: @phodal     微博:@phodal     知乎:@phodal    

微信公众号(Phodal)

围观我的Github Idea墙, 也许,你会遇到心仪的项目

QQ技术交流群: 321689806
comment

Feeds

RSS / Atom

最近文章

关于作者

Phodal Huang

Developer, Consultant, Writer, Designer

ThoughtWorks 高级咨询师

工程师 / 咨询师 / 作家 / 设计学徒

开源深度爱好者

出版有《前端架构:从入门到微前端》、《自己动手设计物联网》、《全栈应用开发:精益实践》

联系我: h@phodal.com

微信公众号: 与我沟通

标签