Skip to content

元数据卡

  • 前置知识:第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:808010.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 里一行:

yaml
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高吞吐,稳定
EnvoyC++ 实现,Sidecar 模式Istio 生态
Spring Cloud GatewayJava/ReactiveJava 生态
APISIXNginx + 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
java
// 熔断器使用示例(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

单体应用中,一个操作可以跨多张表在一个数据库事务中完成:

java
@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
    ...
    如果某步骤失败,协调器发送补偿指令

协调式适合流程复杂、步骤多、需要严格控制的场景。
java
// 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:没有它,跨服务的数据一致性靠猜

常见陷阱

  1. 微服务拆得太碎。 一个服务 100 行代码,两个方法,也要单独部署——这叫微,不叫服务。运维复杂度与服务水平方增长。拆分的颗粒度应该与团队规模和变更频率匹配:一个团队能够独立维护 3-5 个服务。拆碎到 50 个微服务的团队,如果没有对应的运维能力,会在部署流水线上耗费所有精力。

  2. 没有监控就拆微服务。 拆之前先建立 Logging/Metrics/Tracing 基础设施。否则出问题你不知道是哪个服务、哪个调用链路、哪个数据源出了问题。

  3. 在服务间共享数据库。 如果两个微服务直接访问同一个数据库表,它们本质上还是一个单体——耦合在数据库模式上。每个服务拥有并仅访问自己的数据存储。如果数据需要被另一个服务访问,通过该服务的 API,不是直接读数据库。

  4. 认为微服务能提升开发速度。 微服务不能提升开发速度——它提升的是变更独立性和横向扩展能力。如果你只有 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 个实例时,这种操作方式完全不可持续。下一站:容器化与编排。

Built with VitePress | Software Systems Atlas