这是最容易误诊的一类 bug:**同一份脚本,我跑是对的,机器跑是错的。**差别只在环境。

01三类缺口

·一:PATH 里没有大数据组件

cron 的执行环境是非登录 shell,PATH 只有最基础的两个目录,登录时通过 custom_env.sh 导出的 HADOOP_HOME 之类根本不存在。于是 hdfs 变成“未找到命令”。

最危险的连带效应是这个:

bash
set -u                 # 只设了 -u,没设 -e
lines=$(hdfs dfs -count /hardware/ods | awk '{print $2}')
echo "ingest 成功,行数 $lines"

命令找不到时返回码非零,但 set -u 不管返回码;空输出喂给 awk 也只是个空串,脚本继续往下走,最后报“成功”。丢数就这么发生了,而且日志里看不到失败。

同一次故障里还有另一种表现:另一个脚本在 set -Eeuo pipefail 下,$(hdfs ...) 以 127 退出直接让整个主脚本中断。同一个环境缺口,在一处造成静默丢数,在另一处造成硬失败——取决于脚本的严格程度。

修复不是"把 HADOOP_HOME 补进去"。那只解决了 hdfs 一个命令。正确做法是:让 cron 入口显式 source 与人工登录时同一份环境文件,然后对关键命令做存在性断言:

bash
source /bigdata/soft/custom_env.sh
for c in hdfs spark-submit java; do
  command -v "$c" >/dev/null || { echo "missing: $c"; exit 1; }
done

·二:中文路径 + POSIX locale

工作目录里有中文(项目路径本来就叫“论文”)。Java 在 POSIX locale 下解析不了非 ASCII 的工作目录,Spark launcher 抛 ExceptionInInitializerError,接下来数组构造失败报 bad array subscript。

诡异之处在于它只在 cron 里出现:登录 shell 有 UTF-8 locale,所以手工跑永远是对的。

我用四组对照实验把变量钉住:ASCII 路径 + POSIX locale 正常,非 ASCII 路径 + UTF-8 正常,只有 非 ASCII 路径 + POSIX locale 这一组复现失败。

修复:检测到 locale 是 POSIX / C 时,从 locale -a 里挑一个可用的 utf8 条目显式导出,而不是假设系统里一定有某个具体名字。

这件事教给我的是:手工验证通过 ≠ 定时任务能跑。登录 shell 是“最舒服的环境”,它带着你所有的交互配置;cron 是最小环境。用舒服环境验证最小环境要出的问题,永远验证不到。

·三:门禁自己的判据写错了

链路里有一段“质量门禁”,跑完检查维表当天分区是否存在。但那张维表按约定是全量替换、根本没有 dt 分区——两侧实现是一致的,错的是门禁本身。

更糟的是它一直没暴露:断点标记是人工落下的,门禁段从未真正执行过。**断点续跑机制掩盖了一个 bug。**这个组合很常见:一个“跳过已完成”的优化,会让它下游的代码长期不被执行,直到某天全量重跑才发现那里早就坏了。

02验证方法

现在所有编排脚本的改动,我都用同一个方式验证:

bash
env -i HOME=/root bash -c '/path/to/run_nightly.sh ...'

env -i 清空环境,最接近 cron 现场。用登录 shell 验证 cron 脚本,等于没验证。

03顺手补上的两类响亮失败

  • 入湖阶段显式检查每类目入湖行数,为零零行直接 FATAL(而不是“跳过”);
  • 管道里的退出码要显式处理。... | grep '^{' 这类过滤是必要的(HDFS CLI 会把 WARNING 混进 stdout,不过滤会在校验环节报数据质量错误),但空分区时 grep 返回 1,配合 set -e 会让脚本直接终止。加了 || true 和空表保护,同时警惕反方向:| grep ... || true 会把真实退出码吞掉,造成假成功。

一条经验:set -e 的世界里,管道和 || true 是最容易出“沉默失败”的两个地方。要么显式判空,要么显式记录退出码,不要指望默认行为替你负责。

04相关文章