微服务架构设计实战:从单体到分布式

写代码的普通人
2025-12-14 17:24
阅读 772

上周五晚上十一点半,我正一边撸猫一边远程调一个诡异的订单状态同步 Bug,突然钉钉弹出一条消息:“明天上线前必须搞定司机接单超时降级方案”。那一刻,我真的想把 MacBook 合上直接睡觉——但不行,毕竟我是滴滴干了四年后端的老兵了,这种时候就得顶上。

说起来,我从 2019 年校招进滴滴开始,就在司机端核心业务组搬砖。四年间,眼看着我们那个“祖传单体”系统从几十个接口膨胀到上千个微服务模块,中间踩过的坑、熬过的夜、背过的锅,够写一本《微服务血泪史》了。最近不少朋友在知乎和掘金私信问我:“你们是怎么从单体拆成微服务的?Python 在里面怎么用?”、“面试官老问微服务治理,咋答?”

今天这篇博客,就结合我的实战经验,聊聊我们是怎么一步步把那个又大又臭的“单体怪兽”拆成如今这套分布式系统的。不讲虚的,全是线上真刀真枪干出来的教训,顺便也回应几个高频面试题挑战。


起因:不是我想拆,是系统快崩了

时间拉回 2021 年双11前夕。那时候我们的司机端后端还是个典型的 Python 单体应用(Django + Celery + MySQL),所有功能——接单、导航、计费、风控、消息推送——全塞在一个代码库里。听起来很美好?其实每天都在提心吊胆。

最要命的是,改个小需求,全站回归测试。有一次产品经理说“司机头像加个圆角”,结果上线后导致订单创建接口慢了 3 倍(因为头像处理逻辑意外触发了某个全局缓存刷新)。运维同事在群里疯狂@我:“P99 延迟飙到 2s+!司机端卡死了!” 那天晚上我和测试小姐姐对坐到凌晨三点,她眼神里写满了“你再改我就哭”。

更离谱的是数据库。一张 orders 表,字段快 80 个,索引堆了十几条。每次加字段都得 DBA 全程盯着,生怕锁表。有次加了个 driver_rating_score 字段,结果主库 CPU 直接 100%,整个城市司机接不到单……那事故复盘会上,我差点把键盘砸了。

领导终于拍板:“拆!必须拆成微服务!”


第一步:别一上来就搞“高大上”,先画边界

很多人一说微服务,立马想到 Spring Cloud、Service Mesh、Istio……但我们是 Python 技术栈啊!而且团队里一半人连 gRPC 都没写过。所以第一步不是选框架,而是识别业务边界

我们拉着产品、前端、测试开了三天“领域建模工作坊”(其实就是白板上乱画+奶茶续命)。最终按业务内聚性数据一致性要求,把单体拆成了几个核心域:

微服务名称 负责功能 数据库 技术栈
order-service 订单创建、状态流转 MySQL (分库) Python + FastAPI
driver-service 司机信息、在线状态 MongoDB Python + Sanic
pricing-service 动态计价、优惠券 PostgreSQL Python + Flask
notification-service 消息推送、短信 Redis + Kafka Python + Celery

注意,前端在这里起了关键作用。以前单体时代,前端(React Native)直接调一个 /api/v1/driver/order 接口,现在得分别调多个服务。所以我们约定了 BFF(Backend For Frontend)层——由前端团队维护一个 Node.js 网关,聚合下游微服务响应。这样后端不用操心前端兼容性,前端也不用发 N 个请求拼数据。


第二步:服务通信:REST 还是 RPC?

早期我们图省事,全用 HTTP/REST。结果很快发现性能瓶颈:一个接单流程要串行调 5 个服务,RTT 累积到 500ms+。司机端用户可等不了!

于是我们引入 gRPC。Python 虽然不是 gRPC 的首选语言(Go 更香),但 grpcio 库已经很成熟了。举个例子,order-servicedriver-service 获取司机位置:

// driver.proto
service DriverService {
  rpc GetDriverLocation(GetDriverLocationRequest) returns (GetDriverLocationResponse);
}

message GetDriverLocationRequest {
  string driver_id = 1;
}

message GetDriverLocationResponse {
  double latitude = 1;
  double longitude = 2;
}

生成 Python 代码后,调用方只需:

# order_service/grpc_client.py
import grpc
from driver_pb2_grpc import DriverServiceStub
from driver_pb2 import GetDriverLocationRequest

def get_driver_location(driver_id: str):
    with grpc.insecure_channel('driver-service:50051') as channel:
        stub = DriverServiceStub(channel)
        response = stub.GetDriverLocation(GetDriverLocationRequest(driver_id=driver_id))
        return (response.latitude, response.longitude)

对比 REST 的 JSON 序列化,gRPC 的 Protobuf 二进制协议体积小、解析快,平均延迟降低 40%。当然,调试麻烦了点——得用 grpcurl 或 BloomRPC,不像 Postman 那么直观。但为了性能,忍了!

🤓 面试题挑战:为什么微服务不用 HTTP 而用 RPC?
我的答案:高频、低延迟、强类型场景下,RPC 更合适。但内部管理后台这类低频操作,REST 足够且更易调试。


第三步:数据拆分:最难啃的骨头

拆服务容易,拆数据库才是地狱模式。我们遇到的最大问题是:订单和司机信息强耦合。比如订单表里存了 driver_name, driver_phone,拆库后这些字段该放哪?

方案一:冗余存储。订单库里保留司机快照。
优点:查询快;缺点:数据不一致风险高(司机改名了,历史订单还显示旧名)。

方案二:实时关联查询。每次查订单都去 driver-service 拉司机信息。
优点:数据最新;缺点:性能差,且司机服务挂了订单页就挂。

我们最终采用了 CQRS + 事件驱动

  • 写操作:订单创建时,order-service 发送 OrderCreated 事件到 Kafka
  • 读操作:driver-service 订阅事件,将司机快照写入自己的只读库
  • 查询时:订单页直接 join 本地快照表,无需跨服务调用

Kafka 消费者示例(Python + confluent-kafka):

from confluent_kafka import Consumer

def handle_order_created(event):
    driver_id = event['driver_id']
    # 调 driver-service API 获取最新司机信息
    driver_info = get_driver_info(driver_id)
    # 写入本地快照表
    save_driver_snapshot(order_id=event['order_id'], driver_info=driver_info)

conf = {'bootstrap.servers': 'kafka:9092', 'group.id': 'order-snapshot'}
consumer = Consumer(conf)
consumer.subscribe(['order-created'])

while True:
    msg = consumer.poll(1.0)
    if msg is None: continue
    event = json.loads(msg.value())
    handle_order_created(event)

这招上线后,订单详情页 P99 从 800ms 降到 120ms,而且司机服务宕机也不影响历史订单展示。当然,代价是多了套事件处理逻辑,以及偶尔的“快照延迟”——但业务方表示可以接受。


第四步:容错与熔断:别让一个服务拖垮全家

微服务最大的噩梦是什么?雪崩效应。记得去年春节,pricing-service 因为 Redis 连接池打满,响应变慢,导致 order-service 的线程池被占满,最后整个接单链路瘫痪。

血的教训告诉我们:必须做熔断降级。我们在 Python 服务里集成了 pybreaker(类似 Hystrix):

from pybreaker import CircuitBreaker

pricing_breaker = CircuitBreaker(fail_max=5, reset_timeout=60)

@pricing_breaker
def calculate_price(origin, dest):
    # 调 pricing-service
    resp = requests.post("http://pricing-service/calculate", ...)
    return resp.json()

def create_order(...):
    try:
        price = calculate_price(origin, dest)
    except CircuitBreakerError:
        # 降级:使用默认价格
        price = DEFAULT_PRICE
    # 继续创建订单...

同时,所有服务都配了 Sidecar 模式的日志和监控(虽然我们没上 Service Mesh,但用 OpenTelemetry + Jaeger 实现了链路追踪)。现在只要司机端某个接口慢,我就能在 Grafana 上看到是哪个服务拖了后腿。


性能优化彩蛋:连接池与异步

很多人以为微服务性能只靠架构,其实细节决定成败。分享两个实战技巧:

  1. 数据库连接池别瞎设
    我们曾把 max_connections 设成 100,结果高并发时大量请求排队等待连接。后来根据公式调整:

    max_connections = (CPU 核数 * 2) + 有效磁盘数
    

    实测后 QPS 提升 30%。

  2. Python 异步不是银弹
    别看 FastAPI 支持 async/await,但如果你的 DB 驱动是同步的(如 PyMySQL),异步反而更慢!我们改用 asyncpg(PostgreSQL)和 aiomysql,才真正发挥异步优势。


总结:微服务不是银弹,但值得折腾

从单体到微服务,我们花了整整 18 个月。过程中踩过无数坑:Kafka 重复消费、gRPC 版本不兼容、跨服务事务回滚……但结果是值得的:

  • 系统可用性从 99.2% 提升到 99.95%
  • 新人入职第一天就能独立开发一个微服务(不用理解整个单体)
  • 大促期间,可以单独扩容 order-service,而不用全站加机器

当然,微服务也带来了新问题:部署复杂度飙升、本地调试困难、日志分散……所以我们配套建设了 CI/CD 流水线、统一日志平台、混沌工程演练。

最后说句掏心窝的话:不要为了微服务而微服务。如果你的业务量还没到单体扛不住的地步,老老实实用 Django 多好?少掉多少头发!


面试题挑战补充

最近面试常被问:“微服务如何保证数据一致性?”

我的回答分三层:

  1. 强一致:用分布式事务(如 Seata),但性能差,慎用
  2. 最终一致:事件驱动 + 幂等消费(我们主推方案)
  3. 业务容忍:如司机头像更新延迟几秒,完全 OK

记住:没有完美的方案,只有合适的 trade-off


写完这篇博客,窗外天都亮了。赶紧提交代码,然后躺平——毕竟,程序员的快乐,就是搞定一个 Bug 后的那一口冰可乐。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝