FDE Training · Module 14

14

微服务工程落地 · 从单体到服务拆分的实战路线

← 返回 FDE 培养总览

模块 14 · 微服务工程落地

本章目标:当客户系统从"一个大后端"长成"十几个服务"时,FDE 要能亲手拆分、部署、治理、排障。不讲微服务理论大全,只讲 FDE 在客户现场最常用的那套落地姿势:服务怎么拆、服务间怎么调用、分布式事务怎么兜底、网关和注册中心怎么配。


API 网关 路由/鉴权/限流 用户服务 user-service 订单服务 order-service 注册中心/配置中心 Nacos / Consul
微服务三件套:网关入口 → 业务服务 → 注册中心寻址

14.1 为什么要拆:单体的痛 vs 微服务的代价

客户最初的系统往往是一个 Spring Boot / FastAPI 单体。随着业务涨,开始痛:

  • 发布越来越慢:改一个小功能要整包部署,回归测试全跑一遍。
  • 互相影响:订单模块内存泄漏,把用户模块也拖垮。
  • 团队协作打架:十个人改一个 Git 仓库,合并冲突天天有。

微服务的解法是把大应用按业务域拆成多个小服务,每个服务独立部署、独立数据库、独立扩缩容。但代价是:分布式复杂度全来了——网络调用会失败、数据一致性难保证、排障要跨服务追。

FDE 心法:不要为了拆而拆。客户系统日活不高、团队就三五个人,单体模块化比微服务更划算。微服务是"业务规模和团队规模到了那个点"之后的自然选择。


14.2 怎么拆:按业务边界,不按技术分层

最常见的拆法错误是按技术层拆:把"controller 层"拆一个服务、"service 层"拆一个服务——这纯属灾难,每次业务改动都要跨服务改三个地方。

正确姿势是按业务域拆(DDD 限界上下文):

服务职责自己的数据库表
user-service注册、登录、用户资料、权限users, roles, sessions
order-service下单、订单状态流转、支付回调orders, order_items
payment-service对接微信/支付宝、对账payments, refunds
inventory-service库存扣减、回补stocks, stock_logs
notification-service短信、邮件、站内信messages, templates

关键原则:一个服务独占一个数据库 schema,别的服务不能直接连它的表,只能通过 API 或消息拿数据。这叫"数据库 per 服务",是微服务自治的底线。


14.3 服务间怎么调用:同步 HTTP vs 异步消息

14.3.1 同步调用(HTTP / gRPC)

订单服务要扣库存,直接调 inventory-service 的接口:

# order-service 内部,下单时同步调库存服务
import requests

def create_order(user_id, items):
    # 1. 先扣库存(同步 HTTP 调用)
    for it in items:
        resp = requests.post(
            "http://inventory-service.internal/stocks/deduct",
            json={"sku": it["sku"], "qty": it["qty"]},
            timeout=3.0,   # 必须设超时!
        )
        if resp.status_code != 200:
            raise BusinessError(f"sku {it['sku']} 库存不足")
    # 2. 写订单
    order = Order(user_id=user_id, items=items)
    db.session.add(order)
    db.session.commit()
    return order

同步调用的三个坑(必背):

  1. 没设超时:下游卡住,线程池耗尽,整个服务雪崩。timeout=3.0 是底线。
  2. 没重试/没熔断:下游短暂抖动就直接报错给用户。要加重试(只对幂等操作)和熔断(Hystrix/Resilience4j)。
  3. 循环依赖:A 调 B、B 调 A,发版顺序都排不出来。架构评审时就要禁掉。

14.3.2 异步消息(MQ)

下单成功后要发短信通知用户——这种"非核心路径"用异步消息解耦:

# order-service:下单成功后发一条消息,不等人
from kafka import KafkaProducer
producer = KafkaProducer(bootstrap_servers="kafka:9092")

def on_order_paid(order):
    producer.send("order.events", value={
        "event": "ORDER_PAID",
        "order_id": order.id,
        "user_id": order.user_id,
    }.encode())
    # 立刻返回,不等短信服务处理
# notification-service:订阅消息,慢慢处理
from kafka import KafkaConsumer
consumer = KafkaConsumer("order.events", bootstrap_servers="kafka:9092", group_id="notify")
for msg in consumer:
    event = json.loads(msg.value)
    if event["event"] == "ORDER_PAID":
        send_sms(event["user_id"], f"您的订单 {event['order_id']} 已支付")

异步的好处:削峰、解耦、最终一致。坏处:调试难、消息可能重复(要做消费端幂等)。


14.4 分布式事务:别追求强一致,用最终一致

下单要同时写 orders 表和扣 stocks 表——跨服务了,本地事务管不到。常见三招:

方案怎么做适用场景
本地消息表订单服务在同一个本地事务里写 orders + 写一条 outbox 消息,后台任务扫 outbox 发 MQ最常用,可靠
TCCTry-Confirm-Cancel 三步,每个服务实现两个接口资金类强一致场景,成本高
Saga一串本地事务,每步失败就反向补偿长流程(如旅行预订)

FDE 在客户项目里 90% 用本地消息表,简单可靠。代码骨架:

@app.route("/orders", methods=["POST"])
def create_order():
    data = request.json
    with db.transaction():
        order = Order(**data)
        db.add(order)
        # 同一个事务里写 outbox
        db.add(Outbox(
            topic="order.events",
            payload=json.dumps({"event": "ORDER_CREATED", "order_id": order.id}),
        ))
    return {"order_id": order.id}

# 后台 worker:定时扫 outbox,发到 MQ,发成功了标记 sent
def outbox_relay():
    pending = Outbox.query.filter_by(sent=False).limit(100).all()
    for o in pending:
        kafka.send(o.topic, o.payload)
        o.sent = True
        db.commit()

14.5 服务怎么找到彼此:注册中心 + 网关

服务 IP 是动态的(K8s Pod 重启就换),不能写死在配置里。需要注册中心:

  • 每个服务启动时把自己的地址注册到 Nacos / Consul / etcd。
  • 调用方从注册中心拿对端地址列表,做负载均衡。
  • 服务挂了,注册中心心跳检测自动摘除。

API 网关是所有外部请求的统一入口:

# Nginx 网关路由示例
upstream user_service {
    server user-svc:8000;
}
upstream order_service {
    server order-svc:8001;
}
server {
    listen 80;
    location /api/users/  { proxy_pass http://user_service; }
    location /api/orders/ { proxy_pass http://order_service; }
    # 统一鉴权
    location /api/ {
        auth_request /internal/auth;
        proxy_pass http://backend;
    }
}

网关干的事:路由转发、统一鉴权、限流熔断、日志追踪、协议转换。业务服务本身不用关心这些横切关注点。


14.6 分布式追踪:一个请求跨了 5 个服务怎么查

单体时看一个日志文件就够了。微服务时一个下单请求可能经过网关→order→inventory→payment→notification,日志散在五台机器。必须上分布式追踪:

  • 每个请求进来时生成一个 trace_id,通过 HTTP header / MQ 消息透传。
  • 每个服务在日志里打印这个 trace_id。
  • 用 Jaeger / Zipkin / OpenTelemetry 把同 trace_id 的跨度串起来,可视化看到请求链路和耗时。
# FastAPI 中间件:每个请求注入 trace_id
import uuid
@app.middleware("http")
async def add_trace(request, call_next):
    trace_id = request.headers.get("X-Trace-Id") or str(uuid.uuid4())
    request.state.trace_id = trace_id
    response = await call_next(request)
    response.headers["X-Trace-Id"] = trace_id
    return response

14.7 动手练习(可折叠答案)

练习 1:给下面这个单体模块列表,圈出你会怎么拆成微服务

答案思路:典型电商单体里有——用户管理、商品管理、购物车、订单、支付、库存、优惠券、消息通知。

  • 第一刀:用户服务、商品服务、订单服务(核心三域)。
  • 第二刀:支付服务、库存服务(独立对账/扣减逻辑重)。
  • 第三刀:通知服务(纯异步,最容易拆)。
  • 购物车可以先跟订单放一起,优惠券跟商品放一起——不要一次拆太碎。

原则:先拆变化频繁、负载独立、边界清晰的;稳定的、简单的先留单体里。

练习 2:order-service 调 inventory-service 超时了,写出你的处理策略

答案思路:

  1. 设超时(如 2 秒),不能无限等。
  2. 失败重试 1~2 次(扣库存是写操作,要先确认是否幂等,否则不能盲目重试)。
  3. 重试还失败:熔断打开,快速失败给用户"系统繁忙请稍后",不要把线程池占满。
  4. 记录告警,通知值班。
  5. 如果库存服务是关键依赖,考虑降级:先创建订单待支付,后台异步补扣库存,失败了再关单。
练习 3:设计一个"下单后发站内信"的异步链路,保证消息不丢

答案思路:用本地消息表模式:

  1. 下单事务里同时写 orders 表和 outbox 表。
  2. outbox relay 进程轮询 outbox,发到 Kafka。
  3. Kafka 消息持久化,broker 挂了也不丢。
  4. notification-service 消费消息,处理完手动提交 offset。
  5. 消费端做幂等:message_id 唯一约束,重复消息直接跳过。

14.8 本章面试题

  1. "微服务是不是一定比单体好?"→ 答:不是。小团队、业务简单时单体更高效;微服务带来的分布式成本(网络、事务、排障)只有在业务规模和团队规模上去后才划算。
  2. "服务拆分怎么避免循环依赖?"→ 答:按业务域拆,建立依赖方向图(如 user 是最底层,order 依赖 user,不能反过来);架构评审时强制检查;公共逻辑下沉到共享库或独立基础服务。
  3. "跨服务数据一致性怎么处理?"→ 答:多数场景接受最终一致,用本地消息表 + MQ 兜底;资金类强一致才上 TCC;长流程用 Saga 补偿。
  4. "一个请求跨 5 个服务,怎么排查慢在哪?"→ 答:分布式追踪(trace_id 透传 + Jaeger),看每个 span 的耗时;结合日志和 metrics 定位是哪个服务/哪一步慢。

14.9 小结

  • 微服务是规模到了的自然选择,不是目的本身。
  • 按业务域拆,一个服务一个库,服务间只通过 API/MQ 通信。
  • 同步调用必设超时+重试+熔断;非核心路径用异步消息解耦。
  • 分布式一致性靠最终一致(本地消息表最实用),别硬上强一致。
  • 注册中心 + 网关 + 分布式追踪是微服务治理的三件套。
本页由 FDE 培养课程文档生成,完整课程见 FDE 培养 · 课程总览。