一次讲清微服务架构:服务拆分、通信、数据一致性与工程落地
一次讲清微服务架构:服务拆分、通信、数据一致性与工程落地
提到微服务,很多人首先想到的是:
- 把一个项目拆成很多服务;
- 每个服务提供一组 API;
- 用 Docker 打包;
- 用 Kubernetes 部署;
- 服务之间再加一个消息队列。
这些都可能出现在微服务系统里,却不是微服务的本质。
一个系统即使有几十个服务,如果每次上线仍要一起发布、多个服务直接修改同一批数据、一次需求改动要协调所有团队,它依然可能只是一个“分布式单体”。
真正需要回答的问题是:
系统是否围绕业务能力形成了清晰边界,并让各部分能够独立开发、部署、扩展和承担运行责任?
本文将从单体与微服务的选择开始,依次讲清服务拆分、服务通信、数据一致性、容错、可观测性、测试发布,以及如何从一个模块化单体渐进演化到微服务。
一、架构到底是什么
架构不是一个框架,也不等于接口设计。它描述的是系统的整体组织方式,包括:
- 系统由哪些部分组成;
- 每个部分负责什么;
- 各部分如何通信;
- 数据由谁拥有;
- 请求从哪里进入;
- 应用如何部署、扩容和回滚;
- 某个部分失败时系统如何响应;
- 出现问题后如何定位;
- 身份、权限、配置和密钥如何管理。
因此,微服务不是一套固定技术栈,而是一种组织复杂软件及其团队的架构风格。
二、什么是微服务
微服务架构把应用按照业务能力划分为一组自治服务。每个服务具有相对清晰的职责和数据边界,通过 API 或消息与其他服务协作,并能够独立开发、测试、部署和扩展。
例如,一个电商系统可能包含:
- 用户服务;
- 商品服务;
- 购物车服务;
- 订单服务;
- 支付服务;
- 库存服务;
- 物流服务;
- 通知服务。
重点并不是“服务小”或“服务多”,而是下面几项约束:
- 服务围绕业务能力建立边界;
- 服务内部高内聚,服务之间低耦合;
- 一个服务可以独立发布,而不要求其他服务同时上线;
- 数据有明确的所有者;
- 服务故障不会不受控制地向全系统扩散;
- 团队能够独立负责服务的开发和运行。
微软的微服务架构指南同样强调业务能力、限界上下文、独立部署和服务私有数据。也就是说,微服务首先是边界和自治问题,其次才是框架与基础设施问题。
三、单体、模块化单体与微服务怎么选
很多系统真正需要比较的不是“单体还是微服务”,而是下面三种形态。
| 维度 | 普通单体 | 模块化单体 | 微服务 |
|---|---|---|---|
| 部署单元 | 一个 | 一个 | 多个 |
| 模块调用 | 进程内调用 | 受约束的进程内调用 | 网络调用或消息 |
| 数据管理 | 经常共享 | 可划分逻辑边界 | 原则上由服务各自拥有 |
| 本地开发 | 简单 | 较简单 | 较复杂 |
| 事务 | 本地事务为主 | 本地事务为主 | 经常需要最终一致性 |
| 独立扩容 | 较弱 | 较弱 | 较强 |
| 独立发布 | 不支持 | 通常不支持 | 核心目标之一 |
| 运维成本 | 低 | 中低 | 高 |
| 适合阶段 | 小型或早期系统 | 业务增长但边界仍在形成 | 复杂领域、多团队、独立演进需求明确 |
1. 普通单体
单体应用把主要功能放在一个部署单元中。它并不天然落后,反而具有明显优势:
- 启动和调试简单;
- 调用不经过网络;
- 本地事务容易实现;
- 部署链路短;
- 小团队协作成本低。
问题通常不在“单体”二字,而在于内部缺少边界:模块随意互相调用、数据表被到处修改、业务规则散落在各层。
2. 模块化单体
模块化单体仍然整体部署,但内部按业务模块组织,并约束模块之间的依赖和数据访问。
它保留了单体的开发、测试和事务优势,又为未来拆分服务建立了边界,是很多新项目更稳妥的起点。
3. 微服务
微服务把部分模块变成独立的部署和运行单元。它用更高的分布式系统成本,换取:
- 独立发布;
- 独立扩容;
- 故障隔离;
- 团队自治;
- 不同业务能力独立演进。
因此,选择微服务之前,必须先确认这些收益是否真的存在。
四、微服务解决什么问题
微服务主要适合以下矛盾逐渐突出的系统。
1. 发布节奏互相阻塞
一个小功能也必须重新构建和发布整个应用,多支团队的变更长期挤在同一条发布流水线上。
服务独立部署后,团队可以按各自节奏交付。但前提是接口保持兼容,也不能存在“名义独立、实际必须联动发布”的隐藏依赖。
2. 不同模块的扩展需求差异很大
例如商品查询流量很高,后台对账任务计算量很大,而用户资料修改请求很少。单体只能整体扩容,微服务则可以只扩容压力最大的部分。
3. 团队规模和业务复杂度持续增长
当多个团队频繁修改同一代码库、共享同一批数据库表时,技术依赖会转化为沟通和协调成本。
清晰的服务边界可以让一个跨职能团队端到端负责某项业务能力,但团队边界也应与业务边界对齐。
4. 不同业务能力需要不同可靠性策略
核心下单流程和营销推荐的可用性要求可能不同。拆分后,可以分别设计资源隔离、容量、降级和恢复策略。
需要注意:服务拆开并不会自动获得故障隔离。如果调用链过长、超时设置不合理、重试失控,故障反而会通过网络调用快速扩散。
五、微服务带来了哪些新成本
微服务不是把原有复杂度消灭,而是改变了复杂度的位置。
1. 本地调用变成网络调用
一次函数调用通常成功或直接抛出错误,而远程调用还可能出现:
- 请求根本没有到达;
- 请求到达但响应丢失;
- 响应速度异常;
- 客户端超时后,服务端仍然完成了操作;
- 重试导致同一操作执行多次;
- 消息重复、延迟或乱序。
因此,超时、重试、幂等、熔断和降级不再是可选细节。
2. 本地事务变成分布式一致性
在单体中,“创建订单、扣减库存、记录支付”可能放在一个数据库事务里。拆成多个服务后,每个服务只能可靠地提交自己的本地事务。
系统必须定义:
- 哪些数据必须立即一致;
- 哪些状态允许短暂不一致;
- 后续步骤失败时如何补偿;
- 重复消息如何处理;
- 如何确认整个业务流程最终完成。
3. 调试变得困难
一个请求可能经过网关、订单、库存、支付和消息系统。只查看某个进程的日志,很难还原完整过程。
系统需要统一的日志字段、指标、链路追踪和告警。
4. 交付和运维成本上升
更多服务意味着更多:
- 构建与发布流水线;
- 配置和密钥;
- 健康检查;
- 版本兼容问题;
- 容量与成本管理;
- 值班、告警和故障恢复责任。
一句话总结:
微服务不是为了让简单系统看起来更高级,而是为了让已经复杂的系统仍能持续演进。
六、服务应该怎么拆
服务拆分是微服务设计中最关键、也最容易出错的部分。
1. 按业务能力拆,不按技术层拆
较好的划分方式:
- 订单;
- 支付;
- 库存;
- 物流;
- 通知。
容易产生问题的划分方式:
- Controller 服务;
- Service 服务;
- Repository 服务;
- 数据库访问服务。
按技术层拆分会让一次业务请求跨越大量服务。所有服务必须一起修改和发布,网络调用增多,业务边界却没有变清晰。
2. 不要按数据库表或接口数量机械拆分
“一张表一个服务”和“一个接口一个服务”通常会制造过细的服务。服务的粒度应由业务一致性、变更频率、团队责任和运行特征共同决定。
领域驱动设计中的 Bounded Context(限界上下文)可以帮助寻找边界:在一个边界内,业务术语、规则和数据模型保持一致;跨越边界时,通过明确契约协作。
3. 用四个问题检查候选边界
对一个候选服务,可以询问:
- 它是否代表一项相对完整的业务能力?
- 它能否在不修改其他服务的情况下独立演进?
- 它拥有的数据和业务规则是否清晰?
- 一个团队能否端到端负责其开发、部署和运行?
如果答案长期是否定的,边界很可能不合理。
4. 识别“分布式单体”
常见信号包括:
- 服务很多,但每次必须一起发布;
- 多个服务直接读写同一组表;
- 一个需求要同时修改许多服务;
- 同步调用链很长;
- 某个下游变慢会拖慢整条链路;
- 本地启动必须拉起十几个服务;
- 服务名不同,业务责任却互相重叠。
服务拆得越细,不代表架构越好。边界不确定时,先保持较粗粒度通常更容易调整。
七、同步通信与异步通信
微服务常见的通信方式可以分为两类。
| 方式 | 常见实现 | 适合场景 | 主要代价 |
|---|---|---|---|
| 同步通信 | HTTP、REST、gRPC | 调用方必须立即得到结果 | 时间耦合、级联失败、长调用链 |
| 异步通信 | 消息队列、事件流 | 后台处理、削峰、跨服务通知 | 最终一致性、重复与乱序、排障更难 |
1. 什么时候用同步调用
当调用方必须根据结果立即继续时,同步调用更自然,例如:
- 查询商品详情;
- 校验当前库存;
- 获取用户权限;
- 请求支付渠道创建支付单。
每个远程调用都应该考虑:
- 明确的超时时间;
- 只对适合的错误重试;
- 退避策略和最大重试次数;
- 幂等设计;
- 熔断、舱壁隔离与降级;
- 请求标识和链路上下文传播。
重试不是越多越可靠。没有退避和上限的重试,可能在下游故障时制造更大的流量压力。
2. 什么时候用异步消息
当调用方不需要立即得到最终结果,或者需要削峰、解耦时,可以使用异步消息,例如:
- 订单创建后发送通知;
- 支付成功后更新积分;
- 用户行为进入分析流水线;
- 耗时任务进入后台处理。
需要区分两个概念:
- Command(命令):希望某个接收方执行一项动作;
- Event(事件):声明某件事已经发生,发布者不指定所有后续行为。
使用消息系统后,还必须处理重复投递、消费失败、乱序、死信和消息积压。
八、数据归属与分布式一致性
服务自治的重要基础是数据归属。
1. 服务拥有自己的数据
理想情况下,只有订单服务能够修改订单数据,只有库存服务能够修改库存数据。其他服务通过 API、事件或专门的读取模型获取所需信息。
“数据归属独立”并不等于项目第一天就必须购买多个数据库实例。早期可以共享数据库基础设施,但应保持逻辑边界:
- 表由明确模块或服务拥有;
- 禁止跨边界直接写表;
- 跨边界访问经过稳定契约;
- 为未来物理拆分保留可能。
2. 跨服务查询怎么办
常见方案包括:
- API Composition:由聚合层调用多个服务并组合结果;
- 数据副本或快照:订阅事件,保留查询所需的局部数据;
- CQRS 读取模型:为复杂查询建立专门的读模型。
选择取决于实时性、查询复杂度、数据量和可接受的一致性窗口。
3. Saga 解决跨服务业务流程
Saga 把一个跨服务事务拆成多个本地事务。每一步成功后触发下一步;某一步失败时,执行事先设计的补偿操作。
以创建订单为例:
创建待支付订单
↓
预留库存
↓
创建支付
↓
支付成功 → 确认订单
任一步失败
↓
执行对应补偿:释放库存、关闭支付单、取消订单
补偿不一定能把现实世界完全“回滚”。例如支付退款可能产生手续费,物流发出后也不能像数据库事务一样撤销。因此,补偿规则本身就是业务设计。
4. Outbox 避免数据库与消息“双写”
一个常见风险是:
- 业务数据已经提交;
- 消息发布失败;
- 其他服务永远不知道这次变更。
Transactional Outbox(事务发件箱)模式把业务变更和待发送事件写入同一个本地事务,再由独立发布器把 Outbox 记录发送到消息系统。
它能减少“双写不一致”,但消费者仍要具备幂等性,因为消息可能被重复发送。
九、网关、服务发现和流量入口
1. API Gateway
网关通常作为外部客户端的统一入口,负责:
- 路由;
- 身份认证;
- 限流;
- 日志与请求标识;
- 协议转换;
- 灰度路由;
- 必要的响应聚合。
业务规则不应大量进入网关,否则网关会变成新的集中式瓶颈和耦合中心。
2. 服务发现
服务实例可能随扩缩容和故障恢复而变化,调用方不应硬编码实例地址。服务发现把稳定的服务名称映射到当前可用实例。
例如在 Kubernetes 中,Service 为一组动态 Pod 提供稳定的网络入口,并可通过集群 DNS 被其他工作负载发现。
十、容错与稳定性设计
分布式系统中,部分失败是常态。常用机制包括:
- Timeout:避免请求无限等待;
- Retry:对瞬时、可恢复错误有限重试;
- Idempotency:重复请求不会产生额外副作用;
- Circuit Breaker:下游持续失败时暂停调用;
- Fallback:返回缓存、默认值或受限能力;
- Rate Limiting:控制进入系统的请求速率;
- Bulkhead:隔离线程池、连接池或资源配额;
- Health Check:区分进程存活与能否接收流量;
- Graceful Shutdown:停止接流量后完成或移交在途任务。
这些机制必须组合设计。例如,超时过长会占用资源,重试次数过多会放大流量,错误的降级结果则可能破坏业务正确性。
十一、可观测性不是上线后的补丁
一个微服务系统至少需要三类信号。
1. 日志
日志应尽量结构化,并包含:
- 时间;
- 服务名和环境;
- 请求或链路标识;
- 关键业务实体标识;
- 错误类型和上下文;
- 必要的版本信息。
注意不要在日志中记录密码、Token、密钥和不必要的个人信息。
2. 指标
至少关注:
- 请求量;
- 错误率;
- 延迟及 P95、P99;
- CPU、内存、连接池等资源;
- 队列积压;
- 订单成功率等业务指标。
3. 链路追踪
Trace 描述一个请求如何跨越多个服务,Span 表示其中一段操作。统一传播 trace_id 或等价上下文后,才能定位慢请求究竟卡在网关、数据库还是下游服务。
OpenTelemetry 将 Traces、Metrics 和 Logs 作为主要可观测信号,并提供跨语言的采集规范。工具本身不是目标,目标是让团队能够回答:
- 系统哪里出错了?
- 影响了哪些用户和业务?
- 从什么时候开始?
- 根因在哪一个服务或依赖?
十二、安全、测试和交付是微服务的前置能力
1. 安全
微服务安全不能只依赖网关:
- 网关可以执行统一身份认证和基础策略;
- 服务仍需校验业务权限;
- 服务间通信需要可信身份和传输保护;
- 密钥应由专门机制管理;
- 多租户数据必须实施隔离;
- 敏感操作需要审计。
如果任何内部服务都被默认信任,一旦攻击者进入内网,风险会迅速扩大。
2. 测试
常见测试层次包括:
- 单元测试:验证单个规则和函数;
- 集成测试:验证数据库、缓存、消息等真实依赖;
- 契约测试:验证服务提供方与消费方的接口约定;
- 端到端测试:验证少量关键业务链路。
微服务不应依靠大量脆弱的端到端测试兜底。单元、集成和契约测试应尽早发现大部分问题。
3. CI/CD 与发布
独立部署只有在自动化交付成立时才有价值。一条服务流水线通常需要完成:
- 静态检查;
- 单元和集成测试;
- 契约测试;
- 构建制品;
- 安全扫描;
- 测试环境部署;
- 冒烟验证;
- 生产发布;
- 监控与回滚。
发布还要处理 API、消息格式、数据库迁移和配置的向后兼容。代码可以回滚,不代表数据库和已经发出的消息也能自动回滚。
十三、Docker 和 Kubernetes 分别解决什么
Docker 主要解决应用及其依赖如何被一致打包和运行的问题。
Kubernetes 主要解决容器化工作负载如何被调度、服务发现、扩缩容、自愈和滚动更新的问题。
它们都可以支撑微服务,但不会替你解决:
- 服务边界;
- 业务一致性;
- 接口耦合;
- 团队责任;
- 领域模型。
一个边界混乱的系统部署到 Kubernetes 后,只会变成一个运行在 Kubernetes 上的边界混乱系统。
十四、用电商系统理解渐进式拆分
假设我们从一个电商项目开始。
阶段一:先做模块化单体
E-commerce Application
├── User Module
├── Catalog Module
├── Cart Module
├── Order Module
├── Payment Module
├── Inventory Module
└── Notification Module
所有模块仍在一个进程中,但需要明确:
- 模块公开哪些接口;
- 哪些数据由模块拥有;
- 其他模块不能绕过接口直接修改其数据;
- 模块之间的依赖方向。
这一步能够用较低成本验证业务边界。
阶段二:先抽取低风险、弱事务模块
通知通常是一个较适合的候选:
Monolith ── OrderCreated Event ──> Notification Service
通知失败不应阻止订单创建,异步消息可以降低时间耦合。此时可以练习:
- 消息投递;
- 重复消费幂等;
- 失败重试和死信;
- 服务日志与追踪;
- 独立构建和部署。
阶段三:按真实压力继续抽取
当支付、库存已经具备稳定边界,并且存在独立发布、容量或团队责任需求时,再考虑抽取。
典型流程可能是:
Client
↓
API Gateway
↓
Order Service
├── Inventory Service
├── Payment Service
└── Event Bus ──> Notification Service
订单服务需要通过状态机描述“待支付、已支付、已取消、履约中”等状态,而不是假设所有步骤会在一次同步请求中同时成功。
阶段四:按运维压力引入编排平台
当服务数量、实例数量、部署频率和环境复杂度确实需要统一调度时,再引入 Kubernetes 或托管容器平台。
不要为了学习 Kubernetes 而先制造几十个微服务。架构演进应由问题驱动。
十五、什么时候不应该使用微服务
以下情况下,模块化单体往往更合适:
- 团队规模较小,沟通成本尚未成为瓶颈;
- 业务模型频繁变化,边界还不稳定;
- 系统规模不大,整体发布和扩容没有明显问题;
- 缺少自动化测试和部署流水线;
- 没有统一日志、指标、链路和告警;
- 团队尚未准备承担分布式数据和网络故障;
- 服务没有明确负责人和运行责任。
微服务的收益必须大于它引入的协调、基础设施和故障处理成本。
十六、什么时候可以认真考虑微服务
可以寻找这些证据:
- 某些业务模块需要频繁独立发布;
- 不同模块流量差异明显,需要独立扩缩容;
- 多支团队频繁在同一代码和数据边界上冲突;
- 业务边界已经相对稳定;
- 某些能力需要明确的故障和资源隔离;
- CI/CD、可观测性、值班和故障恢复机制已经较成熟;
- 团队能够端到端负责服务生命周期。
这里的关键词是“证据”,而不是“未来也许会变大”。
十七、一条更稳妥的学习与落地路线
推断:结合本文整理的工程约束,一条低风险路线如下。它不是所有团队唯一的答案,但比一开始直接堆叠基础设施更容易验证架构价值。
第一步:建立模块化单体
- 用一个熟悉的后端框架完成核心业务;
- 按业务模块组织代码;
- 明确模块 API 和数据归属;
- 使用本地事务保证核心正确性。
第二步:补齐工程基础
- 用 Docker Compose 运行应用、数据库和缓存;
- 建立自动化测试与 CI;
- 加入结构化日志、请求标识、指标和健康检查;
- 明确配置与密钥管理。
第三步:抽取一个低风险服务
- 选择通知、文件处理等边界清晰的能力;
- 通过 Strangler Fig(绞杀者模式)逐步转发流量;
- 建立独立构建、发布和监控;
- 验证服务归属是否真的减少了协调成本。
第四步:练习可靠通信
- 同步调用设置超时、有限重试和熔断;
- 异步消费处理重复、失败和积压;
- 对有副作用的操作实现幂等;
- 传播链路上下文。
第五步:再处理跨服务业务
- 定义一致性要求;
- 按需要引入 Saga 和 Outbox;
- 设计业务状态机与补偿动作;
- 通过契约测试保护接口和消息兼容性。
第六步:最后学习集群编排
- 理解 Pod、Deployment、Service 和配置管理;
- 实践滚动发布、扩缩容、就绪检查和优雅退出;
- 用真实运行压力验证是否需要更复杂的平台能力。
十八、微服务决策清单
在决定拆分某个服务前,可以逐项回答:
业务边界
- 这个服务代表什么业务能力?
- 哪些规则和数据属于它?
- 它与其他服务的契约是什么?
独立性
- 它能否独立开发、测试、部署和回滚?
- 一次变更是否仍要求多个服务同时发布?
- 团队是否能够独立负责其运行?
数据
- 谁可以修改核心数据?
- 跨服务查询如何实现?
- 允许多长时间的不一致?
- 失败时如何补偿和对账?
可靠性
- 超时、重试和幂等如何设计?
- 下游不可用时如何降级?
- 如何避免故障级联?
运维
- 如何查看日志、指标和完整调用链?
- 如何告警、发布和回滚?
- 谁负责处理生产故障?
如果大部分问题没有答案,优先完善模块化单体通常比继续拆分更有价值。
十九、常见误区
误区一:服务越小越好
过细的服务会增加网络调用、数据一致性和运维成本。好的边界比小的代码量更重要。
误区二:每个服务必须使用不同技术栈
技术自治是一种可能性,不是目标。过多语言和框架会增加招聘、升级、安全和运维成本。团队应在自治与平台标准化之间取平衡。
误区三:用了消息队列就解耦了
如果事件格式频繁变化、消费者依赖发布者内部字段,或者所有流程都依赖固定事件顺序,耦合只是从调用接口转移到了消息契约。
误区四:网关负责所有权限
网关适合统一认证和通用策略,服务仍需验证与业务资源相关的授权规则。
误区五:先上 Kubernetes,再设计服务
Kubernetes 管理运行单元,不负责发现业务边界。先解决领域和团队问题,再决定部署平台。
二十、面试时如何简洁回答
可以这样概括:
微服务是一种按业务能力划分系统的架构风格。每个服务拥有清晰职责和数据边界,能够独立开发、部署和扩展,并通过 API 或消息协作。它适合复杂领域、多团队和独立演进需求明显的系统,但会引入网络故障、分布式一致性、可观测性和交付运维成本。因此,实践中通常先建立模块化单体,再根据真实的发布、扩展和团队协作压力渐进拆分。
如果继续追问,应重点展开:
- 如何识别服务边界;
- 同步与异步通信如何选择;
- Saga、Outbox 和幂等如何维护一致性;
- 超时、重试、熔断和隔离如何避免级联故障;
- 什么情况下不应该使用微服务。
总结
微服务最重要的不是“把代码拆开”,而是建立可独立演进的业务边界。
判断一个微服务设计是否有效,可以看五件事:
- 边界是否围绕业务能力;
- 服务是否真正能够独立发布;
- 数据是否有清晰所有者;
- 网络失败和一致性问题是否被显式设计;
- 团队是否具备测试、交付、观测和运行能力。
对于多数新项目,更可靠的起点是模块化单体。先把边界做好,再让真正需要独立演进的模块跨越进程边界。
架构没有免费的收益。微服务的价值,不在于服务数量,而在于它引入的复杂度是否换来了更低的长期变更成本。
如果这篇文章帮你建立了微服务架构的整体认知,欢迎点赞、收藏,方便后续学习服务拆分、服务通信、数据一致性、容错治理和部署运维时随时回来复习。
你在学习或实践微服务时遇到过哪些问题?欢迎在评论区一起交流。后续我还会继续分享微服务设计、系统架构和后端工程实战,感兴趣的话点个关注,我们下一篇见!
参考资料
- Microsoft Learn:Microservices architecture style
- Microsoft Learn:Use domain analysis to model microservices
- Microsoft Learn:Identify microservice boundaries
- Microsoft Learn:Design patterns for microservices
- AWS Prescriptive Guidance:Saga orchestration pattern
- AWS Prescriptive Guidance:Transactional outbox pattern
- OpenTelemetry:Signals
- Kubernetes Documentation:Service
更多推荐

所有评论(0)