微服务被认为是一种理想的架构模式,因此,Steven Lemon 所在公司的领导层决定从单体架构向微服务架构迁移,这让整个开发团队在随后的的日子里苦不堪言,七大现实问题摆在面前无法解决,微服务架构的好处也没有享受到,并发现这不单单是一个技术问题。最终,整个团队决定放弃。
最近,我所在的开发团队在紧张的交付周期结束后,有了短暂的休息机会。领导层认为可以利用这段时间将单体架构迁移至微服务。经过一个月的调研和准备之后,我们最终放弃迁移计划,继续使用原先的单体架构。在我们看来,微服务不仅不会帮到我们,反而会对开发流程造成严重影响。
微服务被认为是一种理想的架构模式,但并不适合我们。我们公司的情况是这样的:一共有 200 多名开发人员,不过我们团队只有 5 个人,大概有 5% 的后端开发工作涉及公司层面的单体系统,这是一个巨大的 C#应用程序。剩下的时间,我们开发了自己的两个 Node 服务。
我们团队负责的这两个服务都很小巧,完全可以控制开发、架构和部署整个流程。遇到性能问题时,我们会把生产环境中的实例数量增加一倍,直到把底层问题解决。我们几乎不与其他团队合作,因为这些服务是用 TypeScript 开发的,所以我们团队 (主要是前端开发人员) 能够在前端和后端使用相同的编程语言。最重要的是,我们可以在客户端及后端的验证和报告服务中包含复杂的规则计算引擎。总而言之,我们整个团队专注于特定业务。
首先需要声明:我们不喜欢开发单体系统,因为增加新功能、编译和运行测试都很慢,而且架构经常发生变化,在构建过程中总会出现难以预料的东西。所以,当领导提出迁移至微服务架构时,我们也同意了。
然而, 在整个调研过程中,我们发现了如下七大难以解决的问题,这些问题让我们最终选择放弃。
我们的整体应用程序是一个建立在外部产品之上的自定义 UI 层,集成了自定义业务规则,并提供了用于交互的界面。客户端是一个 UWP 应用程序,还有一些后端服务,用于在我们和第三方域之间进行转换。
因为大量依赖第三方,我们在进行微服务划分时遇到了一些问题,例如,为了让第三方的某个域起作用,应用程序必须在域之间做一些转换工作,让第三方域看起来像是我们的域的一部分。如果前端和第三方之间只有一个服务,那么这种转换还是可行的。当我们试图将域划分成多个独立的微服务时,域之间的转换工作带来了很多麻烦。比如,微服务划分是否要跟第三方保持一致,并在两边服务之间重复实现前端需求?或者根据自己的原则来划分微服务,并通过一个微服务从第三方的多个域获取数据?这两种方法都违反了微服务原则,并且会导致额外的耦合。
此外,我们经常要与外部方协同工作,因为一些特性要求双方都做出改动。实际上,外部方成了我们之外的另一个开发团队。如此紧密的合作意味着我们的发布流程必须与他们保持同步。 微服务的一个好处是每个团队都可以独立发布自己的服务,不需要与其他团队进行协调 , 但跨团队甚至是跨公司的协调发布流程导致我们无法享受到这些好处。
微服务的核心思想之一是打破“不同层由不同团队负责开发”的模式。在微服务架构中,每个团队需要处理与其业务相关的整个技术栈。对于我们来说,因为外部方是一家完全独立的公司,所以进行这种重构是不现实的。
我们无法在单体系统中找到可以被明显拆分成微服务的部分。于是,我们随意挑了几个领域模型,得到了一个需要创建的微服务清单,但在着手调研时,我们发现这些微服务之间存在很多共享的业务逻辑和隐式耦合。我们又进一步尝试将这些微服务再细分为更小的服务,但这样却带来了 更多耦合、无处不在的消息总线,以及潜在的通信大爆炸——一个服务需要与十个甚至更多的微服务通信。
这些微服务之所以耦合得很厉害而且难以拆分,主要是因为我们原先的单体架构只为一个业务提供服务。UI 应用程序的主要设计目标是将第三方基础应用程序的数据聚合在一起。为了方便用户,我们创建了跨领域的工作流,将分散的功能聚合在一起。
在整个过程,我们并没有正确理解应该怎样拆分微服务,而且低估了正确选择微服务边界的重要性。如果按照我们的方式来拆分,那么实现一个标准的功能需要同时修改多个微服务。每个功能都需要不同的微服务团队参与开发,这就导致单个微服务无法只由某个团队负责。
我们大约有 12 名开发人员在做这件事情,分布在两个功能团队和一个支持团队。但我们负责的工作是波动的,一个团队不会只负责开发应用程序的某个部分,两个团队同时修改同一处代码的情况并不少见,所以不能将某个微服务的所有权赋予某个团队。
康威定律指出,软件架构将以一种与组织和团队结构类似的方式增长。如果有很多独立的团队,这些团队可以负责不同的业务关注点,那么可以考虑采用微服务架构。但是,如果只有很少的团队,并且开发的是同样的功能,那么还是不要这么做。
因为有各种各样的问题,至少在 6 个月内,我们必须同时部署旧的单体应用和新开发的微服务,无法使用与微服务相关的工具,例如容器、Kubernetes、服务总线、API 网关等。没有这些工具,微服务之间的通信变得更加困难。
因此,我们在每个微服务中都包含了共享逻辑。因为未能正确拆分,所以出现了很多重复工作。例如,有一个特别复杂但很重要的业务逻辑,我们不得不在四个微服务中复制、粘贴和维护。
开发团队只对接下来 6 个月做什么有粗略想法,除此之外就没有其他东西了。业务经常发生变化,需求突然发生变化的情况并不少见。这种不确定性使微服务的开发变得更加困难,因为无法预测会出现什么新状况。例如,微服务之间的关系和耦合会一直增长吗?几个月后,我们需不需要花时间把它们重新连接在一起?
今年早些时候,我们已经试着进行微服务概念验证,但随着业务需求的变化,这被否决了。
时间安排得很紧,领导留的那点时间也就够把单体拆分成计划好的微服务,根本没有多余时间反思自己做了什么或者在必要时调整方向。我们在计划阶段就发现了很多问题和挑战,在实现阶段就更不用说了,开发团队因此陷入一片焦灼。
除了面临风险和时间压力外,负责设计和实现微服务的人之前没有相关经验。由于没有足够的标准工具可用,导致情况更加恶化,我们不得不自己实现基础设施平台。在与一些有微服务经验但没有参与我们项目的人交流之后,引发了更大的恐慌。他们建议的基础设施我们没有,他们还指出了我们在领域模型之间划分界限可能带来的后果。
到目前为止,我们的计划中包含了很多不得已的妥协,多少都偏离了标准的微服务模式。时间紧迫,没有专家指导,犯错成了家常便饭,我们以极高的代价换来血的教训。
当这些事情变得越来越困难,清晰的前行之路开始变得模糊。我们才意识到当初都不知道为什么要做这些事情,我们没有列出痛点是什么,也不知道做这些事情是否可以解决问题。更糟糕的是,微服务可能会带来新的问题。
我们开始分析这些问题:应该从重构中得到什么好处?要解决什么问题?我们试图通过无休止的会议搞清楚这些问题。在每一次休息时间,开发人员之间的每一次对话都在讨论和质疑微服务,但仍然无法得到答案。
事实证明,相比于微服务,我们确实有其他更紧迫的痛点需要解决,只是这些痛点在我们考虑迁移到微服务的过程中被忽视了,但我们没有足够的时间来解决这些痛点,所以我们最后既没有得到微服务的好处,也没能解决其他痛点。
在意识到并不清楚采用微服务的目的之后,我们开始研究微服务能够带来哪些好处。
在采用微服务架构时,开发团队拥有交付特性所需的整个技术栈的控制权,好处是可以减少与其他团队之间的协调工作,互不影响。
在采用单体架构时,开发任务的分配是不固定的,任何人都有可能分配到任意的任务。但如果每个团队可以拥有自己的服务,就可以在特定业务领域积累专业知识,理解特定领域的业务规则和需求。他们对自己的技术栈十分了解,在做出变更时更有信心。
在采用微服务时,开发者可以根据每个服务的性能需求进行伸缩。而在采用单体架构时,虽然也可通过添加更多服务器进行水平伸缩,但却不能让单体的每个组件进行独立伸缩。此外,细粒度的微服务可以更容易根据需要进行垂直伸缩。例如,有时可能希望多处理一些负载,而在处理性能问题时又需要一些额外的机会。
如果回滚某个功能,只需要修改单个微服务就可以直接回滚,不会影响其他团队的工作。此外,微服务架构有助于降低因单个微服务故障导致整个系统宕机的风险。
如果是一个大型系统,每次版本发布都很耗时,且伴随着风险。回归测试需要覆盖很多东西,从而限制发布节奏。开发人员可能需要经过很多人准许,并在所有参与版本发布的团队之间进行协调。微服务缩小了变更范围,减少了团队之间的协调工作量。开发团队可以根据自己的时间表发布版本,而不是被一个整体的节奏所束缚。
微服务的各个团队可以为要解决的问题选择最合适的技术,可以使用最新的技术,而单体系统则很难升级,只能停留在过时的技术平台上。
给大型应用程序升级框架从来都不是件有趣的事情,而且通常都伴有风险。当需要在多个团队间协调相互关联的变更时,一切都变得更加困难。而在采用微服务架构时,可以只升级必要服务,每次只让一个团队升级一个服务。
应用程序的不同部分以不同的速度发生变化,大部分组件可能几个月甚至几年都不需要改动,将很少发生变化的代码与频繁发生变化的代码分开可以降低意外回归带来的风险。
较小的服务更容易理解。一个服务只由一个团队负责开发,服务的设计风格就能够保持一致。小巧的微服务更容易进行重构。相比之下,单体架构可能会出现不一致,因为随着时间的推移,不同的团队会向单体系统中加入不一样的设计想法。
采用微服务有很多潜在的好处,但我们能捞到这些好处吗?
最终,单体架构中无法变更的部分和我们不得不做出的妥协导致无法获得这些好处。开发团队需要在不同的微服务之间协调,一些功能分散在多个共享的微服务中,这说明我们并没有获得微服务的隔离性好处:减少协调和专门化。
微服务之间的差异变成了缺点,而不是优点。开发每一个新功能都需要了解新的微服务以及需要其他团队做出哪些改动。我们对第三方的依赖严重阻碍了开发进程,让我们无法获得微服务伸缩性的优势。
采用微服务架构并不是没有代价,需要解决很多问题,而大部分在单体系统中已经解决过了,例如:日志、监控、异常处理、容错、回退服务间通信、消息格式、容器化、服务发现、备份、遥测、警报、跟踪、构建管道、发布管道、工具、共享基础设施代码、文档、伸缩、时区支持、API 版本控制、网络延迟、健康检查、负载均衡、CDC 测试、容错、在本地开发环境调试和开发多个微服务等。
更糟糕的是,因为没有现成的微服务平台,我们不得不自己完成上面这些事情。要转向微服务,我们确实存在痛点和困难,也确信无法从微服务架构获得任何好处,如果要支持微服务,还需要做一长串额外工作。
下图分别是我们当前的单体架构、计划中的架构和微服务架构。从结构上看,新的架构跟当前的单体架构很像,所有东西仍然紧密耦合在一起。
自从微服务架构大火之后,“单体”成了一个不太好的名词,好像“单体”就是糟糕的东西,而“微服务”就是好东西。但回望过去,我们的开发团队在开发单体系统时并没有遇到什么问题。开发和扩展都非常简单,已经有一个非常好的 CI/CD 管道,部署和回滚都非常容易。我们的分支管理和测试策略确保了很少会有问题进入到生产环境。
我们对微服务研究得越多,就越觉得它与技术无关,更多的是与团队的结构和工作模式有关。我们把微服务视为纯粹的技术问题,或许是我们错了?
除此之外,还有很多全局性的问题无法得到解答。
微服务迁移是件大事情,在几个月的时间里,所有开发人员都停止开发新功能,并在很多先决条件都还不满足的情况下开始拆解单体系统。为了做这件事而做,我们并没有想过是否真的有必要。
这不仅不是从 A 到 B 的问题,反而是一种倒退。我们先是创建微服务,然后搭建基础设施,还忽略了重组团队结构。如果我们先根据业务关注点重组团队,然后准备好基础设施,这就为微服务的自然出现做好了准备。一旦出现任何新的业务问题,就可以将它们直接放到新的服务中。
在拆分微服务时,我们必须预先确定每个微服务的大小。关于微服务大小这个问题,有很多相互矛盾的建议。有人建议微服务应该足够大,大到可以由一个团队负责开发;另一些人则建议微服务应该足够小,小到可以在脑子里浮现出服务结构,甚至小到可以在两周内重写;还有一些人建议,应该与业务大小相仿。
领导层决定基于我们的领域模型来拆分微服务,如果还有问题,就继续把它们拆分成更小的服务。这导致了上面提到的一些问题。事后看来,如果我们先把先决条件准备好,并让微服务自然而然地出现,最终可能会得到切合实际的微服务大小。
随着微服务发布日子的临近,我们的团队发现了越来越多问题。我们做出了更多妥协,微服务带给我们的好处也进一步消失殆尽。从开始实现微服务的第一个 sprint 开始,已经过去了四天,但我们仍然看不到什么收获,反而问题越来越多。我们召开了一次会议,不管领导层想要什么,关于这条路是否要继续走下去,答案都写在每个开发人员的脸上。最终,我们取消了转向微服务的计划。
因为之前把所有精力都放在了如何转向微服务上,所以没有花时间研究其他替代方案。但在放弃微服务之后,我们开始研究其他替代方案。最终,我们没有将单体拆分成微服务,而是将它拆分成多个项目。这种拆分为我们提供了一些额外的结构,我们可以更容易地看出哪里存在耦合和重复,没有额外的负担,也不需要面对微服务架构中存在的问题。
此外,这种结构让我们的领域模型变得更加清晰,能够更容易地评估哪些部分可以被拆分成微服务。如果某些部分被证明是一个合适的微服务候选对象,这个部分就可以从单体中剥离出来,成为一个微服务。
领导层决定转向微服务,但没有考虑到现状和需要面对的挑战。经过评估,我们发现微服务并不适合,我们需要做出大量妥协。这些妥协导致无法获得微服务的好处,所以转向微服务对我们来说是一种损失。
在决定转向微服务时,我们并没有评估团队结构等非技术方面的问题。经过几个月的调研和努力,我们最终放弃了这个想法,并用剩下的时间对“单体”进行了一些小的重构。
Why our team cancelled our move to microservices