在过去的四年中,使用微服务来构建应用程序似乎成了一种标准。大多数我所合作过的团队也对此表现出了不同程度的兴趣。
微服务所承诺的弹性、高可用、低耦合、敏捷,以及能够解决单体架构带来的问题,这些都是它流行的主要原因。
但是近段时间来,对于微服务的一些保留意见和注意事项似乎引起了人们的注意。
在这篇文章中,我重点想讨论的是微服务的应用,它的缺点是什么,以及在什么情况下应该慎重考虑使用微服务架构。
什么是微服务
在工业级别,关于微服务基本特征的定义比较一致。
这些特征可以总结如下:
-
微服务是一种应用于组件设计(服务如何分组)和部署架构(服务如何部署和通信)的模式。
-
微服务适用于创建具有“一定功能复杂性”的分布式应用程序。
-
各个服务必须小。
-
各个服务按功能划分,实现关注点分离。
-
各个服务保持自治和相互解耦,可以独立进行部署、版本控制和伸缩。
-
各个服务之间通过轻量级 API 和异步通道相结合的方式进行通信。
-
各个服务拥有独立的状态,并且只能通过服务本身来对其进行访问。
一个典型的微服务实现模式如下图:
图 1:典型的微服务实现模式
从上图中我们可以看到:
-
微服务中的每组服务有自己的前端(由一个 API 和一个可选的 UI 组件组成)、一个实现自身服务领域逻辑的域层以及独立的数据存储。
-
前端复合。将所有前端组件(UI 组件或 API)组合成一致前端(复合 UI 或 API 网关)。
-
一条事件总线,作为异步通信的骨干。
主要问题是,对于那些适用采用微服务架构的用例,人们对微服务的看法趋于一致。
这就是为什么我想采取相反的方法,并试图说明在哪些情况下,微服务可能不是最佳选择。
微服务的种种挑战
程序员知道种种优势,却对代价一无所知。
—— Rich Hickey (Clojure设计者)
实现微服务需要权衡利弊。由于本文的重点不是抨击微服务的缺陷,所以尽量简明扼要。
微服务很难被正确设计
这里有专门的一本书《Microservices AntiPatterns and Pitfalls》来记录它的缺陷。它需要非常非常多的迭代来完成一个令人满意的领域设计。
同时几个基本的和结构化问题没有直接的答案,往往需要调整和迭代,例如:如何水平切分关注点,如何共享数据以及以什么方式进行数据复制,如何管理报告,是否应该在服务中包括 UI 组件等。
微服务引入复杂性
微服务引入了一定程度上的复杂性,这些复杂性已经被详细地记录下来,其中最著名的是“微服务——不是免费午餐”。
图 2:成倍的增加了 API 的数量
微服务带来的挑战包括:
-
成倍的增加了 API 的数量。这使得变更代码变得困难,并引入了版本控制的复杂性,增加了服务功能性分解的难度。
-
引入了网络延迟。在组合服务时需要在可伸缩性和响应时间增加之间做出权衡。
-
鉴于 CAP 理论,处理横跨多个服务的事务非常复杂。和拥有单一数据库的单体架构不同,这些事务通常不是由基础设施处理。
-
调试分布式系统是复杂的(参见“微服务—不是免费的午餐”)。异步系统、服务间锁和竞态条件中产生的错误很难进行定位和排除。
尽管这些复杂性通过技术手段都可以克服,但是这需要技术人员付出额外的精力,不能让他们专注于实现那些更具价值的业务功能。
微服务需要组织架构发生转变
微服务需要组织转向自治的、跨职能的团队。根据康威定律,这是至关重要的一步。
这意味着前端和后端开发人员、数据平台工程师、QA、产品经理和操作人员必须在一个团队中互相合作。
图 3:微服务需要组织架构发生转变
这样的组织运作起来非常平滑。这是因为大多数依赖关系都存在于团队内部,而且由于优先级是相同的,因此都能够快速解决。
微服务需要过程和实践的改变
微服务需要过程和实践的改变。从偶尔发布几个大版本,到经常发布许多小版本。
从手动的资源供给,到基础设施即代码等自动化形式的资源供给。微服务架构的成功依赖于组织和流程改变的能力,而这往往是最难的。
图 4:微服务需要过程和实践的改变
微服务需要深入的技术栈
上面讨论的这些技术挑战意味着团队的技术集需要更加全面的拓展。
团队成员需要理解分布式系统、DevOps、基础架构即代码(IaC)、不同类型的数据库、前端组件化和复合化、单元测试、全自动发布、迭代、小版本发布计划、测试工具、多版本管理等等。
什么时候不应该使用微服务
应用程序规模太小
应用程序规模太小不足以证明微服务的合理性。当然,这个应用在未来可能会出现增长,直到整个领域添加到其中。
在这种情况下,当接近 RoI 阈值时,使用微服务。同样的道理也适用于那些小型团队。
图 5:代码行数与维护成本
领域不明确
领域不明确或不确定,使得领域模型不确定。比如,当产品被描述为“门户”时、当你开始创业准备做一个应用时、当一个 CRM 系统要管理订单甚至是有邮递外送时,这种问题就会凸显。
在这种情况下,单体结构要灵活得多。在晚上,当你独自将 CRM 领域转换成逻辑解决方案的时候,你可能会惊讶于利用 IDE 来进行重构所带来的便利性以及单元测试套件带来的安全感。
组织不能做出改变
组织不能改变以适应微服务。它仍然包含一个数据组、一个前端组、一个后端组等等。
康韦定律有利于将服务组成一个分层的微服务体系结构。你可以将图 1 与图 6 进行比较。
图 6:传统的组织形式来做微服务
由于不同的团队有不同的优先级,导致跨团队的依赖关系环环相扣,造成延迟和冲突。
缺乏理解
团队缺乏对微服务概念、DDD 或概念设计的经验和理解。虽然这可能算不上关键,但你应该仔细阅读相关文档并向有经验的人寻求建议。
一个体系结构设计不良的的产物往往是一个高耦合的分布式“单体架构”系统,以及随之而来的一切问题:网络的通信性和延迟、复杂性、隐藏的依赖、部署时产生耦合等等。
我们大多数人都喜欢学习新事物,但并非所有人都喜欢,况且学习需要时间和精力。
有时候甚至要不断地犯错误,包括大错误,然后才可能学到一些东西。甚至在某些情况下需要废弃初始的版本,然后从头开始。这些都是潜在的成本,必须要在做出决定之前考虑清楚。
管理层也需要尽早适应重构,在某些极端情况下甚至要接受从头开始一个项目。
经常失败。早点失败。快速失败。拥抱失败是走向成功的一种方式。不要害怕失败,要学会接受失败。
——Gary Burnison, CEO of Korn Ferry
接受失败不是一件容易的事情,因为沉没成本往往会错误指导你的下一步决策。
其他
团队不成熟,技术栈不适应微服务或者人员流动率高。因为系统的无序性(熵)会随着时间的推移而不断增大,代码的维护性也会随着时间的推移而下降。
而且由于一个更复杂的系统更难维护,这可能会加剧第 3.4 带来的问题。
困惑和压力往往会让人们退回到他们所熟悉的圈子。他们会“抄近路”把东西弄出来,或者走捷径来规避复杂性。
很快,这样产生的软件架构可能会被污染到技术组件、“核心”库、服务与服务之间的引用、编排器甚至“CSV 导入服务”,然后这些服务开始进入彼此的数据库。
接下来,有人会问“我们应该如何编排部署以管理服务之间的依赖关系?”这无异于是软件版本的地狱,这种系统也被称为分布式“单体架构”。
图 7:组件依赖
运行和调试的复杂性可能会降低开发的整体效率。跨服务的 Bug 调查和日志分析是非常复杂的。
这会带来一个问题,那就是这些工作只能交给那些团队中技术经验更加丰富的成员。
但是这些成员的关注点应该是如何保持系统长期稳定运行,而不是救火。简而言之,在这种情况下,微服务不但没有带来优势,反而会让整个团队为消极结果买单。
应该如何抉择?
所有软件系统都可以分解为两个主要元素:策略和细节。策略元素包含所有业务规则和过程。策略是系统的真正价值所在。
而细节是使人类、其他系统和程序员能够与策略进行通信的必备基础,但是这些细节都不会影响策略的行为。细节包括 IO 设备、数据库、Web 系统、服务器、框架、通信协议等等。
架构师的目标是为系统创建一个外形。这个外形将策略看做是系统中最重要的元素,同时做到细节与策略无关。这使得有关细节的决策可以延迟和推后。
—— Robert C. Martin, Clean Architecture
如果您正在构建的应用程序有一个相当清晰的领域,未来会演进到一个相当的规模,在项目开始就会配置大型的团队,并且你对团队的技术有信心,你在分布式设计方面有一些经验或者至少有一些素养,同时又能获得管理层方面对于失败和学习的支持和容忍,那么微服务会是一个很好的选择。
但是要注意有时布局候微服务可能会适得其反。如果你所处的场景与前面的描述相类似,那么使用一些更简单的架构作为开始可能更加明智。
比如一个单体架构或者一个分层架构(本身可能包含一些专门的服务)。大部分微服务架构能够解决的问题,其他方案也能解决。
低耦合、可伸缩性和前瞻性的系统来自于精心设计的应用程序架构。这种架构往往具有明确定义的边界和专门的数据存储。
微服务只能通过提供物理限制来满足这种场景。另一种创建物理限制的方法是通过组件(DLL、JAR)。组件架构和部署架构的选择应该尽可能的慎重。
容器和基础结构即代码(IaS)可以与微服务架构一起配合使用。事实上,它们往往更容易实现,因为网络是直接的,配置的数量不那么重要。同时容器也能让搭建和销毁环境变得更加容易,并可以加速开发环境的部署。
构建自动化和频繁发布可以通过增加关注点和减少在制品(Reduced WIP)、小批量(Small Batches)、原子化更改流(Atomic Change Flow)和减少协作来实现。
对于复杂的分布式系统,微服务无疑是一个很好的选择。但微服务并不是唯一的选择,对于微服务的诉求往往会促使人们做出采用它们的决定,而忽略了它带来的种种问题。
一个人应该考虑各种选择,进而做出一个有意识的、有素养的、理性的选择。