KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
08 · 各量级的方案选型:什么量级上什么,什么量级别瞎上 — keel 龙骨
这一章回答:知道自己在第几档之后,具体该上什么方案——以及更重要的,哪些方案在这个量级属于"瞎上"。
这一章回答:知道自己在第几档之后,具体该上什么方案——以及更重要的,哪些方案在这个量级属于"瞎上"。
上一章给了量级的心智模型和换算方法。这一章把它变成决策。前六章讲的每一个机制(异步、限流、熔断、分布式锁、消息队列、最终一致)都是好东西,但它们各自是为某一档的某个痛点设计的;把它们用在没遇到那个痛点的量级上,付出的是复杂度,换回的是零收益甚至负收益。
一、问题现场:一场"每句话都对"的架构评审
- 角色:你在一个内部系统的架构评审会上,系统是给公司内 3000 人用的工单 + 查询平台。
- 输入:有同学提议一整套升级——引入 Kafka 做异步化、按领域拆成 5 个微服务、Redis 换 Cluster、订单表先做分库分表。
- 系统状态:当前峰值 QPS 约 600,P99 800ms,所有接口都是同步阻塞写法,数据库连接池 20,几个关键查询没走索引。
- 失败现象:升级做了三个月。上线后 P99 从 800ms 变成 1.1s(更慢了),运维从"一个人能管"变成"需要专人值班",而那几个慢查询还在。
先别往下读:这四项技术每一个都"很专业",为什么组合起来是负收益?
因为这套系统是千级 QPS、痛点在同步阻塞和慢 SQL——而 Kafka 解决的是百万级吞吐与回放、微服务解决的是团队协作与独立发布、Redis Cluster 解决的是单实例容量与高可用、分库分表解决的是单库写入与数据量上限。这四件事没有一件是它的痛点。 复杂度提前支付了,收益没有发生,还新增了网络跳数与运维面——所以 P99 反而变差。
选型的第一性问题不是"这个技术好不好",而是"我的痛点是不是它解决的那一个"。
二、第一性原则:先定位瓶颈,再选方案
选型的顺序必须是单向的,不能跳步:
① 算在途并发(07 章的 L = λ × W)
↓
② 定档:百 / 千 / 万 / 十万
↓
③ 找这一档的主要瓶颈(是池?是长任务?是连接?是单机上限?)
↓
④ 选针对该瓶颈的方案
↓
⑤ 压测验证:盯在途并发、队列长度、P99
最常见的错误是从 ④ 直接开始——听说某个方案"更高级",或者"大厂都在用",就跳过 ①②③。判断一个提议是否靠谱,只问一句:它替代了我自己哪段容易出错的代码、解决了我哪个实测到的瓶颈?答不上来,它就是多余的复杂度。
三、四档方案地图
下面每一档都按同一格式给:典型场景 → 瓶颈在哪 → 该上什么 → 不该瞎上什么 → 一个类比。
档位 1|千级以内(几百 ~ 2000 在途):普通短任务
- 典型场景:企业内部系统、普通 CRUD API、列表查询、轻量写操作。单次请求 10~300ms。
- 瓶颈在哪:同步阻塞点、worker/连接池上限、慢 SQL 与缺索引、没有超时。注意这一档的瓶颈几乎总在"某个具体的慢动作",而不是"整体架构"。
- 该上什么:
- 异步化(IO 密集场景)——但先确认阻塞点在哪(02 章);
- 连接池参数按实测校准,不是拍脑袋调大;
- 超时 + 重试上限 + 降级(03 章);
- 索引与慢查询治理(
database-tuning那门课); - 本地缓存/Redis 缓存热点读。
- 不该瞎上什么:Kafka(你要的不是百万吞吐)、微服务拆分("一处慢全站慢"要先修阻塞,不是拆服务)、Redis Cluster、分库分表。
- 类比:这条路只是高峰期堵车,你该做的是修红绿灯和疏堵点(超时、索引、池参数),不是去修一条八车道高速。
档位 2|千级 ~ 万级,含长任务(AI 服务典型)
- 典型场景:Agent 对话、批量 embedding、文档解析、报表导出、视频/图片处理。单次 1~30s,且往往占着内存/显存/连接。
- 瓶颈在哪:长任务占满并发位导致排队爆炸(07 章第六节实验);任务状态无处安放;失败后无法恢复。
- 该上什么:
- 把长任务移出在线请求:接口只"接单 + 返回任务 ID",实际执行投递到队列(05 章);
- 独立 worker 消费,按容量消费而不是按到达率硬扛;
- 削峰 + 准入限流(03 章):到不了就快速失败,不要让队列无限增长;
- 任务状态持久化 + 幂等 + 重试上限 + 死信;
- 超时与取消(用户关页面要能真的停)。
- 不该瞎上什么:
- 为了"提高吞吐"去加大连接池——长任务的问题是占着不放,加连接只是让它占更多;
- 为了让长任务变快而猛加 web 实例——占座问题没解决,只是把座位数从 200 抬到 400;
- 用"分布式锁"去保护本可以单实例串行化的任务。
- 类比:火锅店翻台慢,正确做法是把"等位的客人"请到等候区(队列)并控制发号速度(限流),而不是疯狂加服务员。
这一档最需要的一个判断:长任务还分两种,处理方式完全不同。
- IO 型长任务(等模型返回、等第三方 API):协程 + 队列就够了,一个进程能挂成千上万个在等的任务。
- CPU 型长任务(本地推理、向量化、压缩、大文件解析):协程救不了你。看实验 4——8 个 400ms 的 CPU 任务放进协程里跑,实测 3.20s(理想并行约 0.4s),因为它们完全串行;换成进程池(8 workers)是 2.01s(这段还含进程启动开销,Linux 下用 fork 会比这个好看得多)。
结论:CPU 型长任务必须走进程池或独立服务,加协程数只会让事件循环被卡死。
档位 3|万级(2 千 ~ 2 万在途):多类任务混合
- 典型场景:同一个平台同时跑着短接口、长任务、定时任务、实时推送——AI 平台到这个规模基本都是这种形态。
- 瓶颈在哪:互相干扰。长任务把短接口的并发位吃光,一个批量导入能把所有在线接口拖到超时。
- 该上什么:
- 按任务类型隔离:独立队列 + 独立 worker 池 + 独立限流配额;
- 优先级队列(在线请求优先于批处理);
- 熔断与降级,防止一类任务拖垮全局;
- 背压:队列水位到阈值就拒绝或降速。
- 不该瞎上什么:
- 所有任务共用一个池(必然互相饿死,见下面实验);
- 用一套限流阈值管所有类型(长任务和短任务的合理阈值差好几个数量级);
- 每来一类新任务就新建一个服务(先问能不能只是"一个新队列 + 新 worker 池")。
- 类比:医院分诊——急诊、门诊、体检不能排同一个队,否则一个体检团建就能把急诊堵死。
实验 5 就是为这一档做的:短任务(10 个/秒,每个 50ms)稳定运行,t=2s 时涌入 30 个长任务(每个 2s)。对比"共用一个池"与"长/短分开":
长任务涌入前 长任务涌入后
共享池 (W=8) P50 70.8ms P50 4027.8ms
P99 78.4ms P99 5996.0ms
隔离池 (长W=6/短W=2) P50 70.8ms P50 70.3ms
P99 78.5ms P99 78.7ms
请务必看清这个对比的前提:两种方案的总容量都是 8(共享 8,隔离是 6+2)。没有多加任何一个并发位,只是把它们分开,短任务的 P99 就从 5996ms 变成 78.7ms——差了 76 倍。
这就是"队头阻塞"(head-of-line blocking):在共享池里,一个 2s 的长任务会把一个 50ms 的短任务堵在后面;而你没有任何理由让它们排同一条队。这一档的选型核心不是"再加多少容量",而是切分。
档位 4|十万级(> 10 万):先分清是哪一种十万
这是最容易误判的一档,因为"十万"有两种完全不同的成因:
| 成因 | 典型场景 | 真正的瓶颈 | 该走的路 |
|---|---|---|---|
| 极高 QPS 的短请求 | 大促、抢票、热点信息流 | 单机吞吐上限、热点数据、DB 写 | 无状态水平扩展 + LB;分片/分区;多级缓存;全局限流与配额;热点 key 打散 |
| 海量长连接 | IM、推送、IoT、实时协作 | 连接维持(fd、内存、心跳),不是计算 | 连接网关独立部署;心跳与空闲连接治理;连接层与计算层分开扩容 |
- 该上什么:无状态化(这是水平扩展的前提)、负载均衡、分片/分区、多级缓存、全局限流、容量模型与可观测体系。
- 不该瞎上什么:
- 继续在单机上做优化——天花板已经在那,边际收益极低;
- 用分布式锁去解决热点写(正确做法是分片、队列串行化、或合并写);
- 在这个量级还依赖跨服务强一致事务(走最终一致 + 幂等 + 对账,见 06 章)。
- 诚实边界:到这一档,单点技巧已经不够用了。它的本质是容量工程 + 组织工程(容量模型、压测常态化、值班与演练),任何"换个框架就能解决"的说法都不可信。
四、"不该瞎上"清单(反模式)
这一节可以单独当评审 checklist 用。左列是常见的"看起来很专业"的提议,右列是它真正该出现的时机。
| 反模式 | 常见于 | 真实代价 | 什么时候才真的该上 |
|---|---|---|---|
| 千级 QPS 上 Kafka | "异步化"被当成年终 KPI | 运维成本、语义复杂度、调试成本 | 需要分区并行/回放/百万级吞吐时 |
| 为解决阻塞而上微服务 | 一处慢导致全站慢 | 网络跳数、分布式调试、发布复杂度 | 团队边界与独立发布成为主要矛盾时 |
| 长任务场景加大连接池 | "连接不够所以慢" | 占更多资源,DB 侧更差 | 扩容前先确认瓶颈真的是连接数 |
| 所有任务共用一个池 | 图省事 | 队头阻塞(实验 5:P99 差 76 倍) | 从不该——除非任务同质且都很短 |
| 能单实例解决的并发也上分布式锁 | "分布式"听起来更安全 | 锁服务成为新故障点、死锁难查 | 真的跨实例竞争同一份数据时(04 章) |
| 还不知道瓶颈就扩实例 | 压力一来先加机器 | 钱花了,排队依旧(07 章现场) | 已定位到容量不足且已削峰之后 |
| 万级之前就分库分表 | "提前规划" | 事务与查询复杂度爆炸 | 单库写/数据量真的到上限时 |
| 用"大家都这么用"代替痛点论证 | 架构评审 | 复杂度先付、收益后不到 | 从不——本课程的一贯纪律 |
一句话记住这张表:复杂度也是一种成本,而且它到期必付。
五、决策流程(一张图)
flowchart TD
A["① 先算在途并发 L = λ × W"] --> B{"② L 在哪一档?"}
B -->|"百级"| C["先查慢 SQL 与缺失超时<br/>不要动架构"]
B -->|"千级"| D{"是长任务吗?"}
B -->|"万级"| E{"多类任务混跑?"}
B -->|"十万级"| F{"成因是哪一种?"}
D -->|"否:普通短任务"| D1["异步化 + 连接池校准<br/>+ 超时/索引/缓存"]
D -->|"是:AI / 导出类"| D2["长任务入队 + 独立 worker<br/>+ 削峰 + 准入限流<br/>CPU 型另走进程池"]
E -->|"否"| E1["按长任务方案处理"]
E -->|"是"| E2["按类型隔离池/队列<br/>+ 优先级 + 分别限流"]
F -->|"极高 QPS 短请求"| F1["无状态水平扩展 + LB<br/>+ 分片 + 多级缓存 + 全局限流"]
F -->|"海量长连接"| F2["独立连接网关 + 心跳治理<br/>+ 连接层与计算层分开扩容"]
C --> G["⑤ 压测验证:盯在途并发、队列长度、P99"]
D1 --> G
D2 --> G
E1 --> G
E2 --> G
F1 --> G
F2 --> G
G --> H{"瓶颈是否转移?"}
H -->|"是"| B
H -->|"否"| I["收工:写下这轮的瓶颈与方案依据"]
注意最后那个回环:解决了一个瓶颈,压力会转移到下一个组件,所以要回到 ② 重新定档。容量工作不是一次性项目,是一个循环——这也是为什么"盯指标"比"选方案"更重要。
六、动手:可观察结果
| 产出 | 判断标准 |
|---|---|
| 一份选型论证表 | 对你系统里每一个中间件写两行:它解决的痛点是什么、拿掉它会怎样。写不出前者的,列入待删清单 |
| 一次隔离前后对比 | 复现实验 5(共享池 vs 隔离池),给出你自己系统的 P99 对比数字 |
| 一份反模式自查 | 用第四节的清单逐条过一遍,至少找出一条"我们现在就有、但其实不该上"的项 |
| 一次定档复算 | 假设流量翻 10 倍,重算在途并发与定档,写下"下一个瓶颈会转移到哪" |
完成标志:你能对任何一个新技术提议说出"它解决的是第几档的什么痛点,我们现在不在这个档",而不是"这个我没用过/这个很火"。
七、故障注入
| 注入方式 | 观察什么 | 期望的正确行为 |
|---|---|---|
| 在共享池里投一批长任务(复现实验 5) | 短任务 P99 是否被拖到秒级 | 若已隔离:短任务 P99 基本不变;未隔离:应复现数十倍劣化 |
| 把队列的消费者全部停掉,只留生产 | 队列长度与接口延迟 | 有水位上限与准入限流:到顶后拒绝新任务;无保护:队列无限增长、内存告警 |
| 给 CPU 型长任务只加协程数不加进程 | 事件循环延迟、其它接口 P99 | 应该没有改善(实验 4:CPU 任务在协程里是串行的) |
| 把限流阈值设成"所有类型共用" | 长任务与短任务的通过率 | 短任务被长任务挤掉,或长任务被短任务饿死——说明阈值必须分类 |
八、生产边界(替身 → 替换点)
| 教学替身 | 真实替换点 | 注意 |
|---|---|---|
asyncio.sleep 模拟长任务 |
真实模型调用 / 真实批处理 | 真实任务还有额度限制、失败重试、部分成功语义 |
Semaphore 模拟并发池 |
worker 并发度 / 队列消费者数 | 真实环境是"实例数 × 每实例并发度",还要算上 LB 与超时 |
| 单机进程池 | 独立计算服务 / 专用推理服务 | 跨实例后需要分布式任务状态与全局配额 |
| 本机实测数字 | 你的目标环境实测 | 本机是 Windows 开发机;Linux 的 fork、线程栈、调度都不同 |
九、自测题
- 一个千级 QPS、痛点在同步阻塞和慢 SQL 的系统,上 Kafka 为什么是负收益?正确的第一步是什么?
- 长任务和普通任务在"该上什么"上最大的分歧点是什么?为什么加大连接池对长任务无效?
- 实验 5 里两种方案的总容量都是 8,为什么短任务 P99 差了 76 倍?这个结论在你的系统里对应哪个改动?
- CPU 型长任务为什么加协程数没用?你该换成什么?
- "十万并发"有两种成因,分别是什么?它们各自的第一瓶颈有什么不同?
- 你的系统里有没有某个中间件,说不出"它解决了哪个实测到的瓶颈"?如果有,它为什么还在?
现在能解释什么
读完这一章,你应该能:
- 拿到一个系统的 QPS 与延迟,先定档,再说出这一档的主要瓶颈,然后才谈方案;
- 对任何一个技术提议,用一句话判断它是不是这个量级该上的——"它解决的是第几档的什么痛点";
- 识别并反驳最常见的几类过度设计(Kafka、微服务、分库分表、分布式锁、盲目扩实例);
- 解释为什么"切分"比"扩容"更值钱(实验 5:同样的 8 个并发位,分开用 P99 差 76 倍);
- 区分 IO 型与 CPU 型长任务,知道前者靠协程与队列、后者必须进程池或独立服务;
- 最重要的一条:复杂度是成本,到期必付。选型不是选"更高级",是选"正好解决我现在这个瓶颈的那一档"。