本文作者:朱明亮,观远数据计算引擎开发工程师,13 年毕业于湖南大学,有七年多的大数据研发经验。曾就职于知名互联网公司数据研发专家岗位,负责打造服务公司内部各业务线的数据中台,是开源项目 byzer-lang(基于 Spark 的开源中台计算引擎)的主要贡献者之一。
背景
测试同学在内部环境测试发现一个严重的问题,ETL 执行结果不符合预期,根据 Rawdata Date 字段筛选 等于 2022-07-12 筛选出来的数据是 7.11 号的,直接筛选 2022-07-11 出来的数据是空的。
排查过程
1. 简化逻辑
ETL 流程过于庞大,首先需要花点时间简化 ETL,尽量最小化的复现出问题,利于排查。
简化的方法:
-
如果 ETL 执行时间较长,首先缩减数据集的大小,可以 limit 之后保存新的数据集,用新的数据集进行测试。
-
从头尾往中间缩减逻辑,直到不能复现问题。头部的缩减可以保存临时数据集,然后使用临时数据集进行计算,尾部的则直接预览上一个步骤的结果。
简化后的逻辑是两个数据集,一张 A 表,有日期类型字段 a,一张 B 表,有日期类型字段 a,b,用 sql 表示为:
select to_date(a) as a, to_date(b) as b from
(select to_date(a) as a, to_date(A) as b from
(select to_date(a) as a from A group by to_date(a)) t1
union all
select to_date(a) as a, to_date(b) as b from B group by to_date(a), to_date(b)) t2
group by to_date(a), to_date(b)
当然 job engine 得到的是脚本文件,这里写成 sql 更加直观看出逻辑,通过查看脚本,确定 BI 生成的逻辑是正确的,那问题是出在了 engine 层。
2. spark 本地复现排查
本地复现
将简化后的脚本转化为 spark 算子或者 spark sql,测试环境是 spark 3.2.1,在 spark 3.2.1 版本复现了该问题,本地复现后就便于去 debug 调试。
test("test1") {
val sqlText =
"""
|select to_date(a) a, to_date(b) b from
|(select to_date(a) a, to_date(a) as b from
|(select to_date(a) a from
| values ('2020-02-01') as t1(a)
| group by to_date(a)) t3
|union all
|select to_date(a) a, to_date(b) b from
|(select to_date(a) a, to_date(b) b from
|values ('2020-01-01','2020-01-02') as t1(a, b)
| group by to_date(a), to_date(b)) t4) t5
|group by to_date(a), to_date(b)
|""".stripMargin
spark.sql(sqlText).show()
}
期待的结果:
a | b |
---|---|
2020-02-01 | 2020-02-01 |
2020-01-01 | 2020-01-02 |
返回的结果:
a | b |
---|---|
2020-02-01 | 2020-02-01 |
2020-01-01 | 2020-01-01 |
可以看出来,所有数据的 b 取了 a 的值,接下来就是查看 spark 执行计划。
执行计划
先简单介绍下 Spark Catalyst 优化器。
一条 SQL 语句生成执行引擎可识别的程序,就离不开解析(Parser)、优化(Optimizer)、执行(Execution) 这三大过程。而 Catalyst 优化器在执行计划生成和优化的工作时候,它离不开自己内部的五大组件,如下所示:
-
Parser:将 sql 语句利用 Antlr4 进行词法和语法的解析;
-
Analyzer:主要利用 Catalog 信息将 Unresolved Logical Plan 解析成 Analyzed logical plan;
-
Optimizer:利用一些 Rule