4、补数执行模式:串行补数、并行补数、增量补数、全量补数

补数任务怎么跑,其实是个挺讲究的事。我见过不少团队,上来就把所有补数任务扔到集群里,结果资源打满,线上任务全堵死了。说白了,补数执行模式选不对,轻则效率低,重则把整个调度系统搞崩。

这一章,咱们就把四种核心模式掰开揉碎讲清楚。我个人习惯把这四种模式分成两个维度来看:执行顺序和数据范围。串行和并行管的是“怎么跑”,增量和全量管的是“跑哪些”。

补数执行模式分类 补数执行模式 执行顺序维度 数据范围维度 串行补数 并行补数 增量补数 全量补数 核心思路 • 串行 vs 并行:决定补数任务在时间轴上的执行策略 • 增量 vs 全量:决定补数任务覆盖的数据范围 • 实际项目中,这四种模式可以组合使用,比如“串行增量补数” • 选型依据:数据量大小、任务依赖关系、资源水位、业务容忍度

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 增量补数:只补缺失的

增量补数,说白了就是“缺多少补多少”。系统会检查目标分区或表里已经有哪些数据,只补那些缺失的时间段。

实现思路:

  1. 查询目标表的最大分区或最新数据时间戳
  2. 计算从该时间点到目标结束时间的差值
  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”。比如并行+全量,并发度一定要根据集群水位动态调整。我习惯的做法是:先跑一个测试任务,看看单任务资源消耗,再反推最大并发数。

我的建议:在调度系统设计时,把这四种模式做成可配置的参数。用户提交补数任务时,可以自由选择:

  • 执行模式:串行 / 并行(并配置并发度)
  • 数据范围:增量 / 全量(或指定具体时间范围)
  • 失败策略:终止 / 跳过 / 重试

这样既灵活,又不会把选择权完全丢给用户——毕竟不是所有人都懂底层原理。

四种模式没有的好坏,关键看场景。串行稳但慢,并行快但吃资源,增量省事但有前提,全量简单但代价大。你想想看,如果业务方说“给我补一下上个月的数据,半小时内要”,你选哪种?我肯定选并行增量——先查哪些天缺失,然后并发跑,半小时大概率能搞定。