补数把历史数据搞乱了——重跑机制怎么设计才安全?
本文目录
先说结论
安全补数四大原则:①幂等(重跑多少次结果一致)②隔离(补数和正常调度互不影响)③可追溯(什么时候谁补的什么)④可回滚(出问题能恢复)。每条都缺不得。
最容易翻车的不是技术,是流程:补数SQL没review、并发补数互相覆盖、补完没通知下游。技术保证「能补对」,流程保证「不补错」。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
「补数」是数据团队的家常便饭——上游延迟、ETL失败、口径调整、数据修复,都需要补数。但「补数翻车」也是高频事故:补错了分区、补的逻辑和原逻辑不一致、补完下游没刷新、并发补数互相覆盖。这道题考的是工程严谨性。我在出海硬件公司维护过一套支撑日均500+次补数的重跑平台,今天聊聊核心设计。
补数的三种典型场景
| 场景 | 原因 | 范围 | 技术方案 |
|---|---|---|---|
| 失败重跑 | ETL任务失败 | 单分区 | 调度系统自动重试 |
| 延迟补数 | 上游延迟才到 | 1-N天 | Backfill脚本 |
| 逻辑修复 | ETL逻辑bug | 可能历史全量 | 全表回算 |
| 维度变更 | 维度口径调整 | 全表回算 | 全表回算+下游联动 |
原则一:幂等性设计
幂等是补数的生命线。同一个分区补N次,结果必须完全一样。这要求ETL写法必须用OVERWRITE而不是INSERT INTO:
-- ❌ 危险写法:INSERT INTO
-- 重跑会叠加,数据翻倍
INSERT INTO TABLE dwd_order PARTITION(dt='${bizdate}')
SELECT ... FROM ods_order WHERE dt = '${bizdate}';
-- ✅ 安全写法:INSERT OVERWRITE
-- 无论跑几次,结果都是「最新一次的产出」
INSERT OVERWRITE TABLE dwd_order PARTITION(dt='${bizdate}')
SELECT ... FROM ods_order WHERE dt = '${bizdate}';
-- ✅ 流式补数也要幂等:用UPSERT或幂等键
-- 每条数据带biz_id,下游用ON CONFLICT保证幂等
我在出海硬件公司订过一条铁律:所有分区表的ETL必须用OVERWRITE,违反者code review不通过。半年后再没出现「补数据翻倍」类故障。
原则二:隔离机制(补数 vs 正常调度)
最危险的场景:白天补数和凌晨调度撞车。比如凌晨调度跑当日分区,白天有人手动补昨天的,但写的WHERE条件带'今天',结果把今天的数据覆盖。隔离三层:
# 隔离方案
# 1. 物理隔离:补数走单独资源池
backfill_resource_group = 'mr_backfill_pool'
regular_resource_group = 'mr_daily_pool'
# 2. 时间窗口隔离:补数禁止操作未来分区
def safe_backfill(table, dt_range):
today = date.today()
if any(d >= today for d in dt_range):
raise Exception('禁止补数操作今日及未来分区')
if any(d < today - timedelta(days=90) for d in dt_range):
raise Exception('补数超过90天需要Tech Lead审批')
# 3. 锁机制:同一分区不能并发补数
with distributed_lock(f'backfill_{table}_{dt}'):
run_backfill_sql(table, dt)
原则三:可追溯(每次补数都有记录)
补数操作必须留痕——什么时候、谁、为什么、补了哪些分区、结果如何。我设计了一张补数审计表:
CREATE TABLE backfill_audit (
backfill_id STRING, -- 补数任务ID
operator STRING, -- 操作人
reason STRING, -- 补数原因(必填)
ticket_id STRING, -- 关联工单
table_name STRING, -- 目标表
partition_range STRING, -- 分区范围
sql_text STRING, -- 补数SQL
start_time TIMESTAMP,
end_time TIMESTAMP,
status STRING, -- SUCCESS/FAILED
affected_rows BIGINT, -- 影响行数
diff_with_original DOUBLE, -- 和原数据的差异
approver STRING, -- 审批人(高危补数必填)
downstream_notified BOOLEAN -- 是否通知下游
) PARTITIONED BY (dt);
这张表帮我多次定位故障——某次下游报数据不对,查audit表立刻发现昨天有人补过数,原因是某字段口径调整。如果没记录,根因要排查几个小时。
原则四:可回滚(保留快照)
补数前必须保留原始数据快照,万一补错了能恢复:
-- 方案1:物理快照(小表)
CREATE TABLE dwd_order_backup_20240315_v1 AS
SELECT * FROM dwd_order WHERE dt = '20240315';
-- 方案2:Delta Lake/Iceberg版本(推荐)
-- 自动保留版本,无需手动备份
DESCRIBE HISTORY dwd_order;
-- 显示所有版本,含时间戳和SQL
-- 一旦补错,回滚
RESTORE TABLE dwd_order TO VERSION AS OF 42;
-- 方案3:分区级别的「待发布」机制
-- 补数先写到 dwd_order_staging.dt='20240315'
-- DQC通过后才rename到正式表
ALTER TABLE dwd_order_staging RENAME PARTITION (dt='20240315') TO dwd_order;
补数的标准流程
# 补数SOP(含审批和DQC)
def run_backfill(table, dt_range, sql, reason, ticket_id, operator):
# Step 1: 前置校验
validate_partition_range(dt_range)
validate_sql_safety(sql) # 检查WHERE条件、避免无限制更新
# Step 2: 风险审批
risk = assess_risk(table, dt_range)
if risk == 'HIGH':
require_approval(operator, ticket_id) # 钉钉审批
# Step 3: 备份
snapshot_id = create_snapshot(table, dt_range)
# Step 4: 执行(加锁、用资源池)
with distributed_lock(f'backfill_{table}_{dt_range}'):
run_in_pool('mr_backfill_pool', sql)
# Step 5: DQC校验
if not run_dqc(table, dt_range):
rollback_to_snapshot(table, snapshot_id)
raise Exception('DQC failed, rolled back')
# Step 6: 通知下游 + 触发下游补数
notify_downstream(table, dt_range)
trigger_downstream_backfill(table, dt_range)
# Step 7: 审计落库
log_to_audit_table(operator, ticket_id, sql, ...)
面试高频追问
追问一:补数和Time Travel是什么关系?
Time Travel是「事后回滚」的能力,补数是「事中重新计算」的能力。两者互补:补数前用Time Travel做snapshot,万一补错了能立刻恢复。生产实践里,对Delta Lake/Iceberg表的补数远比Hive表安全——后者一旦OVERWRITE出错就回不去了。这也是我推动核心表都迁到Delta Lake的原因之一。
追问二:补数引发的下游连锁反应怎么处理?
两个机制:①血缘驱动的自动级联——当上游表某分区被补数,调度系统自动把所有下游同分区任务标记为「过期」,等待重跑;②业务方明确预期——补数前钉钉群广播「dwd_order 20240315分区已重置,下游报表预计14:00更新」。技术+沟通双管齐下,避免下游用着「还没刷新的旧数据」做决策。
追问三:超大规模补数(30天+全表)怎么做?
两个原则:①分批跑——按时间或按维度拆成多个小批,避免单次SQL跑太久失败重头来过;②先冒烟再全量——先补1天,DQC通过、下游验证OK,再补全量。我在出海硬件公司做过一次180天全表回算,拆成6个30天的批次,每批跑前后都做对账,整个过程花了3天,零故障。
实战踩坑:并发补数互相覆盖的事故
出海硬件公司有段时间数据团队在做某重大口径调整,需要补过去90天的某指标。两个同事不知道对方也在做,同时跑了补数SQL。两人的SQL逻辑不完全一样(一个用了新口径,一个还在用旧口径),在Hive上互相OVERWRITE,最终生效的是「最后跑完的那个」。
第二天业务发现数据「时而正确时而错误」——其实是被两套SQL交替覆盖。复盘的时候才发现这种问题。
解决方案:立刻在补数平台加了分布式锁机制:
# 用Redis/Etcd做分布式锁
lock_key = f'backfill_{table}_{partition}'
with redis.lock(lock_key, timeout=3600):
# 这里只能同时有一个进程在跑
run_backfill_sql(...)
# 如果第二个补数请求来了,要么排队,要么直接拒绝
if redis.is_locked(lock_key):
raise Exception(f'另一个补数任务正在运行:{get_lock_owner(lock_key)}')
教训:生产环境的补数从来不是「一个人的事」。多人并发、跨任务依赖、上下游联动,必须有锁机制+审计追踪。补数平台是工具,但更重要的是流程和规范。
总结
安全补数四原则:幂等、隔离、可追溯、可回滚。配合标准SOP(前校验+审批+备份+执行+DQC+通知+审计)和分布式锁机制。补数是工程严谨性的试金石。面试时把四原则+SOP+并发覆盖故事讲清楚,秀出工程师素养。
下一篇聊一个性能向的话题:高基数字段(user_id/device_id)的质量校验怎么做才不爆内存?
你们补数有没有审批流程?踩过什么坑?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长