为什么幂等性在数据工程中如此重要
Source: Dev.to
在数据工程中,失败是常态:作业崩溃、网络超时、Airflow 重试任务、Kafka 重放消息、回填重新运行数月的数据。在这个充满故障的世界里,幂等性 是保证数据正确、可信且可控的关键。
什么是幂等性?
如果一次运行或多次运行产生相同的最终结果,则该过程是幂等的。
- 示例:处理
2025‑01‑01数据的作业- 运行一次 → 正确结果
- 运行两次 → 同样的正确结果
- 运行十次 → 仍然是相同的结果
没有重复、没有膨胀、没有损坏。
为什么幂等性在分布式数据系统中很重要
现代流水线是分布式的:
- Spark 作业可能因 executor 丢失而失败
- Airflow(或 Dagster、Prefect)任务会自动重试
- 云存储通常具有最终一致性
- API 可能在请求中途超时
如果没有幂等性,重试会导致:
- 数据双计数
- 产生部分写入,破坏表格
- 在“修复”失败时引入新 bug
幂等性把重试从风险转变为特性。编排器默认任务可以安全重试;如果你的任务不是幂等的,重试会悄悄引入数据错误,“绿色 DAG”会隐藏坏数据,调试几乎不可能。
回填
回填是不可避免的(逻辑变更、Bug 修复、迟到数据、模式演进)。有了幂等的流水线,你可以:
- 自信地重新运行历史数据
- 避免手动清理
- 消除专门的回填代码路径
没有幂等性,每一次回填都是高风险,工程师害怕触碰旧数据,技术债务不断累积。
Exactly‑Once 与 At‑Least‑Once
- Exactly‑once 保证复杂且成本高。
- 分布式系统通常只提供 at‑least‑once 投递。
幂等性让你可以安全地接受 at‑least‑once 投递,通过优雅地处理重复数据。
设计幂等流水线
稳定的主键 & Upsert
使用稳定的主键(例如 order_id、user_id + event_time,或业务属性的哈希)。然后在读取时去重或在写入时合并:
MERGE INTO users u
USING staging_users s
ON u.user_id = s.user_id
WHEN MATCHED THEN UPDATE SET ...
WHEN NOT MATCHED THEN INSERT ...
优先使用 INSERT OVERWRITE 或 MERGE,而不是盲目追加:
INSERT OVERWRITE TABLE sales PARTITION (date='2025-01-01')
SELECT * FROM staging_sales WHERE date='2025-01-01';
确定性转换
纯粹的转换:
- 只依赖其输入
- 每次产生相同的输出
避免使用非确定性函数:
CURRENT_TIMESTAMP- 随机 UUID 生成
- 核心转换中调用外部 API
流式 & 增量作业
- 保存 offset、watermark 或处理时间戳。
- 设计同一窗口的重新处理为无操作(no‑op)。
- 确保数据写入是幂等的、下游明确的,并受到严格控制。
副作用
将副作用(邮件、Webhook、API 调用)与数据转换分离。仅在最终状态成功写入后触发它们,并使副作用本身也是幂等的(例如使用去重键或请求 ID)。
实践中的注意事项
该做
- ✅ 设计每个作业时假设它会被重试。
- ✅ 使用覆盖或合并,而不是盲目追加。
- ✅ 让作业确定且可重复。
- ✅ 使用主键和去重逻辑。
- ✅ 将回填视为一等公民。
- ✅ 记录输入、输出和检查点。
不该做
- ❌ 假设“此作业只会运行一次”。
- ❌ 在没有防护的情况下追加数据。
- ❌ 将副作用与转换混在一起。
- ❌ 依赖执行顺序来保证正确性。
- ❌ 在核心逻辑中使用非确定性函数。
- ❌ 依赖人工清理重复数据。
如果重新运行你的流水线让你感到害怕,那它就不是幂等的。
幂等流水线设计检查清单
在设计评审、PR 评审和事后审计时使用此清单。回答核心问题:“如果这个流水线运行两次,结果仍然正确吗?”
1. 重试安全性
- ⬜ 每个任务是否可以在无需手动清理的情况下重试?
- ⬜ 如果作业中途失败并重新运行会怎样?
- ⬜ 编排器(Airflow / Dagster / Prefect)是否会自动重试任务?
- ⬜ 部分写入是否在重试时被清理或覆盖?
- ⬜ 是否有明确的失败边界(每个分区、批次或窗口)?
- 🚩 红旗:“我们从不重试这个作业。”
2. 确定性输入
- ⬜ 输入是否明确限定(日期、分区、offset、watermark)?
- ⬜ 输入源在重新处理时是否稳定?
- ⬜ 是否以确定性的方式处理迟到记录?
- ⬜ 是否防止重复读取重叠窗口?
- 🚩 红旗:输入依赖 “now”、 “latest” 或隐式状态。
3. 写入策略
- ⬜ 写入策略是 覆盖、合并 还是 upsert?
- ⬜ 追加是否受到去重或约束的保护?
- ⬜ 输出是否按确定性键(日期、小时、batch_id)分区?
- ⬜ 单个分区是否可以安全重写?
- 🚩 红旗:盲目的
INSERT INTO或文件追加没有防护。
4. 记录身份
- ⬜ 每个数据集是否有明确的主键或自然键?
- ⬜ 去重逻辑是否明确且有文档?
- ⬜ 键在重试和回填期间是否保持稳定?
- ⬜ 去重是在读取时、写入时还是两者都进行?
- 🚩 红旗:“不应该出现重复。”
5. 确定性转换
- ⬜ 转换是否确定性?
- ⬜ 是否避免使用
CURRENT_TIMESTAMP、随机 UUID 或其他非确定性函数? - ⬜ 是否将外部 API 调用排除在核心转换之外?
- ⬜ 业务逻辑是否独立于执行顺序?
- 🚩 红旗:每次作业运行时输出都会变化。
6. 增量逻辑
- ⬜ offset、检查点或 watermark 是否可靠保存?
- ⬜ 重新处理同一范围是否安全?
- ⬜ “at‑least‑once” 投递是否得到正确处理?
- ⬜ 流水线能否在不损坏数据的前提下回放历史数据?
- 🚩 红旗:“我们无法回放这个 topic/table。”
7. 回填友好性
- ⬜ 流水线是否可以针对任意历史范围运行?
- ⬜ 回填逻辑是否与常规逻辑相同?
- ⬜ 重新运行旧分区时是否能干净覆盖或合并?
- ⬜ 下游消费者在回填期间是否受到保护?
- 🚩 红旗:回填需要特殊脚本或手动 SQL。
8. 隔离的副作用
- ⬜ 邮件、Webhook 或 API 调用是否与核心数据逻辑分离?
- ⬜ 副作用是否仅在成功完成后触发?
- ⬜ 副作用本身是否幂等(去重键、请求 ID)?
- ⬜ 是否有防止双重通知的机制?
- 🚩 红旗:副作用嵌入在转换步骤中。
9. 早期检测
- ⬜ 重跑时行数是否保持一致?
- ⬜ 数据质量检查是否可安全重跑?
- ⬜ 是否监控重复、空值和漂移?
- ⬜ 重跑和回填的血缘是否清晰?
- 🚩 红旗:没有办法判断数据是否意外变化。
10. 文档与所有权
- ⬜ 幂等行为是否有文档记录?
- ⬜ 新工程师是否能安全地重新运行流水线?
- ⬜ 恢复流程是否自动化,而非手动?
幂等性不仅是技术细节,更是一种设计哲学,使数据系统 更具弹性、更易运维、更低成本、更可信。在数据工程中,重新处理是不可避免的,失败是常态,幂等性决定了系统是脆弱的管道还是生产级别的系统。