我曾经看到过一些文章,开发人员在文章中分享了他们的公司转向面向服务架构(SOA),最后以失败告终又迁移回单体架构的案例。当然,这样做并没有什么错。尝试新架构本来就是我们工作的一部分,看看新架构是否有效,如果无效就放弃。SOA 并不适合所有公司,如果不适合你的公司,那就坚持使用单体架构,如果适合,那就要用对它! 多年来,我们一直使用大单体代码库,于是形成了对某些事情的看法和偏见。如果我们在采用 SOA 时仍然无意识地坚持过去的习惯,可能就意识不到将会在哪些方面遭遇失败。
在本文中,我们将讨论在 SOA 架构中采用单体系统开发实践会存在哪些风险。
数据和架构高度耦合
想要在 SOA 世界中取得成功,就要避免数据和架构的高度耦合,这点很重要。每一个服务都应该建立在深思熟虑的抽象基础之上。在某种程度上,服务应该是独立的——理想情况下,它们不需要知道其他服务的存在。而在以下这些情况下,很容易创建出高度耦合的服务:
- 为了临时紧急用例,而构建了临时的服务。
- 抽象的成本可能很高。对于企业来说,耦合服务是阻力最小的一条路径。
- 有些产品的数据模型是中心化的,如果由数据模型来决定抽象的定义,那么服务就会存在某种程度的耦合。
如果不注意,我们可能会得到一个高度耦合的服务生态系统。下面是一些耦合的依赖结构:
左边:服务 A 调用服务 B,知道服务 B 需要调用服务 C,然后它再调用服务 C 获取结果。
右边:服务 A 依赖服务 B,服务 B 依赖服务 C,而服务 C 又依赖服务 A。这是一个依赖循环。
这些结构最终会变成分布式单体。到了某个时候,你会遇到以下这些问题:
- 你需要同时更新两个服务,但不知道先更新哪一个,因为它们彼此依赖。你可以先在其中一个服务中引入非中断的向后兼容性变更,更新它,然后再更新第二个服务,最后移除不需要的代码,但这样做不够“优雅”。
- 一个简单的逻辑可能会跨越多个服务。如果出了问题,只能由对多个服务都了解的人来诊断问题。
“糟糕”的依赖结构是分布式单体的征兆——开发单体的痛苦一点都不会少,面向服务的好处却一点都不会有。此外,高度耦合的服务通常会让你越陷越深。换句话说,如果不解决这些问题,随着服务生态系统的增长,情况会变得更糟。
数据耦合
当数据高度耦合时,你需要使用同步 API 和异步任务来保持数据同步。这些同步过程也可能以 Saga 的形式出现。但不管怎样,你不得不做这些事情:
- 在基础设施上大力投入,并进行大量的测试,以确保数据正确同步。
- 培训开发人员,这样他们就不会意外地做出一些会导致数据同步故障的变更。
- 在多个服务之间同步足够多的数据之后,开发人员肯定会犯错——而且可能是灾难性的错误。
- 在可见性和异常检测方面做很多工作,确保在同步过程出现中断时能够收到警报。当出现故障时(很可能会出现),你不得不去诊断问题并解决它们。根据系统要求,这两种方法的成本都非常高。
但是,如果服务生态系统的数据耦合程度很低或者没有耦合,就可以避免这些麻烦。
下一步
当你发现自己正处在这样的境地,你该怎么办?当然,并不存在放之四海而皆准的解决方案,这里只是提供一些建议:
- 首先,如果服务很稳定,并且没有人在修改它,那就不要管它了。你不会想成为那个打破这种“宁静”的人。
- 如果当前的架构出了问题,那么你要做的第一件事就是分析当前系统的维护成本、修改系统的成本以及新系统所节省的成本之间存在怎样的关系。做好这件事情并不容易,但如果去做了,你会惊讶地发现使用一些指标(比如交付时间、bug 个数或宕机次数、受影响的用户数量等)会给你带来些什么。在进行了彻底的分析之后,你会发现迁移可能是没有必要的。但是,如果分析之后确定需要进行迁移,那么就可以使用分析结果来获得管理层的支持。
- 在很多情况下,修改已有的系统是不现实的。迁移会影响企业的运营,成本可能很高。不过没关系,只要你确保系统的设计是正确的,那就可以继续这样下去。
单体系统开发习惯
在 SOA 世界里,沟通方式、团队、运营、部署和流程与单体架构世界是不一样的。我们开发单体系统使用的工具与开发面向服务系统使用的工具不一定都一样。如果开发人员(和企业)想要成功转向 SOA,那么旧工具和开发习惯也需要被新的工具和开发习惯所取代。
本地开发
在开发单体系统时,我们在本地机器运行大部分的基础设施。开发人员或多或少都会遇到“它无法在我的机器上运行”的问题。企业会提供引导解决方案来避免这个问题,让开发人员尽快恢复开发工作。大多数解决方案的目标是让基础设施更容易在本地机器上运行。
在开发面向服务的系统时,在本地机器上运行所有的东西将会遇到伸缩性问题。
- 在大多数情况下,开发机配置不高,无法在开发期间运行必需的服务。你的机器可能可以运行少量的大型服务,但总有一天配置会跟不上。
- 要在本地运行服务,开发人员必须知道如何运行(或者部署)不是他们开发的服务。
在本地运行多个服务的解决方案是一种单体思维,以这种方式来开发面向服务的系统可能会变成分布式单体。一些公司提供了本地服务开发解决方案,它们通常是这样的:
- 通过依赖注入和客户端开发库来模拟与其他服务的交互。
- 在云端运行一部分基础设施,在本地运行一部分基础设施,并将本地的基础设施与云端集成在一起。
端到端测试
端到端测试有两种形式:
- 自动化测试,通常会借助 CI/CD 管道。
- 手动测试,这是开发人员在提交代码之前和在进行代码评审时所做的工作。
单体代码库的端到端测试比较容易。在准备好适当的数据之后,就可以在本地机器上执行测试。
虽然我们已经进行了单元和集成测试,让测试人员在本地机器上进行端到端手动测试仍然会进一步提升我们的信心。随着服务生态系统的发展,服务会越来越多,想要在本地测试所有东西是不可能的事情:
- 准备数据可能需要很长时间。
- 难以模拟系统(如消息代理、异步作业队列等)交互。
- 开发机无法运行所需的基础设施。
SOA 架构的端到端测试则不太一样。进行本地端到端测试的成本非常高,而且不具备伸缩性。
SOA 之所以流行,是因为它可以加快迭代速度。如果你花了大量时间在本地测试上,那就无法利用 SOA 的优势。你需要放弃在本地测试一切的想法。
你的测试策略取决于你将要采用的本地开发解决方案。下面是一些无需进行本地测试就可以发布代码的方法:
- 启用功能开关,在将功能发布给所有用户之前就可以在生产环境中进行测试。
- 金丝雀部署、影子部署、红黑部署,等等。
- 在发布到生产环境之前对变更进行压力测试。
当然,虽然你放弃了本地测试,但仍然可以通过以下这些途径来提升信心:
- 成熟的可观测性。
- 警报、工作簿和回滚过程,可在发生故障时进行回滚。
调试
另一方面,如果测试策略发生了变化,那么调试策略也需要做出改变。
在单体系统中,一个堆栈跟踪信息就足以让我们着手诊断问题。堆栈跟踪信息为我们指明了方向,我们层层深入,直到找到问题根源。堆栈跟踪信息和传统的调试工具通常也可以用来调试 SOA 架构中出现的问题,但对某些问题是毫无用处的:
- 临时的网络错误。
- 多个服务之间的数据同步问题。
- 不正确的配置——连接超时、读写超时、工作进程数量、伸缩配置等等。
除了看代码找问题,你的调试工具箱中也需要包括这些:
- 用于下载和筛选访问日志的脚本。
- 分布式跟踪,帮你了解用户请求的生命周期。
- 带有 CPU、内存和 P99 指标的仪表盘,用于捕获没有抛出堆栈跟踪信息的问题。
- 用于模拟生产环境负载的策略。
结论
在采用 SOA 架构时很容易把系统开发成分布式单体。我们要避免将单体系统的开发习惯带到 SOA 架构中。以下是一些需要注意的症状:
- 高度耦合的数据或架构。
- 糟糕的本地多服务开发策略。
- 糟糕的测试策略,包括在本地测试所有东西。
- 过时的调试工具,无法诊断网络和跨服务问题。
单体系统并不是坏东西。很多公司为了转向 SOA 而大肆投入资源,但却没有意识到,在某些情况下,单体可能更适合它们。无论你是坚持使用单体还是采用 SOA,都要避免把系统变成分布式单体——这才是最糟糕的。
关注我并转发此篇文章,私信我“领取资料”,即可免费获得InfoQ价值4999元迷你书!