批量聚合策略:固定批次大小、动态批次调整、超时触发机制
批量聚合,说白了就是「攒一批再处理」。但怎么攒、攒多少、等多久,这三个问题直接决定了你的吞吐量天花板。我见过不少团队,上来就设个 1000 的批次大小,结果延迟飙到用户骂娘;也有人设 10ms 超时,结果批次永远凑不满,吞吐量还不如单条处理。
这一章,咱们就把三种主流策略掰开揉碎讲清楚。每种策略都有它的适用场景,也有它的坑。我会结合自己踩过的坑,帮你找到最适合你的那套方案。
固定批次大小:最简单,也最容易翻车
固定批次大小的逻辑很直白:攒够 N 条数据,一次性提交。比如 Kafka 的 batch.size,或者数据库的批量 insert 每次 500 条。
核心参数:批次大小(batch size)
适用场景:数据到达速率稳定、对延迟不敏感的系统
我最早做日志采集系统时,就用的固定 1000 条一批。白天流量大,效果不错。到了凌晨,流量骤降,1000 条要攒 30 秒才能凑满。结果日志延迟从秒级变成了分钟级,运维同学半夜打电话把我叫醒……
所以固定批次大小的致命问题在于:低流量时延迟不可控。你想想看,如果业务高峰期和低谷期的流量差 100 倍,固定批次大小要么高峰期来不及攒,要么低谷期等死人。
// 固定批次大小的伪代码示例
public class FixedBatchProcessor {
private final int batchSize = 500;
private final List<Record> buffer = new ArrayList<>();
public void add(Record record) {
buffer.add(record);
if (buffer.size() >= batchSize) {
flush(); // 攒够了,提交
}
}
private void flush() {
// 批量写入数据库或发送到下游
database.batchInsert(buffer);
buffer.clear();
}
}
这个代码看起来没问题,对吧?但实际生产环境里,你还要考虑并发安全、内存溢出、失败重试等问题。我后面会讲到这些坑。
动态批次调整:让系统自己「看情况办」
动态批次调整,就是让系统根据当前流量自动调整批次大小。流量大时批次大,流量小时批次小。这样既能保证吞吐量,又能控制延迟。
我个人习惯用滑动窗口统计来做动态调整。统计过去 1 秒内的数据到达速率,然后根据目标延迟反推批次大小。
| 流量状态 | 到达速率 | 推荐批次大小 | 预期延迟 |
|---|---|---|---|
| 高峰 | 10000 条/秒 | 1000 | 100ms |
| 正常 | 1000 条/秒 | 100 | 100ms |
| 低谷 | 100 条/秒 | 10 | 100ms |
你看,目标延迟控制在 100ms 左右,批次大小随流量自动变化。这样不管白天黑夜,延迟都稳定。
// 动态批次调整的核心逻辑
public class AdaptiveBatchProcessor {
private final SlidingWindowRateCounter rateCounter;
private final long targetLatencyMs = 100;
public int calculateBatchSize() {
double arrivalRate = rateCounter.getRate(); // 每秒到达条数
// 目标延迟内能处理多少条?
int batchSize = (int) (arrivalRate * targetLatencyMs / 1000);
// 设置上下限,防止极端情况
return Math.max(10, Math.min(2000, batchSize));
}
}
经验之谈:动态调整一定要加上下限。我曾经没设下限,结果流量为 0 时批次大小算出来是 0,导致数据永远不提交……那叫一个惨。
超时触发机制:给批次一个「最后期限」
超时触发,就是给每个批次设一个最大等待时间。不管攒没攒够,时间到了就提交。这是解决「低流量时延迟过高」的最直接手段。
常见的超时策略有两种:
- 固定超时:比如每 100ms 检查一次,到点就提交。简单粗暴,但高峰期可能频繁提交小批次,降低吞吐量。
- 自适应超时:根据当前批次大小动态调整超时时间。批次越大,超时越长;批次越小,超时越短。
我做过一个支付对账系统,要求延迟不超过 500ms。用了固定超时 200ms,配合动态批次大小。效果不错,但有个坑:超时和批次大小是联动的,不能独立调优。
// 带超时机制的批量处理器
public class TimeoutBatchProcessor {
private final int maxBatchSize = 500;
private final long timeoutMs = 200;
private final List<Record> buffer = new ArrayList<>();
private long batchStartTime;
public void add(Record record) {
if (buffer.isEmpty()) {
batchStartTime = System.currentTimeMillis();
}
buffer.add(record);
// 两个条件满足其一就提交
if (buffer.size() >= maxBatchSize ||
System.currentTimeMillis() - batchStartTime >= timeoutMs) {
flush();
}
}
}
注意:超时机制要小心「惊群效应」。如果所有批次同时超时,下游系统可能瞬间被打满。建议给超时时间加一点随机抖动(jitter),比如 200ms ± 20ms。
三种策略的对比与选择
这三种策略不是互斥的,实际生产中往往是组合使用。我画了一张图,帮你理清它们的关系:
从图上你能看到,三种策略是层层递进的关系。我建议的优先级是:固定批次 > 超时 > 动态调整。先保证吞吐量,再控制延迟,最后用动态调整来优化整体表现。
组合策略实战:一个完整的例子
下面是一个我在电商订单处理系统中用过的组合策略。它同时用了固定批次、动态调整和超时三种机制。
public class CombinedBatchStrategy {
// 配置参数
private final int minBatchSize = 10;
private final int maxBatchSize = 2000;
private final long baseTimeoutMs = 100;
private final long maxTimeoutMs = 500;
// 运行时状态
private final RateTracker rateTracker = new RateTracker();
private final List<Order> buffer = new ArrayList<>();
private long batchStartTime;
public void addOrder(Order order) {
if (buffer.isEmpty()) {
batchStartTime = System.currentTimeMillis();
}
buffer.add(order);
// 1. 动态计算当前最优批次大小
int optimalBatchSize = calculateOptimalBatchSize();
// 2. 动态计算超时时间(批次越小,超时越短)
long dynamicTimeout = calculateDynamicTimeout(optimalBatchSize);
// 3. 检查是否满足提交条件
if (buffer.size() >= optimalBatchSize ||
System.currentTimeMillis() - batchStartTime >= dynamicTimeout) {
flush();
}
}
private int calculateOptimalBatchSize() {
double rate = rateTracker.getArrivalRate();
// 目标延迟 200ms
int size = (int) (rate * 0.2);
return Math.max(minBatchSize, Math.min(maxBatchSize, size));
}
private long calculateDynamicTimeout(int batchSize) {
// 批次越小,超时越短,避免低流量时延迟过高
double ratio = (double) batchSize / maxBatchSize;
return (long) (baseTimeoutMs + (maxTimeoutMs - baseTimeoutMs) * ratio);
}
}
关键点:动态超时的计算公式里,我用了线性映射。批次大小从 10 到 2000,超时时间从 100ms 到 500ms。这样小批次不会等太久,大批次也能给足时间攒数据。
避坑指南
这些坑我都是真金白银换来的教训,你一定要注意:
- 批次大小不是越大越好。我曾经把批次设到 5000,结果一次 OOM 把整个服务搞挂了。批次大小要考虑内存和下游处理能力。
- 超时时间不要设太短。设 10ms 超时,基本等于没批处理。建议至少 50ms 起步。
- 动态调整要平滑。不要每次到达都重新计算,建议每秒或每 10 个批次调整一次。否则系统会「抖动」。
- 别忘了监控。我习惯把批次大小、超时次数、平均延迟都打到监控系统里。调优全靠数据说话。
我曾经踩过的坑:有一次动态批次调整的算法写错了,导致高峰期批次大小反而变小。结果吞吐量直接腰斩,数据库连接池被打满。后来我加了「批次大小变化率限制」,每次调整不超过 20%。
批量聚合策略,说白了就是在吞吐量和延迟之间找平衡。固定批次保吞吐,超时机制控延迟,动态调整做优化。三者配合好了,你的系统就能像老司机一样,不管路况怎么变,都能稳稳地跑在目标车速上。