高并发请求折叠:从 Hystrix 到埋点聚批

导读 :高并发里,瓶颈常常是「调用太碎」------十个线程各打一条 SQL,连接、上下文切换、下游 QPS 一起被放大。请求折叠(Request Collapsing) 的做法是:在极短窗口内把同类请求攒成一批,用一次下游调用完成,再按原顺序把结果拆回去。Hystrix 拿它合并并发读,Kafka 拿它攒发送批次,HTTP 网关拿它合并同 URL 请求;APP 埋点上报 则在写路径上用「时间 + 数量」双阈值做聚批落库。下文按读路径、写路径各举一种典型实现,并落到一次真实接口设计。


阅读导航

  • 什么是请求折叠 --- 核心思路、收益与代价
  • [Hystrix 请求折叠](#Hystrix 请求折叠) --- 读路径:并发合并 + 定时触发
  • [Kafka 与同类实现](#Kafka 与同类实现) --- 写路径:缓冲 + 双阈值刷出
  • [HTTP 折叠框架要点](#HTTP 折叠框架要点) --- collapse-executor 抽象
  • 实战埋点上报聚批 --- 有界队列 + 批量落库
  • 选型对比 --- 读折叠 vs 写聚批
  • 结语 --- 落地检查清单

什么是请求折叠

一句话:短时间内的多个请求,合并成一次下游调用,再把结果按原顺序还回去。

不折叠 vs 折叠,差别在下游眼里长什么样:

text 复制代码
  【不折叠】

  线程1 ──▶ SELECT id=1 ──▶ DB
  线程2 ──▶ SELECT id=2 ──▶ DB
  线程3 ──▶ SELECT id=3 ──▶ DB
         ... 共 N 次往返

  【折叠后】

  线程1 ──┐
  线程2 ──┼──▶ Collapser 缓冲 ──▶ SELECT id IN (...) ──▶ DB
  线程3 ──┘         │
                    │ timer 到期 / 凑满 N 条
                    ▼
              拆回各线程结果

能换来什么

说明
连接与线程 10 次 HTTP/DB 往返 → 1 次
下游吞吐 批量 SQL、批量 RPC 通常比循环单条更省
热点保护 ID 生成、库存扣减等可「预取一批,内存里发」

要付什么

说明
延迟 得等凑批窗口或凑满条数,P99 会上浮
复杂度 分组键、回填顺序、超时、拒绝策略都要想清楚
前提 下游得支持批量,或业务接受异步 / 最终一致

打散 是反方向:分库分表、LongAdder 多 cell 是把热点拆开;折叠是把并发 收拢 再处理。Leaf 号段模式则是折叠 + 预取------DB 一次取 1000 个号,内存里逐个发。


Hystrix 请求折叠

Hystrix Request Collapsing 面向 读路径:大量并发、参数可合并的查询,例如「根据 userId 查用户信息」。

整体链路

text 复制代码
           +----------------------+
           |  RequestCollapser    |
           +----------------------+
                      |
                      v
           +----------------------+
           |   RequestBatch.run   |
           +----------------------+
                      |
                      v
           +----------------------+
           | mapResponseToRequests|
           +----------------------+

触发条件(满足任一):timer 到期(如 10ms),或批次条数达上限(如 50)。

关键组件

组件 干什么
RequestCollapser 接收单次 submit,写入当前批次
RequestBatch 本批所有请求的容器
Timer 第一个请求进来时注册,到期触发执行
你的 BatchCommand 一次 RPC/SQL 查回多条,再 map 回去

配置与实现示意:

java 复制代码
public class UserBatchCommand extends CollapserCommand<List<User>> {
    private final List<Long> userIds;

    UserBatchCommand(List<Long> userIds) {
        super(Setter.withCollapserKey(CollapserKey.Factory.asKey("UserBatch"))
            .andCollapserPropertiesDefaults(
                HystrixCollapserProperties.Setter()
                    .withTimerDelayInMilliseconds(10)   // 合并窗口
                    .withMaxRequestsInBatch(50)));      // 单批上限
        this.userIds = userIds;
    }

    @Override
    protected List<User> run() {
        return userDao.batchSelectByIds(userIds);
    }

    @Override
    protected List<User> mapResponseToRequests(List<User> batchResponse) {
        return batchResponse;  // 与 submit 顺序一一对应
    }
}

值得记住的几点

优点 局限
折叠调度不单独占业务线程池 默认靠 CollapserKey 分组,细粒度分组要自己设计
调用方拿 Future/Observable 等结果 条数没凑满也会被 timer 拉走,窗口要调
和熔断、隔离在同一条治理链上 Hystrix 已停更,新项目借鉴思路即可

Kafka 与同类实现

写路径上更常见 先缓冲、再批量刷出 。Kafka Producer 的 RecordAccumulator 是工业界用得最多的样板。

双阈值怎么工作

text 复制代码
              Producer thread
                     |
                     | append(record)
                     v
           +----------------------+
           |  RecordAccumulator   |
           +----------------------+
                     |
           +---------+---------+
           v                   v
      batch full           linger.ms
           +---------+---------+
                     v
           +----------------------+
           |    Sender thread     |
           +----------------------+
                     v
                  Broker
参数 含义
batch.size 单分区批次字节上限(常见 16KB)
linger.ms 没攒满也最多等多久(常见 10ms)

Topic-Partition 分组,每条 record 进对应 batch;满了或时间到了,Sender 线程统一发出去,Producer 侧可用 Future 感知成败。整体偏吞吐,不追求单条最低延迟。

同类思路还有:TCP Nagle (小包合并)、快手 BufferTrigger (时间 + 数量,偏 fire-and-forget)、Leaf 号段(DB 取一段号,内存逐个发)。


HTTP 折叠框架要点

如果要在 Web 入口 合并「同一 URL、参数可合并」的并发 HTTP 请求,可以参考开源 collapse-executor。处理链路如下(每步含义见下表):

text 复制代码
                 HTTP Filter
                      |
                      v
           +----------------------+
           |    InputGrouper      |
           +----------------------+
                      |
                      v
           +----------------------+
           |       Bundle         |
           +----------------------+
                      |
                      v
           +----------------------+
           |   BatchCollector     |
           +----------------------+
                      |
                      v
           +----------------------+
           |     doExecute()      |
           +----------------------+
                      |
                      v
           +----------------------+
           |     bindOutput()     |
           +----------------------+
概念 含义
Bundle 一次请求的参数 + 结果回填句柄
ThreadlessExecutor 不另建池,借用当前 Web 工作线程做折叠与唤醒
SingleThreadExecutor 单队列单线程,把多任务让步合并后交给一条请求线程执行
Filter 入口 在 Servlet 层拦截,决定是否参与折叠

适合:多个客户端同时打同一读接口,且后端有 batch API。不适合:写操作彼此独立、没法合并语义的场景。


实战埋点上报聚批

APP 埋点上报 走的是写路径聚批:调用方不等落库,服务端用有界队列攒批后批量 INSERT,触发模型与 Kafka 的 linger + batch.size 同类。

业务约束

约束 设计选择
峰值数百 QPS,单条 INSERT 顶不住 服务端必须聚批落库
埋点数据可丢 队列满则丢弃,绝不阻塞用户请求
接口要快 校验完入队即返回,落库异步进行

端到端数据流

text 复制代码
           +----------------------+
           |      APP Client      |
           +----------------------+
                      |
                      | POST
                      v
           +----------------------+
           |      Report API      |
           +----------------------+
                      |
                      v
           +----------------------+
           | ArrayBlockingQueue   |
           +----------------------+
                      |
                      v
           +----------------------+
           |    Consumer Loop     |
           +----------------------+
                      |
                      v
           +----------------------+
           |     Batch INSERT     |
           +----------------------+
                      |
                      v
           +----------------------+
           |       Database       |
           +----------------------+

Report API:校验、去重、offer 入队后立即返回。Consumer Loop:poll(1000ms) + drainTo(500)。队列满则丢弃,INSERT 失败整批丢弃。

与 Kafka Producer 的参数对照:

Kafka 埋点管道
linger.ms flushIntervalMs(默认 1000ms,没攒够也刷)
batch.size batchSize(默认 500 条)
RecordAccumulator ArrayBlockingQueue(默认 20000)
Producer 回调 无------埋点可丢,不回调客户端

消费循环(简化)

java 复制代码
List<EventRow> batch = new ArrayList<>();
while (!shuttingDown) {
    EventRow head = queue.poll(flushIntervalMs, MILLISECONDS);
    if (head != null) {
        batch.add(head);
        queue.drainTo(batch, batchSize - batch.size());
    }
    if (!batch.isEmpty()) {
        mapper.batchInsert(batch);
        batch.clear();
    }
}

poll 控制时间窗口,drainTo 控制单批条数;客户端先把多条 event 打进一个 HTTP 请求,服务端再把多批请求里的行事件二次合并成 500 行一次的 INSERT,下游 QPS 从行级降到批级。


选型对比

text 复制代码
           +----------------------+
           |   Request Collapsing |
           +----------------------+
                      |
          +-----------+-----------+
          v                         v
      read path                 write path

   Hystrix / HTTP              Kafka / queue
   Collapser  collapse         Producer  batch
方案 典型场景 触发 回填 分组
Hystrix Collapser 并发查用户/订单 时间 + 数量 Future CollapserKey
Kafka Producer 日志、消息 字节 + linger Future 分区
HTTP collapse-executor 网关合并读请求 时间 + 数量 CompletableFuture InputGrouper
内存队列聚批 高 QPS 写宽表、可丢 poll + batchSize 单管道
Leaf 号段 分布式 ID 号段耗尽 内存发号 业务 key

怎么选

  • 读、要结果、并发重复 → Hystrix 类折叠,或 HTTP collapse。
  • 写、可异步、要吞吐 → 队列 + 双阈值(Kafka / 埋点模式)。
  • 写、强一致 → 别硬折叠;用 MQ 削峰,或业务层提供真正的批量 API。

结语

请求折叠的本质是 用可控的等待换整体吞吐。读路径看 Hystrix / HTTP collapse,写路径看 Kafka / 内存队列聚批,埋点则是「客户端批 + 服务端再批」的叠加。落地前建议逐项过一遍:

# 问题
1 一个上游请求会放大成几次下游调用?
2 下游有没有真正的批量 API?(别用 for 循环假装 batch)
3 合并窗口抬高的 P99,业务能不能接受?
4 分组键会不会把不该合并的请求并在一起?
5 折叠失败时:阻塞、降级,还是丢弃?
6 队列有没有上限?(无界队列在尖刺流量下是内存炸弹)
7 调用方是「必须等结果」,还是「受理就行」?

参考资料

相关推荐
头茬韭菜2 天前
第 10 篇:「Fluss 实战案例集」—— 完整解决方案与最佳实践
c#·linq·fluss
liudashuang20174 天前
Kafka Producer 隐藏深坑:Sender 线程自阻塞(自死锁)导致 BufferExhaustedException 完整复盘
java·分布式·kafka·linq
御坂嘀喵 白日焰火12 天前
LINQ之路18:LINQ to XML之导航和查询
xml·solr·linq
残月心殇 请珍惜枸12 天前
LINQ之路17:LINQ to XML之X-DOM介绍
xml·solr·linq
Volunteer Technology14 天前
Kafka的消费全流程
分布式·kafka·linq
「KISSSHOT」17 天前
抽象与性能:从 LINQ 看现代 .NET 的优化之道
java·.net·linq
香菜TTT17 天前
Kafka_深度解析_从架构原理到生产实践
分布式·架构·kafka·linq
明如正午17 天前
【C#】LINQ_HashSet_BitField解析
c#·linq
暗暗别做白日梦1 个月前
Pulsar 消息同步机制
c#·linq