KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

01 · 架构选型的取舍:从单体到异步服务化 — keel 龙骨

这一章回答:一个中型后端系统该怎么分,以及每一个中间件到底解决了什么问题。

这一章回答:一个中型后端系统该怎么分,以及每一个中间件到底解决了什么问题。

后端"拔高"的第一步不是学更多框架,而是能在选择面前说清代价。绝大多数系统不需要微服务,但几乎每个中型系统都需要异步化、分级缓存、故障隔离这三件事。

一、演进路径:按痛点推进,不按潮流推进

单体(垂直分层)
   │ 痛点:一处慢,全站慢;发布互相阻塞
   ▼
水平拆分(多实例 + 负载均衡)
   │ 痛点:同步链路太长,慢依赖拖垮整体
   ▼
异步化(MQ 解耦非核心链路)
   │ 痛点:不同子系统资源需求不同、故障会传染
   ▼
按域拆分 + 故障隔离(熔断、限流、降级)

判断依据永远是当前的痛点是什么,而不是"大厂都在用 XX"。跳过痛点的架构升级,只会把复杂度提前引入而没有换来收益。

二、技术选型的取舍表

以一个 FastAPI 中型服务(日活十万级、千级 QPS)为例:

组件 选型 解决什么 什么情况下不该选
应用框架 FastAPI 异步 IO + 类型化契约,天然适合 IO 密集 CPU 密集场景,异步收益有限甚至更差
数据库 PostgreSQL / MySQL 事务与强一致 数据模型不稳定时先别急着上复杂 schema
缓存 Redis 热点读、分布式锁、流 数据量超过内存、要求强持久的主数据
可靠消息 RabbitMQ 需要确认机制、路由灵活、消息不丢 百万级吞吐、日志流场景
高吞吐消息 Kafka 分区并行、可回放、吞吐高 需要复杂路由、单条消息级的确认语义
任务队列 ARQ / Celery 后台异步任务 已经用了 MQ 且需求简单时不必再引入
部署 K8s + HPA 按负载自动扩缩 单机足够时,K8s 只会增加运维负担

一句话记忆:RabbitMQ 保可靠、Kafka 保吞吐。很多团队用错的原因,是把"日志管道"和"业务消息"当成了一件事。

三、四条贯穿全局的原则

  1. 分层解耦:接口层不做业务规则,服务层不直接碰 HTTP 细节,数据访问层收口 SQL。好处是任何一层可以单独替换和测试。
  2. 异步优先:主链路只保留必须同步完成的部分(写库、鉴权、返回关键结果),其余一律投递消息后返回。
  3. 缓存分级:本地缓存(进程内,纳秒级,无网络开销)→ Redis(跨实例共享)→ DB。级别越高越贵,能不上就不上。
  4. 故障隔离:任何外部依赖都可能失败,所有调用点都要有超时、重试上限和降级路径。

四、最小可用架构

不必一上来堆全套。一个能承载千级 QPS 的最小组合是:

客户端 → Nginx → uvicorn 多 worker(FastAPI)
                   ├─ Redis(缓存 + 分布式锁 + 流)
                   ├─ 主库(强一致写)
                   ├─ 只读实例(查询与报表)
                   └─ MQ(非核心链路异步化)
       └─ 独立 Worker 进程消费 MQ(失败进死信)

关键点是worker 与应用进程分开:它的崩溃不该影响在线请求,它的积压也不该拖垮接口延迟。

五、应该问自己的三个问题

  1. 我系统里最慢的那个外部调用在哪里?如果它挂了,主链路还能返回吗?
  2. 我引入的每个中间件,分别替代了我自己写的哪段(容易出错的)代码?如果答不上来,它就是多余的复杂度。
  3. 如果流量翻十倍,最先崩的是哪一环?(这个问题决定优化顺序)

动手:可观察结果

产出 判断标准
一张架构图 每个中间件旁边标注一句话:它解决了什么痛点;标不出就该删
一次依赖故障演练 手动停掉 Redis(或 MQ),看主链路是降级还是整体 500
一份组件取舍表 至少三条"我没采用 XX,因为……"的记录

完成标志:你能对着图说出每个组件被移除后的后果,而不是只有"大家都用"。

故障注入

注入方式 观察什么 期望的正确行为
注入一个慢依赖(接口 sleep 3s 才返回) 并发下线程池/连接池是否被打满 主链路有超时保护,快速失败而非排队等死
让 MQ broker 不可用 下单主流程是否被阻塞 投递失败降级为本地记录 + 重试,主流程仍可完成
关闭只读实例 查询请求是否全部打到主库 主库压力上升但接口不报错,或有熔断降级
移除本地缓存只留 Redis P99 延迟变化 上升幅度决定是否值得保留本地缓存层

自测题

  1. "RabbitMQ 保可靠、Kafka 保吞吐"这句话,各自的机制原因是什么(至少各说一条)?
  2. 什么情况下引入消息队列是负收益?
  3. 你的系统里,哪些调用必须有超时、哪些可以无限等待?划分依据是什么?
  4. 如果流量涨十倍,你会按什么顺序做容量准备?为什么是这个顺序?
  5. 举一个你见过的"为了用微服务而用微服务"的后果。

进入 keel 阅读