做数据开发久了,每个人都会攒下一份自己的“报错翻译手册”。DataWorks 这类一站式数据开发治理平台,把数据集成、数据开发、调度运维、数据质量这些环节串成了一条链,好处是全链路在一个界面里就能跑通,坏处是一旦中间某个环节出问题,报错信息可能落在完全不同的三四个位置,你得先知道去哪儿看,才知道怎么修。这份记录就是围绕“DataWorks 常见错误”这一个主题,把我这些年踩过的坑、帮人排查过的现场、以及那些文档里往往一句话带过但实际能卡你半天的细节,按模块整理成一份可以直接对照使用的手册。内容面向刚接手数据开发任务的新人,也面向已经能跑通流程但遇到报错就只会点重跑的老手,读完你应该能做到:看到一段报错信息,三分钟内判断它属于哪一层、大概什么原因、下一步该动哪个配置。
1. 先把报错的“坐标系”建起来
1.1 报错到底该在哪里看,这一点决定了排查效率
很多人排查效率低,不是技术不够,而是找错了地方。DataWorks 的报错至少有四个层级,落点完全不同:编辑器里的即时校验报错、提交(Commit)时的静态检查报错、运行时(Run)的执行引擎报错,以及调度实例的运维报错。这四层是递进关系,前一层不过,后面根本走不到。
编辑器里的报错最好处理,通常是语法高亮标红或者执行前校验弹窗,提示词往往是“Parse exception”“Semantic analysis exception”这类,指的是 SQL 文本本身有问题。提交时的报错会多一层节点配置校验,比如循环依赖、输出名重复、资源组未绑定,这些跟 SQL 无关,是节点层面的问题。运行时才是真正落到计算引擎,报错会以FAILED:开头带一串错误码。调度实例的报错则要看运维中心,重点不是错误码,而是实例状态和上下游依赖。
我的经验是:先判断“任务有没有跑起来”。任务根本没跑,去看编辑器;跑起来了但秒失败,看运行日志;跑了几分钟才失败,多半是资源或数据量问题;任务一直“等待中”不失败,那就是依赖或资源组的事,跟 SQL 一点关系都没有。这个判断做完,能省掉一半无效排查。
提示:运维中心的实例详情里,能直接跳到当次运行的日志。别在编辑器里反复重跑去找同一个错误,调度环境和开发环境用的是不同的项目空间,数据、权限、参数都可能不一样,开发环境跑通不等于调度能跑通。
1.2 错误码前缀和模块的对应关系
DataWorks 底层跑在计算引擎上,所以大部分运行时报错带的是引擎自己的错误码体系。虽然错误码会随版本迭代微调,但前缀和语义分区是相对稳定的,记住几组高频的就够用了。
| 错误码 / 关键词 | 常见含义 | 优先怀疑方向 |
|---|---|---|
ODPS-0130071 | 语义分析失败 | 列名、表名写错,类型不匹配,函数参数不对 |
ODPS-0130161 | 语法解析失败 | 少逗号、括号不配对、关键字位置错 |
ODPS-0123031 | 分区不存在 | 分区值格式、分区是否已产出 |
ODPS-0420095 | 鉴权失败 | 无表权限、跨项目未授权、账号无角色 |
ODPS-0123144 | 作业被终止 | 内存超限、数据倾斜、单作业资源不够 |
ODPS-0110061类 | 系统内部错误 | 偶发,先重试一次再排查 |
DBUtilErrorCode | 数据集成连接层错误 | 连接串、账号密码、网络白名单 |
脏数据/DirtyRecord | 数据质量类错误 | 字段类型、长度、空值约束 |
这张表不用背,但**要建立“看到 013 开头先怀疑 SQL 写法,看到 042 开头先怀疑权限,看到 011 开头先重试一次”**的条件反射。我见过太多人拿到一个鉴权报错,在那儿一行行读 SQL 逻辑,读了半小时才发现是没申请的权限。
1.3 一条报错信息的正确读法
一条典型的运行时报错长这样:FAILED: ODPS-0130071:[12,8] Semantic analysis exception - column t2.user_id cannot be resolved。这里面每个部分都有信息量,但大部分人只看了前半段。
[12,8]是行号和列号,指的是编译出错的位置,不是数据出错的位置。Semantic analysis exception是错误大类,后面才是具体原因。column t2.user_id cannot be resolved告诉你,编译器在 t2 这个别名对应的表里找不到 user_id 这一列。
关键点在于:行号经常“漂移”。如果你用了子查询、CTE、多表 JOIN,报错行号可能指向的是外层语句,而真正的问题在内层。这时候别死盯那一行,往下找Caused by或者找报错里出现的表别名,直接去核对那张表的字段清单,效率高得多。
还有一个细节:报错只报第一个错误。SQL 编译器是遇到第一个致命错误就停,所以你修好一个,可能立刻冒出第二个、第三个。这不是你没修好,是编译器还没走到那儿。改 SQL 的时候,最好一次性把同类问题(比如字段类型、字段名)全核一遍,别一轮一轮试。
2. 数据开发环节的几类高频报错
2.1 字段解析失败:九成不是 SQL 写错了
column xxx cannot be resolved和table not found这两类报错,占了我见过的 SQL 报错的绝大部分,但它们背后的真实原因非常分散。
第一种,表真的不存在或者写错了项目前缀。跨项目访问表必须写全项目名.表名,而且这个项目你得有权限。常见情况是:表在开发环境存在,调度环境指向的项目不一样,于是报“表不存在”。这时候不是 SQL 的问题,是环境切换的问题。
第二种,DDL 还没生效。你刚加了一列,立刻就去查,有时候元数据有缓存延迟,尤其在表特别多、变更特别频繁的项目里。解决办法很简单,等一两分钟或者重新打开一个会话再试。
第三种,大小写和反引号。引擎对字段名大小写通常是敏感的(具体看项目配置),而很多人从别的系统复制字段名过来,大小写对不上。还有一种情况是字段名撞了保留关键字,比如order、desc、date,这种必须用反引号包起来。我踩过一次date字段没加反引号,报错信息看了十分钟才反应过来。
第四种,SELECT * 展开后与 UNION 列数不匹配。两个子查询做 UNION,其中一个select *,另一个手写了列,上游表一加列,UNION 就炸。报错通常指向 UNION 那行,但根因在上游表结构变了。做 UNION 的时候老老实实把列名写全,这不是啰嗦,是给自己省事。
-- 容易炸的写法:上游表加一列,这里立刻报列数不匹配 select * from dwd_order_detail where ds = '${bizdate}' union all select order_id, user_id, amount from dwd_order_refund where ds = '${bizdate}'; -- 稳一点的写法:两边列名、顺序、类型都对齐 select order_id, user_id, amount from dwd_order_detail where ds = '${bizdate}' union all select order_id, user_id, amount from dwd_order_refund where ds = '${bizdate}';提示:字段类型不一致导致的隐式转换,报错往往很晚才出现,甚至在数据写出的那一刻才报。养成 UNION 两边显式
cast的习惯,比事后排查便宜得多。
2.2 分区报错:报错信息很短,坑却很深
分区相关的报错信息往往只有一行,比如Partition not found,简短到让人以为很容易解决,实际上它至少对应四种完全不同的情况。
第一种,分区根本没产出。上游任务失败了,或者上游还没跑完,你这边就开始了。这种要去看依赖配置,不是改 SQL 能解决的。判断方法很直接:去数据地图或者直接show partitions 表名,看目标分区在不在。
第二种,分区值格式不一致。有的表分区是ds=20240101,有的写成ds=2024-01-01,还有的用dt=2024-01-01 00:00:00这种。跨表关联的时候格式对不上,就是查不到。这种问题在跨团队取数时特别常见,因为两个团队建表规范不一样。
第三种,动态分区没开或者开得不对。动态分区写入需要额外参数,而且有分区数量上限,写多了会被拦。见过一次往一张按小时分区的表里写一天的数,直接触发上限报错,把参数调大才过。但这属于“能跑但危险”的写法,分区数暴涨会拖垮元数据,更好的做法是拆批。
第四种,分区值里有特殊字符或者空格。从 Excel 或者 CSV 导进来的分区值带了个尾随空格,肉眼看不出来,引擎就是认不到。这种只能靠length()或者对比字符串字节数来找。
-- 排查分区是否存在 show partitions dwd_order_detail; -- 排查分区值有没有脏字符 select distinct length(ds) as len, ds from dwd_order_detail where ds like '2024%' limit 100;我个人有个习惯:所有分区字段的生成,都经过一层统一的时间格式化函数,不允许各处手拼字符串。这个约束一开始有人嫌麻烦,但它把“分区格式不一致”这一类问题从根上掐掉了。
2.3 权限报错:别急着找人开权限,先确认三件事
鉴权失败的报错关键字通常是Authorization Failed、Access Denied、No privilege,后面会跟一个权限点和对象名。看到这种报错,第一反应不该是去提工单,而是先自己核对三件事。
第一,你用的是哪个账号。开发环境和调度环境跑任务的账号经常不是同一个。开发时用个人账号,调度时用的是项目绑定的调度账号。个人有权限不代表调度账号有权限,这是最典型的“开发跑通、上线失败”。解决办法是把权限授给调度角色,或者把任务改成用项目所有者身份运行(要评估安全影响)。
第二,这是表权限还是项目权限。读一张表需要Select权限,写需要Alter/Insert,跨项目还需要 package 授权。很多人以为申请了表的读权限就万事大吉,结果写目标表的时候又炸一次。
第三,自定义函数和资源也需要授权。用了别人项目里的 UDF,或者引用了某个资源文件,同样要走跨项目授权流程,这类报错的对象名往往是一个函数名或者资源名,容易被误判成表权限问题。
跨项目访问的标准姿势是走 package:源项目建 package,把表加进去,允许目标项目安装;目标项目安装 package 之后,才能在 SQL 里引用。这个流程本身不复杂,但步骤顺序必须对,先装后加表是不生效的,得重新装一次。
注意:权限这东西有缓存。授权成功了但依然报无权限,先等几分钟,别立刻推翻重来。
3. 数据集成同步任务的报错
3.1 脏数据:最常见的“看起来没问题”的失败
数据集成任务的报错里,脏数据(DirtyRecord)绝对排第一。它的迷惑性在于:任务状态是“失败”,但你看日志会发现绝大部分数据都同步成功了,只有那么几条被拦下来,然后整个任务失败。
脏数据本质上就是写入端拒绝接收的那部分记录,常见原因有这么几类:源端字段长度超过目标端定义、字符串塞进数值型字段、非空字段出现 NULL、日期格式不匹配、主键冲突(写入模式配置成 insert 而非覆盖时)。
处理脏数据有三条路,我按推荐顺序排:
第一条,从源头上治。找出脏数据的具体内容,看是不是上游采集逻辑有问题。这是最正确的做法,但往往牵扯到别的团队,推进慢。
第二条,在同步任务里加清洗。数据集成支持字段映射时做简单的类型转换和默认值填充。比如源端某个字段可能为空,就在映射里配一个默认值;字符串转日期,配好格式串。这招见效快,适合赶进度的场景。
第三条,调高脏数据阈值或者关掉脏数据检查。这是纯粹的应急手段,能过任务但把问题留给了下游。如果一定要用,至少把脏数据单独同步到一张表里留着,事后能追溯。
{ "type": "job", "configuration": { "setting": { "errorLimit": { "record": 100, "percentage": 0.01 } } } }上面这段是错误容忍配置的典型形态,record是绝对条数上限,percentage是比例上限,两个条件满足其一就中断。默认值往往很紧,数据量大一点、源端脏一点就直接失败。我的建议是:首次上线时把阈值设宽,观察一两轮之后再收紧,别一上来就设成零容忍。
还有一个高频坑:列顺序。数据集成支持按列名映射,也支持按顺序映射。如果配成了顺序映射,而源端表加了一列,整个数据就错位了,但任务可能不报错,只是数据全错。这类问题比报错更可怕。永远优先用列名映射,这是我给自己定的死规矩。
3.2 连接类报错:报错信息很唬人,原因往往很朴素
Communications link failure、Connection timed out、The last packet successfully received from the server was xxx milliseconds ago,这些报错看着像网络故障,实际上八成的根因在下面几个地方。
连接串写错了。端口、库名、参数拼错,尤其是从别的环境复制过来忘了改。这类错误的好处是必现,好排查。
账号密码过期或者权限不足。数据库侧改了密码,同步任务里没同步更新,报错信息可能是认证失败,也可能伪装成连接超时。遇到“昨天还好好的今天就不行”,第一个怀疑对象就是密码。
网络访问没打通。数据集成跑在资源组上,资源组和数据库之间的网络要通。独享资源组要绑定到对应网络环境,走专线的话还得配路由。这种情况报错通常是超时,而且只有调度环境失败、开发环境偶尔能通,因为开发调试可能走的是另一条链路。
并发太高把数据库压垮。同步任务并发数设太大,目标库连接数被打满,报错五花八门。这种问题的特征是间歇性失败,重跑有时能过。遇到“重跑就好了”的情况,别急着归因为“平台不稳定”,先看并发配置和数据库侧的连接数监控。
| 报错关键词 | 最可能原因 | 排查动作 |
|---|---|---|
Communications link failure | 网络不通 / 连接被重置 | 换个资源组试跑,检查网络配置 |
Access denied for user | 账号密码或授权问题 | 核对账号密码,确认库级权限 |
Too many connections | 并发过高 | 降低并发数,看数据库连接监控 |
Data too long for column | 字段长度不匹配 | 核对源目标字段长度定义 |
脏数据条数超过阈值 | 类型转换失败 / 空值 | 导出脏数据样本逐条核对 |
心得:数据集成排查最省时间的办法,是把任务配置里的“脏数据”样本导出来看。一条真实样本胜过看十遍报错信息。
4. 调度运维侧的报错与排查
4.1 实例一直“等待”,问题不在任务里
调度实例的状态有好几种:等待、等待上游、运行中、失败、成功。其中“等待”和“等待上游”是最容易让人误解的两个状态,因为它们根本不是任务出错。
“等待上游”很明确,就是前置依赖没满足。可能是上游任务失败了,可能是上游的产出时间比预期晚,也可能是依赖配置本身写错了(依赖了错误的节点或者错误的时间周期)。这时候去运维中心看依赖图,一眼就能看出来卡在哪个节点。
“等待”状态则更多是资源问题。任务提交上去了,但调度资源组没有空闲槽位,只能排队。这种情况下,如果队列特别长,任务可能等到超时才失败,报错信息是超时,但根因是资源不足。判断方法:看同一时间段是不是有大量任务都在排队,如果是,那就是资源组容量的问题,扩容或者错峰才是解法。
还有一种很隐蔽的情况:任务被配置成了定时调度,但周期表达式写错了。比如写了个不存在的日期,或者 cron 表达式和预期不符,任务压根没被生成实例。这种问题不会报错,只会表现为“任务没跑”。我一般会去运维中心看实例列表,如果连实例都没有,那就是调度配置的问题,不是运行的问题。
4.2 时间参数:一个字符的差别,结果天差地别
调度里最常被误用的就是时间参数。${bizdate}、${bdp.system.bizdate}、${yyyyMMdd}这些写法在不同版本、不同节点类型下行为不完全一样,混用就会出问题。
最常见的坑是时区。调度配置里有个时区设置,默认值和你本地时区可能不一样,导致${bizdate}取出来是“昨天”还是“今天”跟你预期错一天。这个错一天的问题在跑日增量的时候特别致命,因为不会报错,数据也能跑出来,只是算错了日期。
第二个坑是格式化函数和参数混用。有些人喜欢在 SQL 里写to_char(dateadd(...))来算日期,有些人直接用${bizdate},两种写法在补数据(补历史)的时候行为不同。补数据场景下,${bizdate}会自动替换成补的那个业务日期,而手写current_date()拿到的永远是今天。补数的时候数据算错,九成是这个原因。
第三个坑是跨周期依赖。日任务依赖月任务、小时任务依赖日任务,这种依赖配置要精确到周期。配错的后果是:要么永远等不到,要么依赖了个不存在的东西。我的习惯是配完依赖之后,去运维中心的依赖图里实际看一眼连线,比看配置表单靠谱。
-- 稳妥写法:统一走业务日期参数 insert overwrite table dws_user_day partition (ds = '${bizdate}') select user_id, count(1) as pv from dwd_user_log where ds = '${bizdate}' group by user_id; -- 危险写法:补数时会取到当天,导致历史数据被覆盖 insert overwrite table dws_user_day partition (ds = '${bizdate}') select user_id, count(1) as pv from dwd_user_log where ds = to_char(getdate(),'yyyymmdd') group by user_id;注意:
insert overwrite配合错误的时间条件,是数据事故的头号来源。它不会报错,只会安静地把分区覆盖掉。分区字段永远用调度参数,别用函数现算。
4.3 循环依赖与提交失败
提交节点时报“存在循环依赖”,原因通常是两个节点互相依赖,或者一个节点依赖了自己所在的下游链。这种问题在多节点批量提交时最容易出现。
排查方法很土但很有效:把依赖关系画出来。哪怕就画在纸上,A 依赖 B、B 依赖 C、C 又依赖 A,链条一拉长,用眼睛看配置表单是看不出来的。运维中心的依赖图能直接可视化,遇到循环依赖先去看图。
另一种提交失败是输出名重复。两个节点声明了同一个输出,系统无法判断下游该等谁,直接拒绝。这种情况多发生在复制节点的时候,复制完忘了改输出名。解决起来简单,但发现起来难,因为报错只提输出名,不告诉你是哪两个节点冲突,得自己搜。
5. 一份可以直接对照的速查表与几点私货
5.1 高频报错速查
| 现象 / 报错 | 大概率根因 | 一分钟验证方法 |
|---|---|---|
| 字段无法解析 | 环境切换、字段改名、大小写 | 在目标环境desc 表名核对字段 |
| 表不存在 | 缺项目前缀、无权限、环境不对 | 确认是否写了项目.表全名 |
| 分区不存在 | 上游未产出、格式不一致 | show partitions看实际分区值 |
| 鉴权失败 | 调度账号无权限 | 确认运行身份是个人还是调度角色 |
| 任务秒失败 | SQL 语法或配置错误 | 看报错行号,注意行号可能漂移 |
| 任务跑很久后失败 | 内存超限、数据倾斜 | 看是否有单 key 数据量异常 |
| 实例一直等待 | 依赖未满足或资源排队 | 看依赖图和资源组排队情况 |
| 同步任务失败但数据大部分成功 | 脏数据超阈值 | 导出脏数据样本查看 |
| 补数后数据算错 | 时间参数用了函数而非调度变量 | 检查 SQL 里是否有getdate()类写法 |
| 提交报循环依赖 | 节点互相依赖 | 打开依赖图看连线 |
5.2 几条花钱买来的经验
报错日志要存。每次解决一个非平凡的问题,我会把报错原文、根因、解决办法记一行到自己的笔记里。半年下来这份笔记比任何文档都好用,因为它是针对你所在项目的具体配置和表结构写的。团队里也可以共享这份记录,新人接手时能少走很多弯路。
重跑之前先想清楚为什么失败。无脑重跑在偶发问题上确实有效,但它会掩盖真正的定时炸弹。如果一个任务一周内失败了三次,每次都靠重跑解决,那一定是配置或数据层面有结构性问题,早晚会在大促或者关键节点上爆发。我的判断标准是:同一个任务连续两次失败,就必须停下来查根因,不允许第三次直接重跑。
环境差异要提前对齐。开发、预发、生产三套环境,项目名不同、表不同、权限不同、资源组不同。上线前把这三套环境的差异列一个清单,比上线后一个个试错强得多。特别是资源组和调度账号这两项,最容易在切换环境时被忘掉。
别忽略“不报错但结果不对”。这类问题比报错难查十倍。典型场景就是前面说的时间参数、列顺序映射、隐式类型转换。我的做法是给关键表加数据质量校验,比如行数波动、主键唯一性、关键字段非空率,让问题在小范围就被发现,而不是等到业务方来问“这个数为啥是零”。
关于错误码,别迷信。错误码会随版本迭代变化,同一个码在不同版本可能对应不同语义,反向也可能出现同一个问题报不同的码。真正靠谱的是报错文本里的关键词和上下文。我排查时通常是先抓关键词确定模块,再去核对具体的对象名(表名、字段名、分区值),最后才是去查错误码含义。顺序反了,容易在一个方向上钻牛角尖。
最后分享一个我一直在用的小技巧:遇到完全没见过的报错,先把报错信息里你自己的项目名、表名、字段名全部替换成占位符,剩下的一定是平台侧的通用描述。这么一剥离,你就知道哪些信息是环境相关的、哪些是通用的,再去搜或者问人,问题描述会清晰很多,得到的回答也准得多。这个动作花不了三十秒,但能让沟通效率翻倍。