装载这一段踩的坑有个共同点:都不报错。

01label 幂等:重跑变成“什么都没做”

Stream Load 用 label 做幂等。我们的 label 形态是:

text
{表名}_{dt}_{RETRY_TAG}

最初没有 RETRY_TAG,于是同一天第二次执行时 label 完全相同,Doris 返回 Label Already Exists——它的语义是“这个导入已经做过了”,属于幂等保护,不是错误。

处理方式经历了两次摆动:

  1. 早期把它判为失败 → 正常的重跑会中断链路;
  2. 改成三态(Success / SKIP / FAIL)→ 新问题出现:所有表都 SKIP 时,脚本仍然以退出码 0 结束。

第二种更危险,因为它在补数和重放场景下必然发生,而日志里每一行看起来都“正常”。

最终规范:

  • 生产与补数统一传 RETRY_TAG(时间戳),保证每次都是真实装载;
  • 幂等性交给目标表的 Unique 模型,而不是交给 label;
  • 汇总时把“全 SKIP”识别为异常状态,而不是成功。

这里有个概念要分清:**label 保证的是“不重复导入”,Unique 模型保证的是“重复导入结果一致”。**我们真正需要的是后者;前者只在“同一次运行被重试”时有意义。用前者去做重放的幂等,就会出现“重放什么都没做”的结果。

02UNIQUE KEY 只覆盖本次出现的键

主键模型的表看起来最安全:同键覆盖。但它的覆盖范围是本次导入里出现的键。

于是当某一天的结果行集变小(口径收紧、过滤更严、上游少数据),旧键的残留行不会被删除,表里就会同时存在“今天的口径”和“以前的口径”。我们的表现是同一张 ADS 表,本地比湖表多出若干行——不是同步丢了,是本地有历史残留。

规范:行集可能缩小时的语义变更,重载前先 DELETE ... WHERE dt = ...,让这一天的数据整体重建,而不是增量覆盖。

顺带一条 Doris 事实:不带 WHERE 的 DELETE FROM 是非法的。旧脚本里这句被 || true 包着,于是“清空”从来没生效过;叠加明细表的追加语义,那张本该全量替换的维表只增不减。

03全量替换的正确顺序:先确认输入就绪,再破坏现有状态

改成 TRUNCATE + load 之后,第一版仍然错,而且错得更隐蔽:我把 TRUNCATE 放在输入校验之前。

结果当 staging 文件缺失时:清空成功,装载失败。表变成空表,而链路只是“某一步红了一下”。

正确的顺序必须是:

text
① 确认输入就绪(分区存在 且 有效行数不为零)
② TRUNCATE
③ load
④ 装载后校验(行数 / 关键指标)

原则:任何破坏性动作之前,先把它依赖的输入验证做完。“先清再装”看起来对称,实际上是把可恢复状态破坏掉之后才开始检查能不能恢复。

04平铺 staging 造成的跨日期污染

导出层原来把文件平铺在一个目录里,也就是说:目录里永远是最后一次导出的内容。

而维表装载是按“目标日期”驱动的,两者无关。于是补历史那天发生了:回填了较早一天的数据,随后装载当前日期,结果线上维表被装成了回填那天的快照。

修法是给 staging 目录加分区:

text
ads_product_dim/dt=$DT/...

缺分区直接失败,而不是“读到上一次的残留”。

这类问题的通用形态是:中间产物的标识必须携带它服务的对象。当“最后一次写入”成为唯一状态,任何并发补数或乱序回填都会互相污染,而且污染是静默的。

05管道里的 stdout 污染

装载前要从 HDFS 读数据,命令长这样:

bash
hdfs dfs -cat /path/* | grep '^{' 

那个 grep 不是装饰:HDFS CLI 会把告警信息混进 stdout,不过滤就会被当成数据行送进 Doris,触发数据质量错误。

但空分区时 grep 没有匹配,返回码非零,配合 set -e 会让脚本在“其实一切正常”的情况下退出。所以要加 || true,同时提供空表保护——补历史的第一天没有前一天可比,零行是正确语义,不是故障。

注意这与前面 || true 吞退出码是同一个语法的两面:加它之前要能说出它在忽略哪一种失败。

06装载之后的对账

链路的最后是一段独立校验,比对各层与 Doris 表之间的行数和关键指标。

几条配套经验:

  • 查外部表要先 REFRESH(元数据有 TTL 缓存,否则你看到的是过期的表结构,会误判成“数据不对”);
  • 对账客户端只在特定节点上有,应用机没装——这类环境差异会让人以为查询失败,其实是找不到命令;
  • 对账结论要能区分“丢了”和“口径不同”,前者是链路问题,后者需要改文档而不是改数据。

07相关文章