把一个 Spring Boot 应用拆成多个进程并不难,难的是拆分以后仍能让系统保持清晰、可靠和可维护。原本一次方法调用会变成网络请求,原本一个数据库事务会跨越多个数据边界,原本查看一份日志就能定位的问题,也可能分散在多台机器和多个服务中。
因此,理解微服务不能从“需要安装哪些组件”开始,而要先回答三个问题:为什么要拆、边界在哪里、拆分增加的成本是否值得承担。本文从这些稳定的问题出发,再把服务调用、注册发现、网关、故障治理、数据一致性和可观测性串成一条完整链路。
微服务首先改变的是边界
单体和微服务最直观的区别是部署单元数量,但真正重要的变化是所有权边界。
在结构良好的模块化单体中,订单、库存、支付可以是三个边界清楚的模块,但它们仍然随同一个应用一起构建和发布。微服务进一步把部分模块变成可独立部署的服务,并要求每个服务拥有自己的业务职责、接口和数据。
| 维度 | 模块化单体 | 微服务 |
|---|---|---|
| 部署 | 整体构建和发布 | 服务可独立构建、发布与回滚 |
| 调用 | 进程内方法调用为主 | 网络调用或消息通信 |
| 数据 | 可以共享数据库,但应保持模块边界 | 服务拥有数据,其他服务通过契约访问 |
| 故障 | 进程故障可能影响整个应用 | 可以隔离,但也会产生级联故障 |
| 运维 | 环境和监控相对集中 | 需要自动化部署、观测和治理能力 |
| 适用问题 | 业务尚在探索、团队较小 | 边界较稳定,独立交付或扩容需求明确 |
“服务拥有数据”不等于每个服务必须立即购买一套独立数据库。重点是写入权和模型的归属清楚:库存服务不能绕过订单服务直接修改订单表,订单服务也不应通过跨库 JOIN 依赖库存表的内部结构。物理隔离可以随风险和规模逐步推进,逻辑所有权则应从设计开始就明确。
什么时候值得从单体拆出去
微服务常见的有效动机包括:
- 不同业务能力的发布节奏明显不同,整体发布已经频繁互相阻塞;
- 某个模块需要独立扩容,而整体扩容浪费资源;
- 一个代码库的变化耦合和责任边界持续失控,已经影响交付;
- 多个团队需要围绕稳定的业务边界独立负责开发、运行和演进;
- 某个能力对可用性、安全或技术栈有明显不同的要求。
反过来,如果系统规模不大、业务边界仍在快速变化、团队只有几个人,或者连自动化测试和部署都不稳定,拆成微服务通常会放大问题。此时更合适的起点是模块化单体:先把代码依赖、数据访问和模块接口管住,再根据实际瓶颈提取服务。
如何划定服务边界
服务边界通常从业务能力开始,而不是从技术分层开始。user-service、order-service 可能是业务边界;controller-service、database-service 通常只是把同一个业务请求拆成更多网络跳数。
可以用以下四个问题检查候选边界:
- 它负责什么业务结果? 服务应该能用业务语言描述职责,而不是只描述技术动作。
- 哪些数据由它决定? 服务应拥有写入规则和状态迁移,其他服务通过接口或事件读取结果。
- 它为什么发生变化? 经常一起修改、一起发布的功能通常不适合强行拆开。
- 失败时能否独立处理? 如果两个服务必须同时成功、同时扩容、同时发布,边界很可能还不稳定。
边界不会一次设计到永久正确。更可靠的做法是从较粗的边界开始,通过调用频率、变更记录和故障数据观察耦合,再决定合并或继续拆分。过早拆得太细会产生大量“分布式内部方法调用”,接口数量和运维成本上升,却没有获得独立演进的收益。
一次微服务请求经过哪些组件
以下是一条简化的下单链路。实线表示业务流量,虚线表示注册、发现、配置和遥测等控制关系。
flowchart LR C[客户端] --> G[API Gateway] G --> O[订单服务] O --> I[库存服务] O --> P[支付服务] O --> ODB[(订单数据)] I --> IDB[(库存数据)] P --> PDB[(支付数据)] O --> MQ[(消息系统)] MQ --> N[通知服务] O -. 注册与发现 .-> R[服务注册中心] I -. 注册与发现 .-> R P -. 注册与发现 .-> R G -. 路由与发现 .-> R O -. 配置 .-> CF[配置中心] I -. 配置 .-> CF G -. 日志 指标 Trace .-> OBS[可观测平台] O -. 日志 指标 Trace .-> OBS I -. 日志 指标 Trace .-> OBS P -. 日志 指标 Trace .-> OBS
这张图里的组件不是每套系统都必须单独部署。例如,在 Kubernetes 中,Service 和集群 DNS 可以为一组会动态变化的 Pod 提供稳定名称和访问入口;应用未必需要自己实现一套客户端注册发现。选择组件前,先确认运行平台已经解决了什么问题。
API Gateway:统一入口,不是万能业务层
网关适合处理跨服务的入口能力,例如路由、认证入口、限流、协议适配和访问日志。它不应承载大量业务编排,否则每次业务变化都需要修改中央网关,最终形成新的“大单体”。
内部服务也不能因为位于网关之后就默认可信。身份信息需要使用可验证的方式传递,服务之间仍要做必要的授权;来自客户端的身份头不能未经校验直接透传为可信身份。
服务发现:把服务名映射到可用实例
服务实例会扩容、重启和迁移,IP 地址并不稳定。服务发现维护“服务名到当前可用实例”的关系:提供者注册或由平台发现实例,消费者查询或订阅实例列表,再通过负载均衡选择目标。
这里要区分三个概念:
- 服务发现回答“有哪些实例可用”;
- 负载均衡回答“这次选择哪个实例”;
- 流量治理进一步处理灰度、权重、故障隔离和策略控制。
它们可以由应用框架、注册中心、代理或 Kubernetes 等平台以不同方式组合实现,不必绑定到同一个产品。
配置中心:统一管理运行参数
当服务数量增加后,散落在各个部署包里的环境地址、开关和阈值很难统一管理。配置中心可以提供按环境隔离、版本管理、审计和动态更新能力。
但“支持动态刷新”不代表所有配置都应该热更新。线程池大小、连接池和安全策略等配置是否能安全刷新,取决于应用实现;变更后还要能够验证、灰度和回滚。密码与密钥则应进入专门的 Secret 管理方案,而不是以普通配置明文保存。
同步调用和异步消息怎么选
服务之间最常见的通信方式可以归为同步请求和异步消息。
| 方式 | 适合场景 | 主要代价 |
|---|---|---|
| HTTP/REST | 面向资源、跨语言、需要立即结果 | 调用方与下游在时间上耦合 |
| RPC/gRPC | 明确接口、内部高效调用、流式通信 | 契约演进和调试工具需要治理 |
| 消息或事件 | 解耦、削峰、异步工作流、广播状态变化 | 最终一致性、重复消息和顺序处理更复杂 |
选择的关键不是哪个协议更“先进”,而是业务是否需要立即得到结果。例如查询库存通常需要同步响应;订单创建后发送通知不应阻塞主请求,更适合由事件异步触发。
无论使用哪种方式,都要把接口当成长期契约:
- 新增字段尽量保持向后兼容,不让调用方和提供方必须同时发布;
- 明确超时、错误码和重试语义,而不只定义成功响应;
- 写操作提供幂等键或业务唯一约束,避免重试产生重复副作用;
- 消息事件描述已经发生的事实,例如
OrderCreated,并携带稳定的事件 ID、业务 ID 和版本。
一个简化的事件可以是:
{ "eventId": "evt_01J...", "eventType": "OrderCreated", "version": 1, "occurredAt": "2026-09-12T10:30:00+08:00", "orderId": "order_123", "customerId": "customer_456"}消费者应先根据 eventId 或业务唯一键判断是否已经处理。即使消息基础设施提供某种 exactly-once 能力,数据库写入、外部支付和通知等端到端副作用仍需要业务幂等保护。
服务治理的目标是限制故障扩散
单体中的方法调用通常要么返回,要么抛出异常;远程调用还会遇到超时、连接中断、实例重启、响应丢失和部分成功。最危险的不是某个服务失败,而是调用方持续等待和重试,耗尽线程、连接和队列,最终形成级联故障。
治理策略应从最基本的时间和资源边界开始:
| 策略 | 解决的问题 | 使用边界 |
|---|---|---|
| 超时与 deadline | 避免无限等待,给整条链路分配时间预算 | 连接和读取超时都要明确;下游预算不能超过上游剩余时间 |
| 有限重试 | 应对短暂网络抖动或瞬时不可用 | 只重试明确的瞬态错误;写操作先保证幂等 |
| 熔断 | 下游持续失败时快速拒绝,给其恢复时间 | 与重试目的不同,不能用无限重试代替 |
| 隔离舱 | 限制某个依赖占用的线程、连接或并发 | 隔离池本身也要设置容量和拒绝策略 |
| 限流 | 让入口流量不超过系统可处理能力 | 需要定义拒绝、排队或降级语义 |
| 降级 | 保留可接受的核心能力 | 不能伪造支付成功、库存成功等关键业务结果 |
为什么重试可能让故障更严重
假设订单服务收到 1,000 个请求,每个请求在库存超时时再重试两次,库存服务最坏会面对 3,000 次调用。下游已经过载时,这些额外请求会延长恢复时间,形成 retry storm。
安全的重试通常需要同时满足:错误确实可能短暂恢复、操作幂等、次数有限、使用指数退避和随机抖动,并受全局 retry budget 约束。对于明确的参数错误、权限错误或业务拒绝,重试没有意义。
熔断不是降级的同义词
熔断器根据一段时间内的失败或慢调用情况,在 Closed、Open 和 Half-Open 状态之间切换:正常时放行;达到阈值后快速失败;等待恢复窗口后只放少量探测请求。
熔断决定“还要不要调用下游”,降级决定“调用失败后向上游提供什么结果”。商品推荐失败可以返回空推荐,但支付失败必须返回明确失败或处理中,不能为了页面看起来正常而返回虚假的成功。
跨服务数据如何保持一致
服务拆分后,原本一个本地事务中的操作可能变成:创建订单、预占库存、发起支付。直接让多个服务共享一个数据库事务,会破坏独立性并引入可用性和性能成本;完全不设计一致性,则会留下已付款但未创建订单等业务错误。
更实用的思路是先按业务要求区分一致性等级:
- 余额扣减、库存预占等关键不变量,要在拥有该数据的服务内部用本地事务和唯一约束保证;
- 跨服务状态通过事件和明确的状态机逐步推进,接受可观测、可恢复的中间状态;
- 对必须撤销的步骤设计补偿动作,而不是假设所有系统都能一起回滚。
Transactional Outbox:避免“写库成功但消息没发出”
订单服务可以在同一个本地事务中写入订单和 Outbox 事件,再由独立发布器把事件发送到消息系统。发布成功后标记事件状态;发布器可以重试,因此消费者必须幂等。
本地事务: 1. INSERT orders (..., status = 'PENDING') 2. INSERT outbox_events (..., event_type = 'OrderCreated')
提交后: 3. 发布器读取未发送事件并投递 4. 消费者幂等处理 5. 发布器标记已发送或等待下一次重试Outbox 解决的是本地数据库写入与事件发布之间的原子性缺口,但不会自动解决事件重复、顺序、积压和死信处理,仍需要监控和恢复流程。
Saga:把跨服务事务拆成可补偿步骤
Saga 将长业务流程拆成多个服务内的本地事务。每一步成功后触发下一步;失败时执行已经定义的补偿动作。它可以采用事件驱动的 choreography,也可以由 orchestrator 明确协调步骤。
以订单流程为例:
- 订单服务创建
PENDING订单; - 库存服务预占库存;
- 支付服务确认支付;
- 订单服务将状态改为
CONFIRMED; - 如果支付失败,释放库存并把订单改为
CANCELLED。
补偿不是数据库 ROLLBACK。退款、释放库存等操作可能再次失败,也可能无法完全还原现实世界,所以必须具备幂等、重试、人工介入和审计能力。先画出状态机和失败分支,再选择 Saga 框架,通常比先引入框架更重要。
可观测性让分布式问题能够被解释
当一次请求跨过网关、订单、库存和支付服务,仅按时间搜索多份日志很难还原因果关系。生产系统至少需要把以下三类信号关联起来:
- Logs:记录离散事件和诊断上下文;
- Metrics:观察请求量、错误率、延迟分位数和资源饱和度;
- Traces:用 trace 和 span 还原请求经过的服务及每一段耗时。
Trace context 应随同步请求和异步消息传播,日志中记录 trace_id、service.name、环境和业务关联 ID。业务 ID 方便查询某个订单,trace ID 方便还原某次调用,两者用途不同,不能互相替代。
只有采集信号还不够,还需要围绕用户可感知的目标建立告警。例如订单接口的成功率和延迟比“某台机器 CPU 超过 80%”更接近真实服务质量;CPU 告警仍有价值,但它是诊断信号,不一定代表用户请求已经失败。
微服务依赖工程能力,而不只依赖框架
微服务把一次整体发布变成多个服务的持续交付,因此下面这些能力不是锦上添花:
- 自动化测试以及接口契约的兼容性检查;
- 可重复的构建、部署、回滚和数据库变更流程;
- 统一的日志、指标、Trace 和告警规范;
- 服务清单、负责人、依赖关系和运行手册;
- 配置、Secret、证书和权限的生命周期管理;
- 容量评估、限流、故障演练和事故复盘。
容器和 Kubernetes 可以提供标准化部署、服务发现、滚动更新和弹性能力,但它们不会自动修复错误的服务边界、缺失的幂等设计或不可回滚的数据库变更。基础设施解决运行问题,业务一致性仍由应用架构负责。
更稳妥的迁移路线
从现有单体迁移时,可以按以下顺序降低风险:
- 建立模块边界:禁止跨模块随意访问内部实现和数据表;
- 补齐观测和自动化:先知道系统如何运行,再增加部署单元;
- 选择一个有明确收益的边界:例如独立扩容需求强、变化频繁且数据职责清楚的模块;
- 先定义契约和失败语义:包含超时、幂等、兼容和回滚策略;
- 渐进切流:保留回退路径,通过指标比较迁移前后的交付和运行效果;
- 根据证据继续或停止:没有获得预期收益时,不要为了“架构统一”继续拆分。
这种路径常被称为 Strangler Fig:在旧系统旁逐步建立新能力并迁移流量,而不是一次性重写全部系统。它不会消除迁移成本,但能把不可逆的大爆炸式改造变成多个可验证步骤。
设计评审清单
准备拆分一个服务时,可以逐项回答:
- 它解决了哪个已经存在的交付、扩容、可靠性或团队协作问题?
- 如果保持模块化单体,具体会产生什么不可接受的后果?
- 服务的业务职责、数据所有权和负责人是否明确?
- 上下游契约能否独立演进,还是必须同时发布?
- 每个远程调用的超时、重试、幂等和降级语义是什么?
- 跨服务业务有哪些中间状态、补偿操作和人工恢复入口?
- 日志、指标和 Trace 能否还原一次失败请求?
- 能否独立构建、部署、回滚,并处理数据库兼容?
- 新增的基础设施和运维成本由谁长期负责?
- 用什么指标证明拆分之后确实更好?
如果这些问题大多没有答案,最优选择通常不是先引入更多组件,而是保持或改进模块化单体,继续收集边界和瓶颈的证据。
总结
微服务的核心不是注册中心、网关或某个框架,而是把系统划分成能够独立负责、独立交付和独立运行的业务边界。拆分带来的网络、故障和数据一致性问题不会因为使用框架而消失,只会通过明确的契约、时间与资源边界、幂等和补偿设计、自动化交付以及可观测性得到控制。
判断是否采用微服务时,应把两个问题分开:系统当前的问题是否真的需要解决,以及拆成服务是否是成本最低的解决方案。对于多数仍在快速变化的小型系统,结构清晰的模块化单体是更稳妥的起点;当独立交付和扩缩容的收益已经被事实证明,再渐进拆分通常更容易成功。
参考资料
- Spring Cloud Commons:服务发现与通用抽象
- Spring Cloud LoadBalancer
- Spring Cloud Gateway
- Nacos:Service Discovery Overview
- Kubernetes:Service
- Microsoft Azure Architecture Center:Circuit Breaker
- Microsoft Azure Architecture Center:Saga
- Microsoft Azure Architecture Center:Transient Fault Handling
- OpenTelemetry:Signals
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 0 天
部分内容可能已过时