晚高峰接口一来就抖,CPU 没满、数据库也没打满,但请求还是超时。很多时候不是“算力不够”,而是“每个请求都单独走完整流程”,固定开销被重复支付了太多次。
这时有个很实用的优化思想:用可控的延迟,换更高的吞吐。
先把结论放桌上:
- 适用场景:吞吐优先、可容忍一定排队。
- 典型手段:批处理、微批、队列缓冲、异步流水线、写合并。
- 核心代价:单请求等待时间上升(尤其是低峰期和尾延迟)。
1) 这套思想到底在优化什么
吞吐(Throughput)是“单位时间处理多少请求”;延迟(Latency)是“单个请求从进到出要多久”。
当系统瓶颈包含明显固定成本(网络往返、锁竞争、事务提交、刷盘)时,让请求短暂排队后一起处理,常能把固定成本摊薄。
可以把它理解成高速公路收费站:
如果每辆车都人工慢慢刷卡,队伍会很长;换成 ETC 车道并行批量通过,单车可能多等几秒排队,但每分钟过车数会上去很多。
请求到达
|
v
[队列缓冲] --> (达到批大小 B 或等待到 T 毫秒)
| |
| 否 | 是
| v
+--------------------> [组批处理] --> [写入/计算/下游调用] --> [返回结果]
现在就做一件事:把你的请求链路按这张图画出来,先定位“最贵的固定成本”在哪一段。
2) 五种手段,分别解决什么问题
批处理(Batch)
- 通俗解释:攒够一批再处理,一次做完很多条。
- 生活类比:食堂不是给每个人单独开锅,而是一锅炒 30 份。
- 迷你案例:对账任务把 10000 条流水按 500 条一批入库,事务提交次数从 10000 次降到 20 次,数据库压力明显下降。
微批(Micro-batch)
- 通俗解释:批处理的“小步快跑版”,每 5~20ms 或凑够 N 条就立刻发车。
- 生活类比:电梯不会等到满 20 人才走,通常“够人或等几秒”就开门。
- 迷你案例:日志服务每 10ms 刷一次或满 64 条即刷,吞吐提升明显,同时把等待控制在毫秒级。
队列缓冲(Queue Buffering)
- 通俗解释:生产者和消费者解耦,用队列吸收突发流量。
- 生活类比:医院叫号系统,先登记排队,医生按节奏处理。
- 迷你案例:秒杀下单先入队,库存与订单服务按稳定速率消费,峰值时不再“瞬间打爆”数据库。
异步流水线(Async Pipeline)
- 通俗解释:把一个大步骤拆成多个阶段并行推进,不要求一个请求从头到尾独占资源。
- 生活类比:奶茶店把点单、配料、封口分工,不是一个人全包。
- 迷你案例:图片处理拆为“解码 -> 缩放 -> 水印 -> 上传”,各阶段并行消费,整体处理量提升。
写合并(Write Coalescing)
- 通俗解释:短时间内对同一对象的多次写,只保留必要写入(常见是“保留最后一次”)。
- 生活类比:同一地址的快递集中打包一次送,不用一小时跑五趟。
- 迷你案例:用户资料页在 1 秒内连续改昵称、头像、签名,系统合并成一次 upsert,减少锁与 I/O。
3) 一张表看懂:怎么选手段
| 手段 | 最适合的场景 | 吞吐收益 | 延迟代价 | 常见坑 |
|---|---|---|---|---|
| 批处理 | 离线任务、可延后写入 | 高 | 中到高 | 批太大导致超时 |
| 微批 | 在线高并发写入 | 中到高 | 低到中 | 定时与批大小参数难调 |
| 队列缓冲 | 峰谷明显、突发流量 | 中 | 中 | 无上限队列引发长尾延迟 |
| 异步流水线 | 多阶段处理、可拆步骤 | 中到高 | 中 | 阶段背压处理不好 |
| 写合并 | 高频重复写同一 key | 中到高 | 低到中 | 合并策略错误导致数据丢语义 |
如果你只能先做一件事,优先上“队列缓冲 + 微批”这组组合,通常改造成本和收益比最好。
4) 可复现落地:队列 + 微批 + 写合并 三件套
假设你有一个“用户行为写库”接口,当前是同步单条写入:
- 现状:每次写库约 2ms(网络、事务、刷盘综合),单 worker 理论上约 500 次/秒。
- 高峰:突发到 3000 次/秒,直接排队超时。
改造步骤:
- 请求到达先入内存队列(或消息队列),立即返回“已接收”或短等待结果。
- Flusher 每 10ms 触发一次,或队列满 64 条立即触发。
- 对批内相同
user_id的更新做写合并(保留最后一条)。 - 批量 upsert 到数据库。
- 队列长度超过阈值时触发背压:限流、降级、快速失败三选一。
伪代码示意:
onRequest(event):
queue.push(event)
every 10ms:
batch = queue.popUpTo(64)
merged = mergeByKey(batch, key=user_id, keep="last")
db.bulkUpsert(merged)
一个常见结果(示例值):
- 每批 64 条耗时约 6ms,则单条摊销到约 0.09ms 级别;
- 吞吐从“单条同步写”显著提升;
- 单请求新增等待通常在 5~20ms 区间(取决于流量和参数)。
5) 延迟到底涨多少:用这个公式先估算
单请求总延迟 ≈ 入队等待 + 组批等待 + 执行时间 + 返回时间
经验上:
- 低流量时,组批主要受定时器
T影响,平均等待接近T/2。 - 高流量时,更快凑满批次,等待常小于
T/2,但队列拥堵会拉高尾延迟。
再看同步串行和异步流水线的直观差异:
同步串行
Req1: [验签][查重][写库][发消息]
Req2: [验签][查重][写库][发消息]
异步流水线
S1验签: Req1 Req2 Req3 Req4
S2查重: Req1 Req2 Req3 Req4
S3写库: Req1 Req2 Req3 Req4
S4消息: Req1 Req2 Req3 Req4
把你的处理链路按阶段拆开后,先测每一段利用率,再决定要不要继续加并行度。
6) 参数怎么起步,避免“一上来就翻车”
给一套可直接抄的起步值:
- 微批等待时间
T:先用 10ms(常见范围 5~20ms)。 - 批大小
B:先用 32 或 64,再按数据库最佳批量点微调。 - 队列上限
Q:必须有上限,超过就背压,不要无限排队。 - 并发 worker
C:从 CPU 核数和数据库连接上限倒推,不盲目拉满。
监控最少盯这四个指标:
- 队列长度(queue depth)
- 组批等待时间(batch wait)
- 吞吐(requests/s 或 writes/s)
- 尾延迟(P95/P99)
7) 什么时候不要用“延迟换吞吐”
以下场景要谨慎甚至避免:
- 强实时交互(如极低延迟交易、实时控制指令)
- 用户对单次响应极其敏感(例如首屏关键路径)
- 业务语义不允许重排、合并或异步确认
一句话收尾:
“延迟换吞吐”不是免费午餐,但它常常是高并发系统最便宜的第一步。
先接受一点点排队,再换来稳定的大流量处理能力,系统就不容易在高峰期“原地趴窝”。