使用spark-sql处理Doris大表关联

背景

最近项目上有一个需求,需要将两张表(A表和B表)的数据进行关联并回写入其中一张表(A表),两张表都是分区表,但是关联条件不包括分区字段。

分析过程

方案一

最朴素的想法,直接关联执行,全表关联,一条SQL搞定全部逻辑。想法越简单,执行越困难。由于数据量大,服务器规模较小,尽管各台服务器内存和CPU配置都很高,关联会将数据读取到内存,内存根本放不下,而且集群配置了workload group,可使用内存更小了,方案一不可行。

方案二

可以在关联时增加分区字段对任务进行拆解,这样可以实现,但是会形成笛卡尔积,历史数据量巨大,分区较多(A表和B表都是1年),缺点也很明显。

  • 执行耗时长
  • 执行语句太多,操作不便
  • 如果按照单分区关联,A表的每一个分区将会扫描B表全表

经过评估上述方案二不可行。

方案三

通过外部计算和存储来实现,可选的有Hive、Spark、Flink。三种方案都是可行的,但是从操作复杂度来看使用spark-sql直接读取Doris进行关联并写回Doris,除DDL外,只需要一条SQL即可搞定。

详细过程

  1. 下载预编译的spark和spark-doris-connector
  1. 部署

    将上述安装文件上传至Hadoop集群的其中一台机器,放置到任意目录,比如/opt,请保证使用的用户可以向Yarn提交任务。

    解压缩spark-3.4.3-bin-hadoop3.tgz得到spark-3.4.3-bin-hadoop3目录

    将spark-doris-connector-3.4_2.12-1.3.2.jar放到spark-3.4.3-bin-hadoop3/jars/

  2. 启动spark-sql

    bin/spark-sql --master yarn --num-executors 40 --executor-memory 7G --name Spark-SQL:Doris

  3. 创建映射表

sql 复制代码
-- 用于读取A表数据
CREATE
TEMPORARY VIEW spark_doris_a
USING doris
OPTIONS(
  "table.identifier"="mydb.table_a",
  "fenodes"="10.*.*.1:9030,10.*.*.2:9030,10.*.*.3:9030",
  "user"="root",
  "password"="$YOUR_DORIS_PASSWORD"
);
-- 用于读取B表数据
CREATE
TEMPORARY VIEW spark_doris_b
USING doris
OPTIONS(
  "table.identifier"="mydb.table_b",
  "fenodes"="10.*.*.1:9030,10.*.*.2:9030,10.*.*.3:9030",
  "user"="root",
  "password"="$YOUR_DORIS_PASSWORD"
);
-- 用于向A表部分列写入数据
CREATE
TEMPORARY VIEW spark_doris_a_sink
USING doris
OPTIONS(
  "table.identifier"="mydb.table_a",
  "fenodes"="10.*.*.1:9030,10.*.*.2:9030,10.*.*.3:9030",
  "user"="root",
  "password"="$YOUR_DORIS_PASSWORD",
  "sink.properties.partial_columns"="true",
  "sink.properties.column"="'column_a','column_b','column_c'"
);
  1. DML语句
sql 复制代码
set enable_unique_key_partial_update=true;
set enable_insert_strict=false;

insert into spark_doris_a_sink
(column_a,column_b,column_c)
select a.column_a,b.column_b,b.column_c from spark_doris_a a left join spark_doris_b b on a.colum_d = b.column_e;

遇到的问题

处理过程很简单,但是实际也是遇到了很多问题

  1. DML语句中不能使用分区字段进行过滤,因为Doris 2.0版本在提供的获取执行计划的API中对于引号的处理存在问题,如果传递的是"2024-06-12",则会得到数值2006,该数值无法转换为日期,如果传递"20240612"也无法得到Date("20240612")。理论上,如果调整此处的写法增加嵌套的引号配合转义字符也能实现功能,Java程序员都懂的,有兴趣可以自行验证。
  2. 数据动态变化,由于这两张表的数据都在不断变化,如果读取文件耗时较久,将可能出现tablet对应版本不存在的异常。我的做法是另建表,将数据写入新表,然后再执行关联。

总结

  1. spark executor 内存和并行度设置,这个需要不断调整,我也是尝试了多次,才得到这个可以运行的结果。并行度设置太高了,将会对Doris形成较大的网络和IO冲击,一定要慎重。并行度低了,内存就要高一点,不然数据都已经从Doris读取出来了,关联的时候会内存溢出。
  2. 读取Doris的速率还是很快的,而且spark在读取doris前获取了执行计划,对数据进行了行和列的裁剪,不会将整表数据都读出来。
  3. 耗时,耗时主要在数据读取和数据写入,数据读取是直接访问BE读取tablet,数据写入不是文件级别的,所以写入效率并不高。