Architecture · #architecture#microservices#patterns

微服务架构模式:从单体到分布式

2021.10.25 8 min 3.3k
// 目录 · contents

前言

随着业务规模的增长,单体架构逐渐暴露出部署困难、扩展受限、技术栈锁定等问题。微服务架构通过将系统拆分为一组小型、自治的服务来解决这些问题。但微服务并非银弹,它引入了分布式系统的复杂性。本文将系统性地梳理微服务架构中的核心模式,帮助你在实际项目中做出正确的架构决策。

单体架构 vs 微服务架构

单体架构的特征

单体架构将所有功能模块打包在一个部署单元中,共享同一个数据库。它的优势在于开发简单、部署方便、调试直观。

1
2
3
4
5
6
7
8
9
// 典型的单体应用结构
@SpringBootApplication
public class MonolithApplication {
// 所有模块在同一个进程中
// - UserModule
// - OrderModule
// - PaymentModule
// - InventoryModule
}

单体架构的痛点

当团队规模超过 10 人、代码量超过 50 万行时,单体架构的问题开始显现:

  1. 部署耦合:修改一行代码需要重新部署整个应用
  2. 扩展受限:无法针对热点模块单独扩容
  3. 技术锁定:所有模块必须使用相同的技术栈
  4. 故障传播:一个模块的 OOM 会导致整个应用崩溃

微服务架构全景

graph TB
    Client[客户端] --> Gateway[API Gateway]

    Gateway --> UserService[用户服务]
    Gateway --> OrderService[订单服务]
    Gateway --> PaymentService[支付服务]
    Gateway --> InventoryService[库存服务]

    UserService --> UserDB[(用户DB)]
    OrderService --> OrderDB[(订单DB)]
    PaymentService --> PaymentDB[(支付DB)]
    InventoryService --> InventoryDB[(库存DB)]

    OrderService -->|异步消息| MQ[消息队列]
    MQ --> PaymentService
    MQ --> InventoryService

    subgraph Service Mesh
        UserService
        OrderService
        PaymentService
        InventoryService
    end

服务分解策略

按业务能力分解(Business Capability)

根据企业的业务能力来划分服务边界。业务能力是企业为产生价值而做的事情。

graph LR
    subgraph 电商平台
        subgraph 商品管理
            A[商品目录服务]
            B[库存服务]
        end
        subgraph 订单管理
            C[订单服务]
            D[物流服务]
        end
        subgraph 用户管理
            E[账户服务]
            F[认证服务]
        end
        subgraph 支付管理
            G[支付服务]
            H[结算服务]
        end
    end

按子域分解(DDD Subdomain)

基于领域驱动设计(DDD)的限界上下文来定义服务边界,这是目前最被推荐的分解方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 订单上下文中的 Order 聚合根
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderItem> items;
private OrderStatus status;
private Money totalAmount;

public void place() {
if (items.isEmpty()) {
throw new OrderException("Order must have at least one item");
}
this.status = OrderStatus.PLACED;
registerEvent(new OrderPlacedEvent(this.id, this.customerId, this.totalAmount));
}

public void cancel() {
if (status != OrderStatus.PLACED) {
throw new OrderException("Only placed orders can be cancelled");
}
this.status = OrderStatus.CANCELLED;
registerEvent(new OrderCancelledEvent(this.id));
}
}

分解原则

  • 单一职责:每个服务只负责一个业务能力
  • 高内聚低耦合:服务内部紧密相关,服务之间松散依赖
  • 数据自治:每个服务拥有并管理自己的数据库
  • 合理粒度:不要过度拆分,一个小团队(2-pizza team)能维护一个服务

API Gateway 模式

API Gateway 是微服务架构中的核心基础设施,它作为系统的统一入口,承担请求路由、协议转换、认证授权、限流熔断等职责。

sequenceDiagram
    participant C as 客户端
    participant G as API Gateway
    participant A as 认证服务
    participant O as 订单服务
    participant U as 用户服务

    C->>G: GET /api/orders/123
    G->>A: 验证 Token
    A-->>G: Token 有效, userId=456

    par 并行调用
        G->>O: GET /orders/123
        G->>U: GET /users/456
    end

    O-->>G: 订单数据
    U-->>G: 用户数据
    G->>G: 聚合响应数据
    G-->>C: 完整订单信息(含用户)

BFF 模式(Backend for Frontend)

不同的客户端(Web、移动端、第三方)有不同的数据需求,可以为每种客户端提供专属的 API Gateway。

graph TB
    Web[Web App] --> WebBFF[Web BFF]
    Mobile[Mobile App] --> MobileBFF[Mobile BFF]
    ThirdParty[第三方] --> OpenAPI[Open API Gateway]

    WebBFF --> Services[微服务集群]
    MobileBFF --> Services
    OpenAPI --> Services
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Nginx 作为简单 API Gateway 的配置示例
upstream order-service {
server order-1:8080;
server order-2:8080;
}

upstream user-service {
server user-1:8080;
server user-2:8080;
}

server {
listen 80;

location /api/orders {
# 限流配置
limit_req zone=api burst=20 nodelay;
proxy_pass http://order-service;
proxy_set_header X-Request-ID $request_id;
}

location /api/users {
limit_req zone=api burst=20 nodelay;
proxy_pass http://user-service;
proxy_set_header X-Request-ID $request_id;
}
}

Service Mesh 模式

Service Mesh 将服务间通信的复杂性从应用代码中剥离出来,下沉到基础设施层。每个服务实例旁边部署一个 Sidecar 代理,由它负责所有的网络通信。

graph TB
    subgraph Pod A
        ServiceA[服务 A] <--> ProxyA[Envoy Proxy]
    end

    subgraph Pod B
        ServiceB[服务 B] <--> ProxyB[Envoy Proxy]
    end

    subgraph Pod C
        ServiceC[服务 C] <--> ProxyC[Envoy Proxy]
    end

    ProxyA <-->|mTLS| ProxyB
    ProxyB <-->|mTLS| ProxyC
    ProxyA <-->|mTLS| ProxyC

    ControlPlane[控制平面 Istiod] -->|配置下发| ProxyA
    ControlPlane -->|配置下发| ProxyB
    ControlPlane -->|配置下发| ProxyC

Service Mesh 提供的核心能力: - 流量管理:负载均衡、熔断、超时、重试、灰度发布 - 安全通信:mTLS 加密、访问控制 - 可观测性:分布式追踪、指标采集、访问日志

Saga 模式

在微服务架构中,跨服务的事务不能使用传统的 ACID 事务。Saga 模式通过一系列本地事务和补偿操作来维护数据一致性。

编排式 Saga(Orchestration)

由一个中央协调器(Orchestrator)来控制整个 Saga 的执行流程。

sequenceDiagram
    participant Orch as Saga Orchestrator
    participant Order as 订单服务
    participant Payment as 支付服务
    participant Inventory as 库存服务
    participant Delivery as 物流服务

    Orch->>Order: 创建订单
    Order-->>Orch: 订单已创建

    Orch->>Payment: 扣款
    Payment-->>Orch: 扣款成功

    Orch->>Inventory: 扣减库存
    Inventory-->>Orch: 库存不足!

    Note over Orch: 触发补偿流程
    Orch->>Payment: 退款(补偿)
    Payment-->>Orch: 退款成功
    Orch->>Order: 取消订单(补偿)
    Order-->>Orch: 订单已取消
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Saga 编排器示例
public class CreateOrderSaga {
private final SagaDefinition<CreateOrderState> sagaDefinition;

public CreateOrderSaga() {
this.sagaDefinition = SagaDefinition.<CreateOrderState>builder()
.step()
.invokeParticipant(this::createOrder)
.withCompensation(this::cancelOrder)
.step()
.invokeParticipant(this::processPayment)
.withCompensation(this::refundPayment)
.step()
.invokeParticipant(this::reserveInventory)
.withCompensation(this::releaseInventory)
.step()
.invokeParticipant(this::arrangeDelivery)
.build();
}

private CommandWithDestination createOrder(CreateOrderState state) {
return send(new CreateOrderCommand(state.getOrderDetails()))
.to("order-service")
.build();
}

private CommandWithDestination cancelOrder(CreateOrderState state) {
return send(new CancelOrderCommand(state.getOrderId()))
.to("order-service")
.build();
}
}

协同式 Saga(Choreography)

没有中央协调器,每个服务监听事件并决定下一步操作。

graph LR
    A[订单服务] -->|OrderCreated| B[支付服务]
    B -->|PaymentCompleted| C[库存服务]
    C -->|InventoryReserved| D[物流服务]

    C -->|InventoryFailed| B
    B -->|PaymentRefunded| A
    A -->|OrderCancelled| E[结束]

编排式适合流程复杂、参与者较多的场景,便于理解和维护。协同式适合简单流程,避免了单点故障,但流程复杂时难以追踪。

事件驱动架构

事件驱动架构(EDA)是微服务间通信的核心模式之一,它实现了服务间的松耦合。

graph LR
    subgraph 生产者
        OrderService[订单服务]
        UserService[用户服务]
    end

    subgraph 事件总线
        Topic1[order-events]
        Topic2[user-events]
    end

    subgraph 消费者
        Analytics[分析服务]
        Notification[通知服务]
        Search[搜索服务]
    end

    OrderService -->|publish| Topic1
    UserService -->|publish| Topic2

    Topic1 -->|subscribe| Analytics
    Topic1 -->|subscribe| Notification
    Topic1 -->|subscribe| Search
    Topic2 -->|subscribe| Analytics
    Topic2 -->|subscribe| Notification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 领域事件定义
public record OrderPlacedEvent(
String orderId,
String customerId,
BigDecimal totalAmount,
List<OrderItem> items,
Instant occurredAt
) implements DomainEvent {}

// 事件发布
@Service
public class OrderService {
private final EventPublisher eventPublisher;

@Transactional
public Order placeOrder(PlaceOrderCommand cmd) {
Order order = Order.create(cmd);
orderRepository.save(order);

// 使用 Transactional Outbox 模式保证可靠发布
outboxRepository.save(new OutboxMessage(
"order-events",
order.getId(),
new OrderPlacedEvent(order.getId(), cmd.customerId(), order.totalAmount(), order.getItems(), Instant.now())
));

return order;
}
}

// 事件消费
@Component
public class InventoryEventHandler {
@EventListener(topics = "order-events")
public void handleOrderPlaced(OrderPlacedEvent event) {
for (OrderItem item : event.items()) {
inventoryService.reserve(item.productId(), item.quantity());
}
}
}

绞杀者模式(Strangler Fig Pattern)

绞杀者模式是将单体架构逐步迁移到微服务架构的最佳策略。就像绞杀榕缠绕宿主树一样,新的微服务逐渐替代旧系统的功能。

graph TB
    subgraph "阶段1: 初始状态"
        Client1[客户端] --> Monolith1[单体应用<br/>用户/订单/支付/库存]
    end

    subgraph "阶段2: 开始迁移"
        Client2[客户端] --> Facade2[反腐层/Facade]
        Facade2 --> Monolith2[单体应用<br/>用户/支付/库存]
        Facade2 --> OrderMS2[订单微服务]
    end

    subgraph "阶段3: 逐步替换"
        Client3[客户端] --> Facade3[API Gateway]
        Facade3 --> Monolith3[单体应用<br/>库存]
        Facade3 --> OrderMS3[订单微服务]
        Facade3 --> UserMS3[用户微服务]
        Facade3 --> PayMS3[支付微服务]
    end

    subgraph "阶段4: 完成迁移"
        Client4[客户端] --> Gateway4[API Gateway]
        Gateway4 --> OrderMS4[订单微服务]
        Gateway4 --> UserMS4[用户微服务]
        Gateway4 --> PayMS4[支付微服务]
        Gateway4 --> InvMS4[库存微服务]
    end

实施步骤

  1. 识别边界:在单体中找到可以独立拆分的模块
  2. 建立反腐层:在新旧系统之间建立适配层
  3. 逐步迁移:将流量从旧模块路由到新服务
  4. 数据迁移:将数据从共享数据库迁移到服务专属数据库
  5. 退役旧代码:确认新服务稳定后,删除单体中的对应模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 使用 Nginx 实现流量切换
upstream monolith {
server monolith:8080;
}

upstream order-microservice {
server order-service:8080;
}

# 将订单相关请求路由到新的微服务
location /api/orders {
proxy_pass http://order-microservice;
}

# 其他请求仍然路由到单体应用
location / {
proxy_pass http://monolith;
}

微服务架构决策框架

在采用微服务之前,需要回答以下问题:

维度 适合微服务 适合单体
团队规模 > 10 人 < 10 人
系统复杂度 多个业务域 单一业务域
发布频率 各模块独立发布 统一发布即可
扩展需求 各模块扩展需求不同 整体扩展即可
技术多样性 需要不同技术栈 统一技术栈
运维能力 有 DevOps 文化和工具链 运维能力有限

总结

微服务架构不是目标,而是手段。在实施微服务架构时,需要根据团队能力、业务复杂度和技术基础设施来选择合适的模式:

  • API Gateway 解决统一入口和请求聚合的问题
  • Service Mesh 将通信复杂性下沉到基础设施层
  • Saga 模式 解决分布式事务的一致性问题
  • 事件驱动架构 实现服务间的松耦合
  • 绞杀者模式 提供从单体到微服务的平滑迁移路径

记住 Martin Fowler 的建议:Monolith First。先用单体架构快速验证业务,在确认需要时再逐步演进到微服务。


踩坑记录

我们第一次服务拆分,按数据库表来拆,把用户模块拆成了「用户基础信息服务」、「用户地址服务」、「用户积分服务」三个。结果一个「查询用户完整信息」的接口要调用 3 个服务,N+1 查询问题比单体时代更严重,链路延迟从 20ms 变成 200ms,还多了 3 倍的网络故障点。

根本原因是按表拆而不是按业务能力拆。表是实现细节,不是业务边界。

后来重新梳理,按照 DDD 限界上下文重新划分:User Profile(身份/地址)、User Behavior(积分/历史)、User Account(余额/支付)。每个服务内部数据完整,跨服务只有事件通知。重新拆完后,核心接口从 3 次 RPC 变回 1 次,延迟降到 12ms,故障影响范围也大幅缩小。

实测结果

阶段 部署窗口 核心接口 P99 服务间依赖数 故障影响范围
单体 每两周一次,全量发布 20ms 0 全部功能
按表拆分后 可独立,但耦合严重 200ms 3 次 RPC 仍波及全链路
按业务能力拆分后 完全独立 12ms 1 次 RPC 故障隔离有效

我的看法

「按表拆」是微服务拆分最常见的错误。DDD 的限界上下文真正有价值的地方在于:它告诉你什么数据应该在同一个服务里,以及服务间的数据冗余是被允许的

不要为了「单一职责」而拆到每个服务只有一张表。微服务的边界应该和团队边界对齐(Conway’s Law),一个团队能独立负责、独立发布的才是合理的服务边界。如果两个服务经常需要同时修改、同时发布,那它们根本就不应该被拆开。

作者 · authorzt
发布 · date2021-10-25
篇幅 · length3.3k 字 · 8 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论