4、补数执行模式:串行补数、并行补数、增量补数、全量补数
补数任务怎么跑,其实是个挺讲究的事。我见过不少团队,上来就把所有补数任务扔到集群里,结果资源打满,线上任务全堵死了。说白了,补数执行模式选不对,轻则效率低,重则把整个调度系统搞崩。
这一章,咱们就把四种核心模式掰开揉碎讲清楚。我个人习惯把这四种模式分成两个维度来看:执行顺序和数据范围。串行和并行管的是“怎么跑”,增量和全量管的是“跑哪些”。
4.1 串行补数:稳,但慢
串行补数,就是按时间顺序一个一个跑。比如你要补过去7天的数据,系统会先跑第1天,跑完再跑第2天,以此类推。
什么时候用?
- 任务有强依赖:后一天的计算依赖前一天的结果。比如某些累计指标,第2天要读第1天的产出。
- 资源紧张:集群就那么点资源,并行跑会把线上任务挤死。我遇到过一个小公司,Hadoop集群总共就20个槽位,并行补数直接把ETL任务全堵了,老板半夜打电话骂人。
- 数据量极大:单天数据量上百GB甚至TB级,并行跑磁盘IO直接打满。
串行补数的核心特征
- 执行顺序:严格按时间递增/递减
- 资源消耗:低,始终只占用1个任务槽位
- 执行时间:Σ(单天执行时间),线性累加
- 失败影响:单天失败不影响其他天,但后续天会阻塞
代码实现上,串行补数通常用一个循环搞定:
// 伪代码:串行补数逻辑
for (date in dateRange) {
try {
runTask(date);
log.info("补数成功: {}", date);
} catch (Exception e) {
log.error("补数失败: {}", date, e);
// 可以选择继续或终止
break; // 终止后续补数
}
}
我的经验:串行补数虽然慢,但排查问题最方便。哪天失败了,日志里清清楚楚。我曾经用串行补数跑一个月的金融数据,第15天挂了,一看是上游接口超时,修完从第15天继续跑就行,前面的不用重来。
4.2 并行补数:快,但有代价
并行补数,就是多个时间点的任务同时跑。系统会一次性把N天的任务都提交到调度队列里,能跑多少跑多少。
什么时候用?
- 任务间无依赖:每天的数据是独立的,比如日志清洗、独立报表。
- 资源充足:集群有足够的计算和存储资源。
- 时间窗口紧:业务方要求半小时内补完一个月的数据。
注意!并行补数最大的坑是“资源雪崩”
我曾经见过一个案例:某团队并行补数100天的数据,每个任务都要加载1GB的维表到内存。结果100个任务同时启动,直接把NameNode搞OOM了,整个HDFS集群挂了2小时。所以并行补数一定要做并发度控制。
并发度控制怎么做?看这段代码:
// 伪代码:带并发控制的并行补数
int maxConcurrency = 10; // 最大并发数
Semaphore semaphore = new Semaphore(maxConcurrency);
for (date in dateRange) {
semaphore.acquire();
threadPool.submit(() -> {
try {
runTask(date);
} finally {
semaphore.release();
}
});
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
并行补数的执行时间理论上接近单天执行时间(如果并发度足够大),但实际上受限于资源瓶颈。你想想看,100个任务同时读写同一张表,数据库锁竞争就能让你怀疑人生。
| 对比维度 | 串行补数 | 并行补数 |
|---|---|---|
| 执行时间 | 线性累加 | 接近单天时间(受限于并发度) |
| 资源消耗 | 低,1个槽位 | 高,N个槽位 |
| 失败影响 | 后续任务阻塞 | 不影响其他并行任务 |
| 适用场景 | 强依赖、资源紧张 | 无依赖、资源充足、时间紧 |
| 实现复杂度 | 低 | 中(需并发控制) |
4.3 增量补数:只补缺失的
增量补数,说白了就是“缺多少补多少”。系统会检查目标分区或表里已经有哪些数据,只补那些缺失的时间段。
实现思路:
- 查询目标表的最大分区或最新数据时间戳
- 计算从该时间点到目标结束时间的差值
- 只补这个差值范围内的数据
// 伪代码:增量补数逻辑
Date maxPartition = queryMaxPartition("target_table");
Date targetEnd = parseDate("2024-12-31");
if (maxPartition == null) {
// 表是空的,全量补
fillRange(startDate, targetEnd);
} else {
// 只补缺失的部分
Date fillStart = addDays(maxPartition, 1);
if (fillStart.before(targetEnd)) {
fillRange(fillStart, targetEnd);
} else {
log.info("数据已完整,无需补数");
}
}
增量补数的优势
- 节省时间:只跑缺失的部分,不跑重复的
- 节省资源:减少不必要的计算和存储
- 适合长期维护:每天跑一次增量补数,保持数据完整性
不过增量补数有个前提——你的数据源必须能支持“按时间范围读取”。比如Hive分区表、HBase按时间戳扫描、或者数据库的日期字段索引。如果数据源不支持范围查询,增量补数就玩不转了。
避坑指南:我曾经遇到一个项目,增量补数逻辑里没处理“数据回溯”的情况。业务方修改了历史数据,要求重新补某几天的数据,结果增量补数检测到那些分区已经存在,直接跳过了。后来我加了一个强制补数模式,允许用户指定“即使存在也要覆盖”。
4.4 全量补数:简单粗暴
全量补数,就是不管目标表里有没有数据,把整个时间范围内的数据全部重新跑一遍。简单粗暴,但有时候反而是最稳妥的方案。
什么时候必须用全量?
- 数据源不可回溯:比如某些API只返回最新数据,无法按历史时间查询。
- 业务逻辑变更:计算逻辑改了,历史数据必须全部重算才能保证一致性。
- 数据质量修复:发现历史数据有脏数据,需要整体清洗重跑。
全量补数的代码反而最简单:
// 伪代码:全量补数
for (date in fullDateRange) {
// 不管有没有数据,直接覆盖
overwritePartition(date);
runTask(date);
}
但全量补数的代价也最大。补一个月的数据可能还好,补一年的数据,那资源消耗和等待时间都是几何级增长。
全量补数的资源评估
假设单天任务需要10分钟、消耗5GB内存。补365天的全量数据:
- 串行模式:365 × 10分钟 ≈ 60小时,约2.5天
- 并行模式(并发10):365/10 × 10分钟 ≈ 6小时,但内存峰值 = 10 × 5GB = 50GB
所以全量补数前,一定要先算清楚资源够不够。我一般建议:全量补数放在业务低峰期,并且提前跟运维打好招呼。
4.5 四种模式的组合策略
实际项目中,很少只用单一模式。我总结了几种常见的组合:
| 组合模式 | 适用场景 | 示例 |
|---|---|---|
| 串行 + 增量 | 任务有依赖,且只需补少量缺失数据 | 每日凌晨补前一天失败的增量数据 |
| 并行 + 增量 | 任务无依赖,快速补缺失数据 | 补最近一周的日志清洗任务 |
| 串行 + 全量 | 任务有依赖,且需要整体重算 | 财务月报重算,每天依赖前一天 |
| 并行 + 全量 | 任务无依赖,快速全量重跑 | 离线数仓的维度表全量刷新 |
嗯,这里要注意一点:组合模式不是简单的“1+1=2”。比如并行+全量,并发度一定要根据集群水位动态调整。我习惯的做法是:先跑一个测试任务,看看单任务资源消耗,再反推最大并发数。
我的建议:在调度系统设计时,把这四种模式做成可配置的参数。用户提交补数任务时,可以自由选择:
- 执行模式:串行 / 并行(并配置并发度)
- 数据范围:增量 / 全量(或指定具体时间范围)
- 失败策略:终止 / 跳过 / 重试
这样既灵活,又不会把选择权完全丢给用户——毕竟不是所有人都懂底层原理。
四种模式没有的好坏,关键看场景。串行稳但慢,并行快但吃资源,增量省事但有前提,全量简单但代价大。你想想看,如果业务方说“给我补一下上个月的数据,半小时内要”,你选哪种?我肯定选并行增量——先查哪些天缺失,然后并发跑,半小时大概率能搞定。