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 保吞吐。很多团队用错的原因,是把"日志管道"和"业务消息"当成了一件事。
三、四条贯穿全局的原则
- 分层解耦:接口层不做业务规则,服务层不直接碰 HTTP 细节,数据访问层收口 SQL。好处是任何一层可以单独替换和测试。
- 异步优先:主链路只保留必须同步完成的部分(写库、鉴权、返回关键结果),其余一律投递消息后返回。
- 缓存分级:本地缓存(进程内,纳秒级,无网络开销)→ Redis(跨实例共享)→ DB。级别越高越贵,能不上就不上。
- 故障隔离:任何外部依赖都可能失败,所有调用点都要有超时、重试上限和降级路径。
四、最小可用架构
不必一上来堆全套。一个能承载千级 QPS 的最小组合是:
客户端 → Nginx → uvicorn 多 worker(FastAPI)
├─ Redis(缓存 + 分布式锁 + 流)
├─ 主库(强一致写)
├─ 只读实例(查询与报表)
└─ MQ(非核心链路异步化)
└─ 独立 Worker 进程消费 MQ(失败进死信)
关键点是worker 与应用进程分开:它的崩溃不该影响在线请求,它的积压也不该拖垮接口延迟。
五、应该问自己的三个问题
- 我系统里最慢的那个外部调用在哪里?如果它挂了,主链路还能返回吗?
- 我引入的每个中间件,分别替代了我自己写的哪段(容易出错的)代码?如果答不上来,它就是多余的复杂度。
- 如果流量翻十倍,最先崩的是哪一环?(这个问题决定优化顺序)
动手:可观察结果
| 产出 | 判断标准 |
|---|---|
| 一张架构图 | 每个中间件旁边标注一句话:它解决了什么痛点;标不出就该删 |
| 一次依赖故障演练 | 手动停掉 Redis(或 MQ),看主链路是降级还是整体 500 |
| 一份组件取舍表 | 至少三条"我没采用 XX,因为……"的记录 |
完成标志:你能对着图说出每个组件被移除后的后果,而不是只有"大家都用"。
故障注入
| 注入方式 | 观察什么 | 期望的正确行为 |
|---|---|---|
| 注入一个慢依赖(接口 sleep 3s 才返回) | 并发下线程池/连接池是否被打满 | 主链路有超时保护,快速失败而非排队等死 |
| 让 MQ broker 不可用 | 下单主流程是否被阻塞 | 投递失败降级为本地记录 + 重试,主流程仍可完成 |
| 关闭只读实例 | 查询请求是否全部打到主库 | 主库压力上升但接口不报错,或有熔断降级 |
| 移除本地缓存只留 Redis | P99 延迟变化 | 上升幅度决定是否值得保留本地缓存层 |
自测题
- "RabbitMQ 保可靠、Kafka 保吞吐"这句话,各自的机制原因是什么(至少各说一条)?
- 什么情况下引入消息队列是负收益?
- 你的系统里,哪些调用必须有超时、哪些可以无限等待?划分依据是什么?
- 如果流量涨十倍,你会按什么顺序做容量准备?为什么是这个顺序?
- 举一个你见过的"为了用微服务而用微服务"的后果。