主题
微服务架构概述
一句话:微服务把「一个进程内的模块调用」换成「跨网络的独立部署单元」,用运维、网络和一致性的复杂度,换取每个业务域独立发布与扩容的自由。
适用版本:Spring Boot 3.2.x / Spring Cloud 2023.0.x / Spring Cloud Alibaba 2023.0.x。组件默认值以官方文档为准。
为什么需要
单体不是原罪,痛点出现在具体时刻:
- 改一个不相关的模块,也要全量回归、全量重启。
- 一个慢查询把整个进程的线程池占满,所有接口一起超时。
- 某模块流量是其他模块的十倍,却只能整机扩容。
- 多个团队共用一个发布窗口,互相等待。
拆分的收益是独立发布、独立扩容、故障隔离;代价是网络调用、分布式事务、链路排障、版本兼容。微服务不是「更先进的单体」,是一次成本结构交换。
核心机制
一个请求的路径
一个请求从入口到落库会依次穿过下面这些组件:网关管入口横切,LoadBalancer 管选实例,Feign/Dubbo 管发请求,Sentinel 管每一跳的流量保护,链路追踪管事后定位。注册中心只出名单、不在链路上。
结论是:治理能力和业务调用在同一条链上各管一段,每一层解决一个不同的问题,不能互相替代:
| 能力 | 解决什么 | 缺了会怎样 | 典型组件 |
|---|---|---|---|
| 注册发现 | 实例地址动态变化 | IP 写死在配置里,扩缩容要改配置重启 | Nacos、Consul、Eureka |
| 配置中心 | 配置变更不重启 | 改一个开关要发版 | Nacos Config、Apollo |
| 网关 | 入口统一鉴权、限流、路由 | 每个服务重复实现鉴权,规则不一致 | Spring Cloud Gateway |
| 客户端负载均衡 | 多个实例选一个 | 调用方自己写随机/轮询 | Spring Cloud LoadBalancer |
| 熔断限流 | 下游故障时不拖死自己 | 一个慢依赖耗尽调用方线程 | Sentinel、Resilience4j |
| 分布式事务 | 跨库写入的一致性 | 部分成功、部分失败,数据对不上 | Seata、本地消息表 |
为什么是这些组件,而不是「一套全家桶」
Spring Cloud 提供的是抽象和默认实现,Spring Cloud Alibaba 提供的是经过大规模验证的实现。两者的关系是「接口与实现」,不是「新旧替代」。选型时按能力选,不按厂商选:
- 注册配置一体、团队小 → Nacos。
- 需要审批、灰度、审计 → Apollo 做配置,Nacos 只做注册。
- 已有 K8s 与多语言 → Consul/Istio 可能更合适。
- 只需要简单限流 → Guava
RateLimiter或 Resilience4j,不必上 Sentinel 全家桶。
Netflix 组件现在是什么状态
| 组件 | 状态 | 迁移方向 |
|---|---|---|
| Eureka | 1.x 仍在维护,Spring Cloud Netflix 模块处于维护模式;Eureka 2.0 已停止开发 | Nacos/Consul |
| Ribbon | 已在 Spring Cloud 2020.0 移除 | Spring Cloud LoadBalancer |
| Hystrix | 已停止开发 | Sentinel/Resilience4j |
| Zuul 1 | 已停止开发 | Spring Cloud Gateway |
「维护模式」不等于「已经停止」:老系统不必一夜迁移,但新项目不应再把它们作为默认选择。
最常见的反模式:分布式单体
服务拆开了,但:
- 所有服务共用一个数据库,表结构变更仍然互相阻塞。
- 一个业务请求同步串行调用五六个服务,任何一个慢就全链路慢。
- 服务之间共享 DTO/工具类,改一处要全量发版。
这时拆分的成本已经付了,收益却没拿到。判断标准:每个服务能否独立发布、独立扩容、独立排障;不能,就还没有真正拆开。
实战场景
一、拆完服务,发布反而更慢:分布式单体
现象:订单域拆成 6 个服务后,一次需求平均要同时发 4 个服务,发布窗口从 15 分钟涨到 90 分钟;一次改 orders 表加字段,连带阻塞了 3 个团队的上线。上线后 P99 从 120ms 涨到 300ms。
排查:用三条判据检查“是否真的拆开了”,每条都要有证据。
| 判据 | 取证据 | 本例结果 |
|---|---|---|
| 独立发布 | 统计需求单:一次变更涉及几个服务;查是否有共享 jar 被频繁改 | 4/6,共享 order-common 一个月改 9 次 |
| 独立数据 | 数 schema 归属:几个服务连同一个库/同一组表 | 6 个服务共用 1 个 MySQL 实例、同一套表 |
| 独立排障 | 抽 5 次故障,统计定位时长;看是否有贯穿全链的 traceId | 平均 2 小时,MQ 之后链路断裂 |
| 调用形态 | trace 里一个入口请求的同步串行跳数 | 串行 5 跳,P99 ≈ 各跳之和 |
三条都不满足,就是拆分只付了成本、没拿到收益。这不是组件配置问题,而是边界问题——先有共库,才有连带发布。
对症做法:
yaml
# 1. 先止血:把必须一起发的服务合并部署单元,逻辑边界保留、物理边界收回
# 共享 order-common 里的 DTO 拆成各服务自己的视图对象,禁止跨服务复用实体text
2. 数据边界按“谁写这张表”划分,一个表只有一个写服务
orders / order_item → order-service 独占写
其他服务只通过接口或读模型访问,不直连表
3. 串行链路改异步:非关键跳(积分、通知、风控留痕)走消息,
入口只等核心写;P99 从各跳之和变成核心跳之和判断“要不要合回去”的成本口径很简单:如果一次需求平均涉及的服务数 ≥ 3,且这些服务的发布窗口重叠,那当前的边界就没有价值。相关建模方法见 DDD 战略设计。
复盘要点:拆分的验收标准应该是“一个服务能独立完成开发、发布、扩容、排障”,不是服务数量。这次改动之后,把“单需求涉及服务数”“共库表清单”列入季度架构体检,两个数字不再下降就说明边界仍需调整。
二、一次 GC 抖动放大成全链路雪崩
现象:库存服务一次 Full GC 停顿 1.8 秒,随后 3 分钟内下单链路整体超时,营销、优惠券服务也被拖垮;库存恢复后,故障仍在其他服务里持续了十几分钟。库存服务本身只停机了一次。
排查:先确认请求有没有被重复发送。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 同一请求的重复 span | trace 里按 traceId 看下游 span 数量与时间间隔 | 一个入口请求触发 3 次同一下游调用 → 有重试 |
| 每跳超时配置 | 汇总入口/Feign/Ribbon/LoadBalancer/数据库的超时值 | 入口 3s,下游每跳 2s → 上游必然先超时再重试 |
| 实际 QPS 放大 | 故障时段下游 QPS / 正常时段 | 超过 2 倍即为重试放大,不是流量自然增长 |
| 熔断是否生效 | Sentinel 拒绝数指标在故障时段是否上升 | 一直为 0 → 熔断阈值没配或资源没接入 |
| 恢复慢的原因 | 上游线程池活跃数与队列长度 | 线程被慢调用占住,新请求进不来 |
根因链条是超时预算不自洽:入口 3s,下游读超时 2s 且重试 2 次,最坏一次入口请求给下游打了 3 次、耗时可达 6s。
对症做法:把“入口超时 > 下游读超时 ×(重试次数 + 1)+ 连接超时”当作配置校验,而不是口头约定。
yaml
feign:
client:
config:
stock-service:
connectTimeout: 300
readTimeout: 800 # 入口预算 3s,扣掉网络与排队,留给单跳 800ms
httpclient:
max-connections: 200
ribbon:
MaxAutoRetries: 0 # 只读接口才允许重试;写接口一律 0
MaxAutoRetriesNextServer: 1 # 换一台重试,且要求幂等java
// 写接口不做自动重试,失败交给调用方显式决定;幂等用业务唯一键保证
@PostMapping("/stock/deduct")
public Resp deduct(@Valid @RequestBody DeductReq req) { // req.requestId 作幂等键
return idempotentGuard.runOnce(req.requestId(), () -> stockService.deduct(req));
}重试次数越多,越需要退避与抖动;固定间隔重试会在同一时刻把失败请求再次对齐。
复盘要点:超时是端到端预算,不是每层各自定一个“看着合理”的数字。做法是维护一张链路超时表(入口→各跳→DB),发布前用脚本校验不等式;再配一条常态化故障演练——杀掉一个实例并注入 2s 延迟,验证拒绝、熔断和降级是否按预期出现,见 全链路超时与自适应限流。
三、组件全家桶上了线,排障仍然靠人
现象:服务数从 8 个涨到 40 个后,Nacos、Gateway、Sentinel、Seata、Sleuth 全上了,但一次“下单少发一张券”的故障查了 3 小时;关键卡点是消息队列之后的链路断了,且 40 个服务里有 11 个没接 traceId 透传。
排查:组件在不在用,和治理能力是否生效是两件事。
| 能力 | 取证据 | 判定 |
|---|---|---|
| 链路贯穿 | 抽 10 个入口请求,看 trace 是否覆盖 DB、RPC、MQ 生产与消费 | 覆盖不全 → 断链就是排障时长主因 |
| 实例可定位 | 错误日志是否带实例 IP/Pod 名与版本 | 只有服务名 → 无法判断是否单实例问题 |
| 流量保护 | Sentinel 控制台是否有对应资源与拦截计数 | 资源为空 → 控制台装了但代码没接 |
| 配置生效 | 关键开关在 /actuator/configprops 的实际值 vs 配置中心值 | 不一致 → 配置项被覆盖或没被读取 |
| 演练验证 | 最近一次故障演练记录与结论 | 没有演练 → 所有能力都只是“应该可用” |
本例命中的是第 1 行与第 3 行:MQ 消费者用了自建线程池,traceId 未随消息头传播;Sentinel 的 @SentinelResource 只加在 20 个接口上,其余接口没有任何流控资源。
对症做法:
java
// 1. 链路必须跨线程、跨消息传播:发送端写入消息头,消费端恢复上下文
message.putUserProperty("traceId", MDC.get("traceId"));
// 消费端
MDC.put("traceId", msg.getUserProperty("traceId"));
try { handle(msg); } finally { MDC.clear(); }
// 2. 线程池统一包装,避免每次手写传播
Executor biz = ContextPropagatingExecutor.wrap(Executors.newFixedThreadPool(16));text
3. 治理能力的最小集按“服务上线即接入”约束:
traceId 贯穿(HTTP + RPC + MQ)+ 实例标识 + 关键接口的流控资源 + 一条演练用例
4. 组件本身可以渐进:先只做注册发现与链路,限流按接口逐个接,不一次性全上复盘要点:这一类问题的共同形状是“组件已部署,能力未生效”,判据只能是抽样验证而不是配置清单。把“trace 覆盖率”“流控资源覆盖率”做成周度指标,覆盖率低于 100% 的新服务不允许进生产;详见 企业级可观测性与故障定位。
最小上手
用 Docker 起一个 Nacos,再起一个最小服务注册上去。Spring Boot 3.2 + Spring Cloud Alibaba 2023.0.x:
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>yaml
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
server:
port: 8080bash
docker run -d --name nacos -p 8848:8848 -p 9848:9848 \
-e MODE=standalone nacos/nacos-server:v2.3.2
# 启动服务后验证注册
curl -s "http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=order-service"返回 JSON 中能看到 hosts 里有 127.0.0.1:8080,说明注册成功。Nacos 2.x 客户端还会连接 9848(gRPC),只映射 8848 会导致注册成功但推送/心跳异常。
生产动作
- 版本先对齐:Boot 3.2.x ↔ Cloud 2023.0.x ↔ SCA 2023.0.x;用
mvn dependency:tree确认没有混入旧版spring-cloud-starter-netflix-*。 - 注册中心高可用:Nacos 至少 3 节点 + 外置 MySQL,
MODE=cluster;不要用单机模式上生产。 - 优雅下线:先摘除实例,等一个 LoadBalancer 缓存周期(默认 35s)再停进程;否则下线瞬间仍有请求打过来。
- 超时链路:入口超时 > 下游读超时 ×(重试次数 + 1)+ 连接超时;任何一层不满足,重试就会放大成雪崩。
- 可观测性:每个请求带 traceId,网关、Feign、MQ 都要透传;至少能看到「哪个服务、哪个实例、哪一跳慢」。
- 故障演练:定期杀掉一个下游实例,确认注册摘除、负载均衡切换、熔断降级都按预期工作。
边界
- 团队小于 5 人、业务边界还在变:单体 + 模块化更快,拆分只会增加运维负担。
- 强一致跨库写入很多:先考虑能否通过业务设计避免;不能避免再引入分布式事务,成本很高。
- 没有自动化部署与监控:几十个服务靠手工部署,故障定位会失控。
- 只是想把代码分目录:用模块化,不要用微服务。
- 把网关当业务编排层:网关只做横向切面,业务编排放服务内或专门的编排层。
讲清楚
微服务的本质是把模块调用换成网络调用:好处是每个业务域能独立发布、独立扩容、故障隔离,代价是网络延迟、分布式事务、版本兼容和排障复杂度都上升。Spring Cloud 提供的是一组治理能力:注册发现解决地址变化,配置中心解决改配置不重启,网关解决入口统一鉴权限流,客户端负载均衡解决多实例选择,熔断限流解决下游故障扩散,分布式事务解决跨库一致性。它们各自解决一个问题,不能互相替代。判断要不要拆,不看技术先不先进,看「独立发布、独立扩容、独立排障」的收益是否大于网络和一致性的成本;如果拆完还共库、还全链路同步串行,那只是分布式单体。