跳到正文

微服务架构概述 ​

一句话:微服务把「一个进程内的模块调用」换成「跨网络的独立部署单元」,用运维、网络和一致性的复杂度,换取每个业务域独立发布与扩容的自由。

适用版本: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 组件现在是什么状态 ​

组件状态迁移方向
Eureka1.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 分钟内下单链路整体超时,营销、优惠券服务也被拖垮;库存恢复后,故障仍在其他服务里持续了十几分钟。库存服务本身只停机了一次。

排查:先确认请求有没有被重复发送。

定位层取证据判定
同一请求的重复 spantrace 里按 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: 8080
bash
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 提供的是一组治理能力:注册发现解决地址变化,配置中心解决改配置不重启,网关解决入口统一鉴权限流,客户端负载均衡解决多实例选择,熔断限流解决下游故障扩散,分布式事务解决跨库一致性。它们各自解决一个问题,不能互相替代。判断要不要拆,不看技术先不先进,看「独立发布、独立扩容、独立排障」的收益是否大于网络和一致性的成本;如果拆完还共库、还全链路同步串行,那只是分布式单体。

把一门技术讲透到既懂也会用