3.2

查看英文版

3.2 架构风格与模式

概述与动机

架构风格是组织一个系统的一种宽泛、可复用的形态:系统如何被拆解、各部分如何通信,以及边界划在哪里。选择哪种风格,是大型组织能做出的最具后果性(也最容易被误解)的决策之一。这个选择太多时候是跟风做出的(“大家都在用微服务”),而不是根据团队、领域和运维现实的真实约束来决定的。结果你往往会陷入两种混乱局面之一:一个组织根本运维不起来的分布式系统,或者一个没人敢安全改动的、纠缠不清的单体应用。这都不是风格本身的错,而是风格与所处情境不匹配的结果。

对大型开发团队而言,架构风格之所以格外重要,首先是因为康威定律:一个系统的结构往往会映射出构建它的组织的沟通结构。所以架构风格同时也是一项组织设计决策。把一个系统拆成多个服务,实质上是在拆分团队、所有权和值班责任。拥有数百名工程师的企业负担得起(也往往需要)能够独立部署的细粒度服务,因为正是这种独立性让众多团队能够互不阻塞地各自发布。若把同一套模式硬套在一个小团队身上,它就只会继承所有运维上的负担,却享受不到任何组织层面的好处。

政府和企业场景还会叠加更多约束:系统生命周期长、变更管控严格、有采购周期、要与根深蒂固的记录系统集成,还要满足可审计性。这些因素都更偏向那些能让边界保持清晰、依赖关系易于查验的风格。本章将纵览各主要风格:从单体到微服务,带有 CQRS(命令查询职责分离)和事件溯源的事件驱动架构,服务网格和网关模式,无服务器架构,以及六边形架构和整洁架构这些内部纪律。更重要的是,它能帮你判断每一种风格在什么情况下才真正适用。

另请参阅:第 2.2 章(软件设计原则,包括领域驱动设计)、第 3.1 章(架构基础)以及第 3.3 章(分布式系统)。

关键原则

  • 风格应当追随各种作用力,而不是追随潮流。 根据团队规模、领域复杂度、负载和运维成熟度来选择,而不是因为某项技术正流行。
  • 耦合才是真正的敌人,可部署单元的数量不是。 一个模块化良好的单体,胜过一堆搅在一起的分布式泥团。
  • 分布式是你为独立性付出的成本。 只有当独立部署、独立扩容或故障隔离所带来的价值,超过跨服务网络调用、部分故障和数据一致性所带来的成本时,才值得拆分。
  • 边界应当追随业务领域。 让服务和模块与限界上下文(每一个都是拥有自己明确边界的自包含领域模型)对齐,而不是与技术层次对齐。
  • 无论外部风格如何,都要把内部设计好。 六边形/整洁分层能让业务逻辑在任何风格下都独立于框架和基础设施。
  • 康威定律是无法回避的,那就善用它。 把团队边界和架构一起设计。
  • 从比你以为需要的更简单的方案开始。 你可以从一个良好的模块化单体中抽取出服务;但要把一个过早分布式化的烂摊子重新收拢回来,则要困难得多。

建议

默认使用模块化单体,用证据驱动拆分

大多数系统一开始都应该作为一个单一的可部署单元,配合强内部模块边界:接口清晰、不越界访问其他模块的数据、并强制执行依赖规则。这样你能获得简单的事务处理、容易的重构,以及只需部署和观测一个东西的便利。只有当你有具体理由时()某部分必须独立扩容、某个团队需要按自己的节奏部署、某个需要隔离的故障域,或某项与系统其余部分不同的技术需求()才把一个模块拆分成独立服务。拆分的时候,要沿着限界上下文的边界拆,这样每个服务才能拥有自己的数据,并对外暴露一个稳定的契约。

了解微服务在什么时候才真正物有所值

微服务能给你独立可部署性、独立扩容能力、故障隔离,以及混用不同技术的自由。作为交换,它要求成熟的 CI/CD(持续集成与持续交付)、自动化基础设施、分布式追踪、服务发现,以及一种值班文化。诚实地问问自己:你的组织能否在生产环境中可靠地运行数十个独立部署的服务?如果平台和运维成熟度都还不到位,微服务只会成倍地增加你的故障模式,却带不来它本该有的好处。许多组织的最佳实践是围绕几个主要领域构建少数几个粗粒度的服务,而不是养一大群细小的服务。

在解耦和异步性真正有回报的地方使用事件驱动架构

事件驱动架构让生产者能够在不知道谁在消费的情况下发出事实。这换来了松耦合、应对负载尖峰的缓冲空间,以及轻松添加新消费者的能力。在工作流天然是异步且反应式的地方使用它。CQRS(命令查询职责分离)把写模型与一个或多个读模型分离开来,这在读写负载或形态差异悬殊时很有帮助。事件溯源把状态存储为一份只能追加的事件日志,而不是当前状态本身,这为你提供了完美的审计轨迹和”时间旅行”能力。这对金融和政府领域尤其强大,因为在这些领域”我们是怎么走到这个数值的?“是一个法律问题;但它也会在事件版本管理、重建投影视图以及推理最终一致性方面带来真实的复杂度。要有意识地采用这些模式,而不是想当然地默认使用。

运用网关、BFF 和网格模式来管理众多服务

API 网关为外部客户端提供单一入口,负责身份认证、限流、路由和 TLS(传输层安全协议)终结。前端专属后端(BFF)为每种客户端类型(Web、移动端、合作伙伴 API)提供各自量身定制的聚合层,让你避免一个臃肿的、一刀切的 API。服务网格把横切关注点(双向 TLS、重试、超时、流量切换和遥测)下沉到一个边车基础设施层,这样应用团队就不必各自重复实现它们。只有当服务数量多到逐个服务处理这些关注点变得难以管理时,才引入服务网格。如果只有寥寥几个服务,服务网格带来的运维负担会超过它的价值。

诚实地权衡无服务器架构

函数即服务和托管式无服务器平台把服务器管理从你手中拿走,能够缩容到零,并按使用量计费,这对突发性、事件驱动或低基线负载的工作负载,以及小团队而言非常合适。其代价也是真实的:冷启动延迟、执行时间和资源限制、本地测试更困难、可能的供应商锁定,以及在持续高流量下成本可能超过预置基础设施。在无服务器的经济性和运维简便性明显占优的地方使用它,不要因为一时兴起就把稳定、高吞吐的核心系统硬塞进无服务器架构。

让每个服务内部的业务逻辑保持整洁

无论外部风格是什么,都要用六边形架构(端口与适配器)或整洁架构来保持内部的整洁:业务规则位于中心,只依赖抽象;框架、数据库和消息传递都作为可替换的适配器处于边缘。这能让你宝贵的领域逻辑在不依赖基础设施的情况下也可测试,并且能在技术更迭时保持可移植()对于那些将比好几代框架都更长寿的政府和企业长期系统而言,这是决定性的优势。

权衡:优点与缺点

风格最适用场景优点缺点
模块化单体大多数系统,尤其是早期阶段运维简单,事务处理和重构容易单一部署单元;作为一个整体扩容;存在被侵蚀的风险
微服务团队众多、规模庞大、平台成熟独立部署/扩容,故障隔离分布式复杂度、数据一致性问题、运维成本高
事件驱动 / CQRS / 事件溯源异步工作流、审计需求、读写差异悬殊松耦合、可审计性、可扩展的读操作最终一致性、事件版本管理、调试更困难
无服务器突发性或低基线、事件驱动的工作无需管理服务器,缩容到零,按使用付费冷启动、限制、锁定,持续高负载下成本较高

反复出现的主题是:你用运维和认知复杂度换取了灵活性和独立性。分布式和事件驱动风格放弃了单一调用栈和单一事务的简单性,换来了独立扩容、独立部署和独立故障的能力。这笔交易在规模化场景、配合成熟平台时是划算的;没有这些前提,则是灾难性的。内部纪律(六边形/整洁架构)几乎总是值得的,因为它们成本很低,又能让你在日后改变风格时保留选择余地。

与团队讨论的问题

  1. 在拆分下一个服务之前,你们会同步拆分拥有它的团队吗?谁有权做出这个决定? 康威定律意味着服务边界本质上就是团队边界,所以一个组织架构图没有支持的拆分,只会产生一个分布式单体:两个可部署单元、一条发布流水线、共享的值班安排。在大型企业中,重塑团队的权力通常在工程部门之上,涉及汇报线、财务和人力资源,这正是架构设计和组织设计必须一起决策的原因。带上证据来讨论:提议中的服务是否有一个能端到端拥有它、能配备自己的值班人员、能按自己节奏部署的团队?如果答案是否定的,要么为这个团队争取拨款,要么把这项能力保留为单体中的一个模块。拆分代码却不拆分所有权,只会买到分布式的全部成本,却买不到任何一点独立性。

  2. 你们的每个服务今天真的能独立部署吗,还是暗地里都是齐步走发布的? 分布式单体是本章中最糟糕的结局:你付出了跨服务网络调用、部分故障和数据一致性的全部代价,却仍然无法在不影响其他服务的情况下单独发布一个服务。典型的征兆是共享数据库、强制协调升级的共享库,以及必须把整个体系一起跑起来才能通过的集成测试。对大型团队而言,这会悄悄限制吞吐量的上限,因为尽管架构图上看起来是独立的,每个团队实际上都在排队等待同一趟发布列车。挑一个最近的改动,数数为了让它安全上线,有多少个服务必须一起部署;如果一个只涉及单一能力的改动却要求这个数字大于一,那说明你的边界划错了。修复方法通常是给每个服务配上自己的数据和一份稳定的、带版本管理的契约,而不是再增加更多服务。

  3. 你们现有的哪些服务拆分已经不再有回报,你们愿意把它们重新合并回去吗? 本章中最进阶的一个习惯,是把风格决策当作可逆的:出现驱动因素时就抽取出去,驱动因素消失时就重新合并回来。大多数组织只会一路拆分下去,于是纳米服务和”话痨”实体服务不断累积,直到编排和网络开销远远超过每个服务本身所做的工作。留意那些总是一起部署的服务,那些因为某张数据库表而不是某项业务能力而存在的服务,以及那些网络跳转已经主导了请求延迟的服务。在人员编制和预算都受到严格审视的企业和政府体系中,把两个瘦弱的服务重新并回一个粗粒度的服务,是一个正当的、能节省成本的举措,而不是承认失败。要像看待拆分一样,坦率地把重新合并摆到台面上讨论,并用同样的证据来决定这两种选择。

  4. 你们的平台和值班成熟度,真的支撑得起你们正在提议的风格吗?在承诺之前,你能说出具体的差距在哪里吗? 微服务、服务网格和事件驱动骨干,只有建立在成熟的 CI/CD、分布式追踪、服务发现,以及一种能够推理部分故障的值班文化之上,才能兑现它们的好处。大型组织往往会在架构论坛上先定下目标风格,等到数十个服务已经上线、每次事故都要花上数小时才能诊断出来的时候,才发现平台缺失。要把独立部署和独立扩容的吸引力,与”凌晨三点谁来处理”这个冷静的问题放在一起权衡:同一次拆分,既让团队获得了并行发布的自由,也让每个团队必须理解的故障模式成倍增加。带上一份诚实的清单来讨论:当前的部署频率、平均恢复时间、你们是否具备跨服务边界的追踪能力,以及一个团队实际上能够运维多少个服务。在企业和政府场景中,还要加上你们所缺乏的平台能力的采购和招聘周期,因为一种假设你们拥有服务网格和平台团队、而你们实际上并未为此拨款的风格,其实是在计划运行一套无法维系的体系。

  5. 对于领域的哪些部分,完整的事件溯源审计轨迹是法律上的必需,而不只是一种便利?谁有权做这个决定? 事件溯源和 CQRS 为你换来了完美的、可重建的历史记录,以及能够独立扩容的读模型,但代价是事件版本管理、投影重建,以及在系统整个生命周期中都要推理最终一致性。把它应用在一个从不需要审计轨迹的领域,那份复杂度就是纯粹的税负;而在一个”我们是怎么得到这个数值的”本身就是法律问题的领域中却不使用它,它的缺失就是一次合规失败。相互竞争的考量一边是可审计性和查询可扩展性,另一边是调试难度和开发者认知负担,所以这个决定应该交给既理解监管义务、又理解运维负担的人,而不是交给对这个模式最热衷的人。带上具体的法律或合同上的留存与重建要求、预期的事件量,以及对版本管理和投影工作量的诚实估算来讨论。在金融、税务和政府领域,多年后重建某个决策可能是一项法定义务,因此要指定一位负责的所有者,由他签字确认某个限界上下文是否需要一份不可变的事件日志。

  6. 你们打算如何防止模块化单体逐渐被侵蚀,从而让日后的抽取仍然成本低廉?又是什么在强制执行这些边界? 从模块化单体起步的整个理由,都建立在一个承诺之上:干净的内部边界能让日后的服务抽取变得可负担。然而只要一个截止日期诱使某个模块伸手去碰另一个模块的数据,这些边界就会悄悄腐蚀。对于一个有众多贡献者的大团队来说,仅凭良好的意愿和代码评审是守不住这条防线的;没有强制机制,单体就会悄悄变成这种风格原本要避免的那种大泥团。要权衡强制执行的依赖规则和模块接口所带来的摩擦,与多年后才发现没有一条边界是真的、每一次抽取都意味着要解开一团共享状态所带来的代价。带上证据来:模块边界是由构建工具、静态分析或包结构强制执行的,还是仅仅是被最近六次合并都无视了的文档化约定?在必须经受严格变更管控、并跨越好几代框架而存活下来的长期企业和政府系统中,要把边界的强制执行当作一项可审计的控制措施,这样”日后再分布式化”这个选项才是你真正保留下来的,而不是你以为自己还拥有的。

行业视角

初创企业。 默认使用单一的模块化单体,抵御住转向微服务的诱惑,因为你最稀缺的资源是工程注意力,而一群服务带来的运维税,是你在达到产品市场契合度之前根本负担不起的。保持干净的模块边界,以便日后能够抽取出去;在缩容到零和按使用付费适合你突发性、低基线负载的地方,使用无服务器架构。只有在出现具体驱动因素时()比如一个突发性的通知发送模块()才拆分出恰好一个东西,绝不提前动手。

小型企业。 没有平台团队、预算又紧张,优先选择单体或托管平台上的少数几个粗粒度服务,购买托管基础设施,而不是自己搭建服务网格、追踪系统和服务发现。把无服务器和托管数据库当作彻底避免自己运行服务器的一种方式来权衡,并对一种没人能够承担其运维负担的分布式设计保持警惕。正确的架构,是一两个人真正能够部署、观测和恢复的架构。

企业。 真正的问题在于众多团队和康威定律:让粗粒度的服务与限界上下文和团队所有权对齐,并有意识地投资于让分布式变得安全的平台(CI/CD、追踪、服务网格和服务发现)。把网关、BFF 和内部整洁架构模式标准化,让各团队不再重复发明它们,并把服务的抽取和重新合并当作基于证据的产品组合决策来治理,而不是局部偏好。要为每一次拆分明确编列运维成本预算,因为在你们这样的规模上,分布式单体这种失败模式代价高昂,而且很难收拾。

政府。 系统生命周期长、变更管控严格、采购周期漫长,以及可审计性,共同塑造了这个选择:优先选择边界明确、可查验,契约能比供应商和几代框架都更长寿的风格。当重建一个面向公民的决策是一项法定义务时,事件溯源的复杂度就是值得的,所以要有意识地把它用在核心账本上,并在每个服务内部保持整洁架构,把随每一次预算变化而变化的规则隔离开来。把摆脱专有无服务器或供应商平台的可移植性和退出能力,当作采购要求,而不是事后才想起的补充条款。

示例

初创企业。 一家四人规模、正在构建排班产品的初创公司,因为一位竞争对手写博客宣传微服务而感到压力,想一开始就上微服务,但他们抵住了这份诱惑。他们把排班、计费、通知作为一个可部署单元中三个各自独立的模块,构建成一个带有清晰内部边界的模块化单体,这样一位工程师就能在本地运行整个系统,一次发布只需一次推送。随着产品逐渐站稳脚跟,只有那个在突发负载下要向邮件和短信分发消息的通知发送模块,被抽取成了独立服务。在他们还在寻找产品市场契合度的阶段,他们没有背负一打服务所带来的任何运维税。

企业。 一家大型电商公司最初也是从模块化单体起步。随着流量增长、团队增多,它把负载最高、演进最独立的几个领域(商品目录、购物车、结账和搜索)抽取成了各自独立的服务,每个服务都拥有自己的数据。结账服务通过一条事件骨干发出事件,供库存、履约和分析系统消费,这样新的消费者(欺诈检测、会员忠诚度)就可以在不触碰结账服务的情况下接入。一个 API 网关负责身份认证和限流,一个 BFF 为移动端量身定制负载数据。剩下流量较低的领域仍留在单体之中,避免了不必要的碎片化。

政府。 一家税务机构为核心账本构建了一个基于事件溯源的评估平台,因为纳税人应缴税额的每一次变动,都必须能在多年后被重建并接受法律审计。命令(申报纳税、缴纳税款、发出调整)产生不可变的事件,读模型为税务官员和公民投影出当前余额。CQRS 让面向公众的查询端能够在报税高峰季独立扩容,而不会把写入端置于风险之中。在内部,每个服务都遵循整洁架构,所以随每一次预算变化而变化的评估规则,得以与持久化和消息传递技术相隔离。

商业理由:动机、投资回报率与总拥有成本

架构风格选择所牵涉的资金规模巨大,因为这个决策一旦做出,逆转的代价高昂。过早采用微服务,会通过平台搭建、重复的基础设施、分布式调试和更沉重的运维负担而推高总拥有成本()这些成本会伴随系统的整个生命周期。而拒绝拆分一个真正已经不堪重负的单体,会限制交付吞吐量的上限:各团队排队等待同一次共享发布,而每一次改动都会让整个系统承担风险。投资回报率的讨论,本质上是关于把运维支出与组织需求相匹配的讨论。

向领导层论证时,要谈吞吐量和风险,而不是技术本身。独立可部署性意味着更多团队能够并行发布、交付周期更短:这是可衡量的业务速度。故障隔离意味着更少的整体停机事故和更小的影响范围:这是可衡量的可用性和声誉保护。但同样也要诚实地面对每种风格所要求的平台投资:服务网格、追踪能力和 CI/CD 成熟度是前提条件,而不是可有可无的附加项,它们的成本理应计入总拥有成本。对许多组织来说,眼下最廉价的路径是一个模块化良好的单体,配上干净的内部边界,让日后的抽取变得低成本。这为你买下了”日后可以分布式化”这个选择权,而不必在还不需要的时候就为它付费。

反模式与陷阱

  • 分布式单体。 必须一起部署、共享一个数据库的多个服务:付出了分布式的全部代价,却没有得到任何独立性。
  • 纳米服务。 服务粒度细到编排和网络开销远远超过它们所完成的工作。
  • 没有平台支撑的微服务。 在还没有 CI/CD、追踪能力和值班成熟度之前就先拆分,导致故障模式成倍增加。
  • 实体服务。 按数据库表拆分(“用户服务”、“订单服务”),而不是按业务能力拆分,导致每一次操作都需要一堆”话痨”式的跨服务调用。
  • 到处都用事件溯源。 把它应用到不需要审计轨迹的领域,白白付出复杂度的代价,却没有任何好处。
  • 网关变成了单体。 把业务逻辑塞进 API 网关,重新制造出一个中心瓶颈。
  • 与框架耦合的核心。 业务逻辑与 Web 框架或 ORM(对象关系映射)框架纠缠在一起,使得测试和技术更迭都变得痛苦。

成熟度模型

  • 第 1 级,启动: 风格的选择靠潮流或偶然。你要么有一个纠缠不清的单体,要么有一个意外形成的分布式烂摊子,边界追随的是技术层次或历史,而不是领域,拆分是在出问题时被动发生的。
  • 第 2 级,发展: 一些团队开始在单体内部有意识地划分模块边界,或搭建了几个粗粒度的服务,一些横切关注点得到了一致的处理。实践参差不齐:一些团队让服务与限界上下文对齐,另一些团队仍按数据库表拆分,抽取仍然是临时性的。
  • 第 3 级,标准化: 一套有文档记录的方法在整个组织范围内得到强制执行:服务与限界上下文对齐并拥有自己的数据,网关和 BFF 模式用在恰当的地方,内部整洁或六边形分层是标准做法,每一次拆分都要求说明明确的驱动因素。模块边界由工具强制执行,而不仅仅是约定俗成。
  • 第 4 级,管理: 风格决策相对于基线被衡量和控制。你追踪每个服务的部署频率和交付周期、平均恢复时间、一次典型改动需要多少服务一起部署,以及网络跳转带来的延迟,并将每一次拆分的成本与它本应换来的独立性进行比较。证据而不是偏好,决定着一条边界能否存续,向分布式单体演变的迹象会被指标捕捉到,而不是等到一次停机事故才被发现。
  • 第 5 级,编排: 架构与组织设计相互整合,并持续调整。一个成熟的平台(CI/CD、追踪能力,以及在必要时的服务网格)让分布式化和重新合并的成本都很低,各团队会在出现驱动因素时常态化地抽取服务,在驱动因素消失时把服务合并回去,风格选择也会随着团队拓扑、负载和整个体系的风险图景变化而不断重新平衡。

讨论话题

  1. 你们系统中的哪个部分,单体反而是一种优势?哪个部分又是真正的瓶颈?
  2. 什么样的具体驱动因素才能证明抽取你的下一个服务是合理的?你能在动手构建之前就说出它是什么吗?
  3. 你们的组织是否具备微服务所要求的运维成熟度?缺的是什么?
  4. 对于领域中的哪些部分,完整的事件溯源审计轨迹是法律或业务上的必需,而不只是锦上添花?
  5. 你们目前的服务边界与团队边界的映射程度如何?这种一致性是在帮助你们,还是在拖累你们?
  6. 如果明年不得不更换 Web 框架或数据库,你们需要重写多少业务逻辑?

关键要点

  • 根据真实的作用力(团队规模、领域、负载、运维成熟度)来选择架构风格,而不是跟风。
  • 对大多数系统而言,模块化单体是正确的默认选择;只有出现具体驱动因素时,才沿着限界上下文的边界抽取服务。
  • 微服务用运维和认知复杂度,换取独立部署、独立扩容和故障隔离;它们需要一个成熟的平台。
  • 事件驱动、CQRS 和事件溯源以最终一致性和版本管理复杂度为代价,换来解耦和可审计性;要有意识地采用它们。
  • 网关、BFF 和服务网格能驯服众多服务组成的体系,但也会增加负担;在规模真正需要时才引入它们,而不是提前引入。
  • 在每个服务内部运用六边形/整洁架构,让宝贵的业务逻辑保持可测试,并能在技术更迭中长久存续。

参考资料与延伸阅读

  • Sam Newman, Building Microservices and Monolith to Microservices
  • Chris Richardson, Microservices Patterns
  • Eric Evans, Domain-Driven Design
  • Vaughn Vernon, Implementing Domain-Driven Design
  • Robert C. Martin, Clean Architecture
  • Alistair Cockburn, “Hexagonal Architecture (Ports and Adapters)”
  • Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns
  • Martin Fowler, Patterns of Enterprise Application Architecture(以及关于 CQRS 和事件溯源的文章)
  • Matthew Skelton and Manuel Pais, Team Topologies