元数据卡
- 前置知识:第2章(RPC)、Vol 6 软件工程(CI/CD、测试策略)
- 预计时间:45 分钟
- 核心难度:进阶
- 阅读模式:高度专注
- 完成标志:能设计出一个包含服务发现、配置中心、API Gateway、熔断和 Saga 的微服务架构,理解每个组件解决的是什么问题
你的进度
你构建了一个能处理 3PB 日志的分布式计算系统,但你的部署结构还停留在"一个大包里包含所有功能"。前哨站的用户服务、情报分析、地图渲染、报告生成——它们在同一进程中运行。
林将军看着你的架构图说:"如果情报分析崩了,整个系统都崩了?一个模块出 bug,全局停机?"
你点了点头。
他接着说:"去拆分。一个堡垒管一个事,它们各自为战、协调配合。这叫阵型。"
这就是微服务。
你的任务
掌握微服务架构的核心模式和组件。不把微服务规划成"一个大项目拆成几个子项目"——你需要理解服务发现、配置管理、API 网关、熔断降级、Saga 模式背后的原理。这一章不是"怎么用 Spring Cloud",而是"为什么需要这些组件"。
破局 · 溯源
单体服务的瓶颈
你的第一个版本是一个单体应用(Monolith),代码结构大致这样:
atlas-app/
├── src/main/java/
│ ├── user/ # 用户管理
│ ├── intelligence/ # 情报分析
│ ├── map/ # 地图渲染
│ ├── report/ # 报告生成
│ └── alert/ # 警戒系统
├── src/main/resources/
│ └── application.yml
└── pom.xml单体有它的优点:一个部署单元、一个进程、一个端点、调试简单。
但随着功能膨胀:
- 修改情报分析的一个类,需要重新部署整个应用(包括完全不相关的用户管理)
- 情报分析模块需要 8GB 内存,但报告生成只需要 1GB——你在同一台机器上分配
- 警戒系统扛不住流量,你不能单独给警戒系统扩容
- 一个模块的 bug 导致 JVM OOM,整个应用挂了
微服务的思路:一个应用拆成多个独立服务,每个服务各自部署、各自扩缩、各自负责自己的数据。
服务的分裂
拆开后的架构:
atlas-sys/
├── user-service/ # 用户管理 - 2 个实例
├── intelligence-service/ # 情报分析 - 8 个实例
├── map-service/ # 地图渲染 - 4 个实例
├── report-service/ # 报告生成 - 1 个实例
└── alert-service/ # 警戒系统 - 4 个实例每个服务有自己的仓库、CI/CD 流水线、数据库或资源。但拆开后产生了一些新问题。
第一个问题:服务发现
以前单体应用是一个 IP:PORT,现在用户服务有 2 个实例(10.0.1.1:8080、10.0.1.2:8080),情报分析服务有 8 个实例。服务 B 想调用服务 A,它怎么知道 A 有哪些实例可用?
手动配置 IP 列表——但实例会在自动扩缩中增减、在故障中重启——IP 列表是动态的。
解决方案:服务注册与发现。
服务实例启动 → 注册到注册中心(把自己的 IP:PORT 写入注册中心)
服务实例下线 → 从注册中心注销
其他服务查询注册中心 → 获取可用的实例列表
注册中心类型:
| 组件 | 实现 | 数据存储 | 特点 |
|------|------|---------|------|
| etcd | Raft 共识 | 键值 | 强一致、小数据量 |
| Consul | Raft + Gossip | 键值 + 健康检查 | 自带 DNS 接口 |
| ZooKeeper | ZAB 共识 | ZNode | 成熟、Java 生态 |
| Eureka | AP(最终一致) | 内存 | Netflix 方案、允许不准确 |客户端发现 vs 服务端发现:
客户端发现(常见于 Spring Cloud):
客户端 → 查询注册中心 → 获取实例列表 → 轮询/随机选一个 → 发起调用
优点:调用链短,延迟低
缺点:每个语言/框架都要实现发现逻辑
服务端发现(常见于 K8s):
客户端 → 请求负载均衡器(如 K8s Service)→ 转发到实例
优点:客户端只需知道一个地址
缺点:额外一次网络跳转第二个问题:配置管理
以前 application.yml 里一行:
database.url: jdbc:mysql://localhost:3306/atlas现在你管理 20 个服务,每个服务有开发、测试、生产环境。改一个数据库连接串,你不会想逐个登录服务器修改配置文件。
解决方案:配置中心。
配置中心(如 etcd、Consul、ZooKeeper 或 Apollo、Nacos):
|
v
每个服务启动时从配置中心拉取配置
|
v
配置变更时,配置中心推送通知
|
v
服务收到通知 → 重新拉取配置 → 热加载(不需重启)
配置分层:
application.yml (本地基础配置)
|
配置中心 (环境特定配置: 数据库地址、日志级别)
|
运行时参数 (环境变量: K8s Pod IP、端口)关键设计:配置中心本身要高可用——它挂了你的服务启动不了。配置也应该缓存在服务本地,即使配置中心不可用,服务也能用最后一次获取的配置继续运行。
第三个问题:API 网关
客户端(Web、移动端)怎么调用 20 个服务?客户端不应该知道服务拆分细节。
如果没有网关:
客户端:
GET /user/profile
GET /intelligence/reports
POST /report/generate
GET /alert/status
... 客户端需要知道 20 个不同的端点地址解决方案:API 网关。
客户端 → API Gateway (统一入口) → 路由到后端服务
/api/user/* → user-service
/api/intelligence/* → intelligence-service
/api/report/* → report-service
API Gateway 还负责:
1. 认证 & 鉴权:检查 JWT Token
2. 限流:防止突发流量打垮后端
3. 请求聚合:一个网关请求聚合多个后端响应
4. 协议转换:外部 REST → 内部 gRPC
5. 响应缓存:减少后端压力网关模式也有争议。它引入了一个新的 Single Point of Failure(非单点,多个网关实例可解决,但仍是架构中枢)。选择网关组件时:
| 组件 | 特点 | 适用 |
|---|---|---|
| Kong | 基于 Nginx + Lua | 高吞吐,稳定 |
| Envoy | C++ 实现,Sidecar 模式 | Istio 生态 |
| Spring Cloud Gateway | Java/Reactive | Java 生态 |
| APISIX | Nginx + Lua + etcd | 高性能,Apache 项目 |
第四个问题:熔断与降级
服务 A 调用服务 B,服务 B 挂了。通常的做法是:A 重试几次,然后抛出 500。
在微服务中,一个 B 的故障如果不隔离,会像多米诺骨牌一样传播:
A 调 B(B 挂了)→ A 的线程池被阻塞等待
→ 所有调用 A 的请求也被阻塞
→ 调用 A 的前端 C 也被阻塞
→ C 的线程池耗尽
→ 整个系统挂掉这就是级联故障。
解决方案:熔断器模式(Circuit Breaker)。
熔断器三种状态:
+──────────+
| CLOSED | ← 正常状态:请求直接通过
+─────┬────+
| 失败计数超过阈值
v
+──────────+
| OPEN | ← 熔断状态:请求直接返回失败,不调用后端
+─────┬────+
| 超时(如 5s)
v
+──────────+
| HALF-OPEN | ← 半开状态:放一个请求试探
+─────┬────+
| |
成功 → 回到 CLOSED 失败 → 回到 OPEN// 熔断器使用示例(Resilience4j,Java)
// Resilience4j 是 Spring Cloud Circuit Breaker 默认实现
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import java.time.Duration;
// 配置熔断器
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50% 请求失败时熔断
.slidingWindowSize(10) // 最近 10 次请求为窗口
.minimumNumberOfCalls(5) // 最少 5 次才触发熔断判断
.waitDurationInOpenState(Duration.ofSeconds(5)) // 熔断持续 5 秒
.permittedNumberOfCallsInHalfOpenState(3) // 半开时允许 3 次探测
.build();
CircuitBreakerRegistry registry =
CircuitBreakerRegistry.of(config);
CircuitBreaker cb = registry.circuitBreaker("intelligence-service");
// 使用熔断器包裹远程调用
Supplier<String> decorated = CircuitBreaker
.decorateSupplier(cb, () -> intelligenceClient.getReport());
// 如果熔断器 OPEN,这里不会调用 getReport(),而是抛出异常
String report = Try.ofSupplier(decorated)
.recover(throwable -> "fallback report: default")
.get();降级(Degradation) 是熔断后的策略——不是等系统崩了再处理,而是主动降低服务质量:
- 情报分析服务挂了 → 显示最后一次缓存的情报摘要
- 推荐引擎挂了 → 显示默认推荐
- 警戒系统超时 → 跳过实时分析,改为定期批处理
降级比报错好——用户看到"部分功能不可用"比看到"500 Internal Server Error"舒服得多。
第五个问题:分布式事务与 Saga
单体应用中,一个操作可以跨多张表在一个数据库事务中完成:
@Transactional
public void transferScout(String scoutId, String fromOutpost, String toOutpost) {
userRepo.updateOutpost(scoutId, toOutpost);
outpostRepo.removeScout(fromOutpost, scoutId);
outpostRepo.addScout(toOutpost, scoutId);
logRepo.createTransferLog(scoutId, fromOutpost, toOutpost);
}在微服务中,每个服务有自己的数据库。上述跨多个服务的操作无法用一个本地事务完成。
Saga 模式:把一个全局事务拆成一系列本地事务,每个本地事务执行完后发布一个事件,触发下一个本地事务。如果中间某一步失败,执行补偿操作(回滚)。
Saga 两种协调方式:
编排式(Choreography):
user-service 更新用户归属 → 发送事件
→ outpost-service 监听事件,移除/添加驻地 → 发送事件
→ log-service 监听事件,创建日志
编排式适合步骤少、逻辑简单的流程。
编排式的问题:当流程变长(>5步),事件链的因果关系难以追踪。新增步骤需要修改多个服务的监听逻辑。
---
协调式(Orchestration):
Saga 协调器(Orchestrator)控制每一步:
1. 协调器发送 "transferScout" 命令到 user-service
2. user-service 执行本地事务,回复 "done"
3. 协调器发送 "removeScout" 命令到 outpost-service
4. outpost-service 执行,回复 "done"
5. 协调器发送 "addScout" 命令到 outpost-service
...
如果某步骤失败,协调器发送补偿指令
协调式适合流程复杂、步骤多、需要严格控制的场景。// Saga 协调器示例(使用 Axon Framework 或类似框架的概念代码)
// Saga 协调器负责编排多个微服务的本地事务
public class TransferSaga {
@Saga
public void handle(TransferScoutCommand cmd) {
// 第1步:更新用户驻地
send(new UpdateUserOutpostCommand(cmd.scoutId, cmd.toOutpost));
// 等待 user-service 完成
waitFor(UpdateUserOutpostCompletedEvent.class)
.onMatch(event -> {
// 第2步:移除来源驻地
send(new RemoveScoutCommand(cmd.fromOutpost, cmd.scoutId));
});
// 等待 outpost-service 完成
waitFor(RemoveScoutCompletedEvent.class)
.onMatch(event -> {
// 第3步:添加目标驻地
send(new AddScoutCommand(cmd.toOutpost, cmd.scoutId));
});
// 如果任何一步失败,发送补偿指令
onFailure(TransferScoutFailedEvent.class)
.then(compensateWith(new RollbackTransferCommand(cmd.scoutId, cmd.fromOutpost, cmd.toOutpost)));
}
}Saga 不像数据库事务那样提供 ACID——它提供 BASE(Basically Available, Soft state, Eventually consistent)。最终一致性是微服务中分布式事务的默认模型。
总结:微服务组件的全景
客户端
|
[API Gateway]
/ | \
/ | \
user-svc intel-svc report-svc
| | |
[DB/user] [DB/intel] [DB/report]
| | |
+--+----------+----------+--+
| 配置中心 |
| 服务发现(注册中心) |
+---------------------------+不只有这些组件,但缺少任何一个,微服务体系就不完整:
- 服务发现:没有它,实例变化无法感知
- 配置中心:没有它,改配置叫运维
- API 网关:没有它,客户端直接暴露内部拓扑
- 熔断器:没有它,一个服务挂了全系统瘫痪
- Saga:没有它,跨服务的数据一致性靠猜
常见陷阱
微服务拆得太碎。 一个服务 100 行代码,两个方法,也要单独部署——这叫微,不叫服务。运维复杂度与服务水平方增长。拆分的颗粒度应该与团队规模和变更频率匹配:一个团队能够独立维护 3-5 个服务。拆碎到 50 个微服务的团队,如果没有对应的运维能力,会在部署流水线上耗费所有精力。
没有监控就拆微服务。 拆之前先建立 Logging/Metrics/Tracing 基础设施。否则出问题你不知道是哪个服务、哪个调用链路、哪个数据源出了问题。
在服务间共享数据库。 如果两个微服务直接访问同一个数据库表,它们本质上还是一个单体——耦合在数据库模式上。每个服务拥有并仅访问自己的数据存储。如果数据需要被另一个服务访问,通过该服务的 API,不是直接读数据库。
认为微服务能提升开发速度。 微服务不能提升开发速度——它提升的是变更独立性和横向扩展能力。如果你只有 3 个开发者和一个 5000 行代码的项目,单体是更好的选择。
通关挑战
热身:画出你当前项目(或上一份工作)的服务依赖图。找出级联故障的可能路径。在图上标出应该在哪些位置加熔断器。
挑战:用 Spring Cloud Gateway 或 Kong 搭建一个 API 网关。后端运行两个 mock 服务,网关配置路由、限流和鉴权。验证限流生效时客户端收到 429。
观察:启动三个模拟服务:A 调 B、B 调 C。在 C 中模拟随机故障(每次请求有 30% 概率超时),在 A 中配置熔断器。观察熔断器状态从 CLOSED 变为 OPEN 再变回 HALF-OPEN 的过程。记录状态变化时的失败率和恢复时间。
旅人笔记
微服务不是银弹,它是取舍。用运维复杂度换取独立部署、独立扩缩、独立故障边界。服务发现、配置中心、API 网关、熔断、Saga——这些组件不是"Spring Cloud 的东西",它们是微服务体系必须回答的问题。当你面对"要不要拆"时,先想清楚当前系统最大的痛点是什么——如果是部署太麻烦,拆微服务不是唯一答案,先试试更好的 CI/CD。
下一站预告
你有了 20 个微服务,每个部署在 2-8 个实例上。你要手动管理这些实例:登录服务器、部署 jar 包、配置环境变量、重启服务。50 个实例还行,但当你面对 500 个实例时,这种操作方式完全不可持续。下一站:容器化与编排。