01链路的形状

text
采集机 JSONL(dt 分区)
  → 打包拉取 → HDFS ODS
  → Spark 多阶段加工
      A 清洗      → dwd_price_snapshot_di
      B 参数抽取  → dwd_product_param_di
      C 实体对齐  → dwd_product_df
      C2 双源对价 → dwd_price_dual_df
      D 截面特征  → dws_*
  → 导出 staging JSON
  → Doris Stream Load → ADS
  → 对账校验

分层本身没什么新鲜:ODS 只做搬运和留痕,DWD 定粒度,DWS 做轻度汇总,ADS 只服务具体展示。真正花时间的是下面这些工程细节。

02参数为什么不写进集群配置

提交命令里显式带上了内存、核数、shuffle 分区数、map 去重策略:

bash
spark-submit \
  --master spark://hadoop01:7077 \
  --driver-memory 512m \
  --executor-memory 512m \
  --total-executor-cores 2 \
  --conf spark.sql.shuffle.partitions=6 \
  --conf spark.sql.mapKeyDedupPolicy=LAST_WIN \
  ...

不改进全局配置,是因为这套集群所有组件混部在同一批机器上:HDFS、Doris FE/BE、Spark 常驻进程加起来已经把内存吃掉大半。默认配置下 Spark 会尽量多起 executor,结果不是 OOM 而是更糟的“假死”——进程在,任务不推进。把限制写死在提交命令里,每一次运行的资源诉求都是可读的。

shuffle.partitions 调小是为了匹配小集群的并行度。默认值在大集群合理,在这里会产生海量空分区,任务提交开销反而盖过计算。

mapKeyDedupPolicy=LAST_WIN 是被 DUPLICATED_MAP_KEY 教育的结果:上游构造 map 时允许重复键(原始参数里同名 key 来自不同区块),Spark 默认策略直接抛异常。这个配置的含义是“重复键以后写入者为准”——它把一次崩溃变成一个可接受的语义选择,而且这个选择必须写下来让人看见。

03executor 的代码从哪来

--py-files 指向一个打包好的 zip,而不是代码目录。这条规则背后是两次事故:

  1. 代码目录只存在于提交节点,而 executor 可能落在别的节点上。磁盘上的 .py 只对 driver(工作目录)有效,executor 唯一的代码来源就是那个 zip;
  2. --py-files 不接受目录,包内相对导入必须整体归档,否则 driver 能跑、executor 报 ModuleNotFoundError。

最麻烦的是 zip 属于构建产物:它过期不会报错,只会让你静默地跑旧代码。改了 stage 却看到“没有生效”的结果,是这类项目里最难查的一类问题,因为逻辑上你确定代码是对的。

治理方式很简单但必须自动化:提交前按源文件 mtime 重建 zip。人工记忆“改完要重打包”这件事,早晚会漏。

顺带一条同类规则:包内要带上数据文件(品牌词表 JSON),否则 executor 侧读不到。

04JDK 也能成为链路故障源

有一类崩溃完全不在 Python 代码里:executor 报 free(): invalid pointer、退出码 134,hs_err 日志指向 StringTable::intern。

结论是集群那个 JDK 小版本本身有原生堆损坏缺陷。换 JDK 版本后消失。为了不改变其他作业的默认行为,做法是加一个专用环境变量(只在跑湖相关任务时指定),代码里写成 JAVA_HOME=${HWLENS_JAVA_HOME:-/bigdata/server/jdk} ——新变量只在需要它的链路上生效,其余场景逐字节不变。

同一次排查还发现:同一作业里,Iceberg 的向量化读(走 Arrow unsafe)与生产 Parquet 读混跑也会崩。处置是在建表时就固化关闭向量化读、并使用兼容性最好的压缩编解码。把绕法写进建表定义,而不是记在脑子里。

05校验要在写入单点做一次,不能只在编排层

分区日期 dt 的格式校验原本只在编排脚本里用 bash 正则做。问题是 backfill 和手工补数脚本走的不是那条路,绕过了校验;一旦出现坏 dt,脏分区会一路写到服务层才炸。

修法是把断言收到写入的唯一入口:

python
assert re.fullmatch(r"\d{4}-\d{2}-\d{2}", dt), f"bad dt: {dt}"

原则是:任何“所有写入路径都必须满足”的约束,要放在写入函数里,而不是放在某条调用链上。编排层的校验只是便利,不是保证。

06对账必须在链路末尾

末尾有一段独立的校验,比对各层行数与关键指标是否一致。它的价值不在“发现错误”,而在给每个环节一个明确的失败边界:装载段没跑、湖表与生产表不一致、某天补数漏了——都在这一步暴露,而不是等到大屏上少一块图。

我后来把所有“看起来成功但可能没装载”的环节都按这个标准检查一遍:一个环节必须有独立的产出验证,否则它的成功状态不能进入下一段的判断条件。

07相关文章