架构风格
模块化单体是一个可部署单元,内部边界被强制约束。微服务是可独立部署的服务,各自拥有自己的数据。面向服务的架构介于两者之间,服务粒度更粗,且常常共享基础设施。我们根据有多少团队需要独立发布,以及各部分的扩展需求差异有多大,来选择相匹配的风格。
- 模块化单体
- 微服务
- 面向服务的架构(SOA)
- 六边形架构
- API 网关与 BFF(Backend-for-Frontend)
- 绞杀者模式(Strangler Fig)迁移
软件工程是这样一种实践:构建的系统要在多年时间里、在负载之下、在不同的人参与开发的情况下,始终保持正确和可演进。架构是其中日后改动代价高昂的部分:边界划在哪里,哪个组件拥有哪些数据,组件之间如何通信,以及其中一个失效时会发生什么。
大多数架构问题都是边界问题。共享同一个数据库的服务无法独立部署。互相直接访问对方数据表的模块,无法各自独立地分析和理解。一条由五次调用组成的同步链路,其可用性只等于这五次调用可用性的乘积。把边界划对,比在单体和微服务之间做选择更重要,而同一套领域分析对两者都适用。
Starone 的架构师根据约束来选择结构:团队规模、变更频率、一致性要求、负载特征和运维成熟度。本页介绍架构与工程实践;前端、后端和移动端工程各有独立的能力页面。
我们构建什么
模块化单体是一个可部署单元,内部边界被强制约束。微服务是可独立部署的服务,各自拥有自己的数据。面向服务的架构介于两者之间,服务粒度更粗,且常常共享基础设施。我们根据有多少团队需要独立发布,以及各部分的扩展需求差异有多大,来选择相匹配的风格。
领域驱动设计(DDD)通过研究业务而不是数据库 Schema 来寻找边界。限界上下文是这样一个范围:其中每个术语只有一种含义,并由一个团队负责;聚合则定义了哪些内容必须在同一个事务中一起变更。这些边界会成为模块或服务,是系统中最持久的决策。
事件把生产者与对其作出响应的消费者解耦,既在时间上解耦,也在可用性上解耦。不需要立即得到结果的工作转入队列,从而吸收突发流量并隔离缓慢的依赖。代价是最终一致性和更困难的调试,因此我们在解耦确有价值的地方使用事件,在调用方需要拿到结果的地方使用直接调用。
一旦系统跨越多个进程,网络会出故障,消息会重复到达,时钟也会不一致。我们对此做显式设计:按操作选择一致性级别,让写入具备幂等性,并在读写模型的形态或负载差异明显时用 CQRS 将二者分离。当变更历史本身就是需求时,例如账本和审计要求高的领域,才使用事件溯源。
容错意味着某个依赖发生故障时,只有一项功能降级,而不是整个系统受损。水平扩展要求服务无状态,状态存放在为此而设计的数据存储中。两者都要从一开始就设计进去,手段是超时、有上限的重试和负载削减,并通过负载测试和故障测试来验证。
架构只有在日常实践的保护下才能延续。我们采用分层测试策略,评审每一次变更,持续集成,并把 API 契约当作版本化制品来管理。重要的决策以架构决策记录的形式留存,让之后维护系统的人能够了解当初的理由。
微服务解决的是一个组织问题:许多团队按各自的节奏发布,系统的不同部分有不同的扩展方式或失效方式。为此付出的代价是:函数调用变成网络调用,出现分布式事务,每个服务一条发布管道,以及大得多的运维面。
模块化单体能以更低的成本获得大部分结构上的好处。模块有明确的公开接口,拥有自己的数据表,通过进程内调用或内部事件通信,边界则由构建规则或架构测试强制保证。只有一次部署、一个事务边界和一个调试的地方。
务实的路径是从模块化起步,在有具体理由时再抽取服务:某个模块需要独立的扩展特征、发布节奏、技术栈或隔离性。如果边界保持得干净,抽取只是机械性的工作。如果边界不干净,那么采用微服务只会得到一个分布式单体,兼具两者的缺点。
架构
客户端
API 网关 / BFF
领域服务
每个服务负责一个限界上下文
异步消息
数据存储
每个服务独立数据库,不共享表
平台
单个数据库可以提供原子事务。跨服务则没有这样的保证,而两阶段提交会以大多数系统无法接受的方式把各方的可用性耦合在一起。标准的做法是 Saga:一系列本地事务,每个事务发布一个事件,后续步骤失败时执行补偿操作。
有两种机制让这种做法变得安全。事务性发件箱把状态变更和待发出的事件写入同一个数据库事务,因此事件既不会丢失,也不会为已回滚的变更而发布。幂等消费者让重复投递不再造成危害,因为消息中间件在实践中是至少投递一次。两者都到位后,最终一致性就是可预测的。
并非所有操作都能容忍最终一致性。余额、库存预留和唯一性检查通常需要强一致性,所以我们把这些不变量保持在同一个聚合、同一个服务之内。
在分布式系统中,部分失效是常态。响应缓慢的依赖比彻底宕机的依赖更糟,因为调用方在等待期间会一直占用线程和连接,缓慢会向上游蔓延。
因此,每一次远程调用都有一个从调用方自身延迟预算推导出的超时,有次数上限并带抖动的重试,以及一个熔断器,用来停止对不健康依赖的调用。舱壁为每个依赖分配独立的连接池,使任何一个依赖都无法耗尽共享资源。系统过载时,会尽早丢弃低优先级的工作,而不是无限制地排队。
我们如何构建
架构工作从领域和约束出发,并尽早在可运行的代码中得到验证。
与实际执行业务的人一起梳理业务流程,绘制出限界上下文、关键实体和重要的事件。
把可用性、延迟、吞吐量、一致性和合规约束写成可度量的目标。它们对结构的影响比功能更大。
选择架构风格、数据归属和通信模式,并把每一项重要决策连同被否决的备选方案记录为 ADR。
在扩大范围之前,先交付一条贯穿所有层的薄切片,包括 CI、部署和可观测性。它能在改动成本还很低的时候,用现实检验架构。
在自动化测试、契约检查和代码评审的保护下交付小步变更,并配备一旦越过边界就让构建失败的架构测试。
上线前进行负载测试并注入依赖故障,之后随着真实流量数据的到来重新审视各项决策。
取舍
在确定架构之前,我们会与您的团队逐一厘清的问题。
生产就绪
安全是设计的一部分:在架构阶段做威胁建模,在每个服务边界强制执行认证与授权,校验输入,把密钥存放在专用的密钥保管库中,并在 CI 中扫描依赖。服务以其所需的最小权限运行。
服务无状态,在负载均衡器之后水平扩展;状态存放在数据库、缓存和消息中间件中,按需分区或复制。容量上限靠负载测试发现,而不是靠事故发现。
每个服务都输出结构化日志、指标和分布式追踪,并共用同一个关联标识。服务等级目标(SLO)定义了什么叫健康,告警则与用户能够察觉的症状挂钩。
每一次变更都要通过自动化测试,并构建出一个不可变制品,原样在各环境间逐级发布。发布采用滚动、蓝绿或金丝雀策略,数据库迁移保持向后兼容,功能开关把部署与发布分离开来。
集成通过版本化契约完成:调用使用 OpenAPI 或 gRPC,消息使用在 Schema Registry 中注册的事件。防腐层把领域模型与第三方系统和遗留系统隔离开,消费者驱动的契约测试则在发布前捕获破坏性变更。
根据工作负载和您现有的技术资产来选择。
从零到生产
想法
探索
系统架构
UX / 产品设计
应用工程
AI / 数据层
云基础设施
安全
质量保证与性能
部署
可观测性
持续演进
常见问题
从约束出发。我们考察领域边界、团队数量、所需的可用性和一致性、预期负载,以及组织运维所建系统的能力,然后选择能满足这些约束的最简单结构,并记录理由。
可以。我们先阅读代码并了解它的运维历史,为即将改动的部分补上测试,然后在开发功能的同时逐步改善结构。
够到团队可以有把握地修改系统。在实践中,这意味着针对领域逻辑的快速单元测试,针对数据库等真实依赖的集成测试,服务边界上的契约测试,以及针对关键用户路径的少量端到端测试。
架构决策记录、上下文图和容器图、API 规范、运维任务的操作手册(Runbook),以及一份能让新工程师把系统跑起来的 README。文档存放在代码仓库中,与代码一同变更。