一次Spark SQL提取数据所遇到的问题

需求背景及解决思路

boss给了一份csv文件,要求从数仓中的点位表中筛选出csv文件中点位所对应的uuid信息;
需求很简单,用spark读取csv文件,然后join数仓的点位表即可,伪代码如下:
(csv文件2M,数仓中的点位表100亿数据)

...
 val frame = sparkSession.read.csv(localpath)
 frame.repartition(1).createOrReplaceTempView("t1")
 ...
 sparkSession.sql(
        s"""
           |select * from
           |(select * from t1) t
           |join
           |(select * from table_test) p
           |on t.id = p.id
         """.stripMargin
      )

问题Ⅰ:csv编码问题

当我拿到csv文件时,就直接读取了,可是会报如下错误(摘取部分):

WARN scheduler.TaskSetManager: Lost task 0.0 in stage 0.0 :
java.lang.ArrayIndexOutOfBoundsException: 62
at org.apache.spark.unsafe.types.UTF8String.numBytesForFirstByte(UTF8String.java:191)
at org.apache.spark.unsafe.types.UTF8String.numChars(UTF8String.java:206)
at org.apache.spark.sql.catalyst.expressions.
Caused by: java.lang.ArrayIndexOutOfBoundsException

我们可以看到数组下标越界,这是因为csv里有中文列,而这些中文编码格式有问题导致的;
我这里用的是notepad++打开csv文件,选择utf8编码格式,然后另存为一个文件,这样就能解决编码问题。

问题Ⅱ:shuffle前后的分区问题

首先我们这个需求中要有几个操作:
stage1.读csv文件生成frame,然后注册内存表tmp
stage2.读内存表tmp
stage3.读数仓中的表T1
stage4.tmp和T1进行join

然后我们再来看任务提交后每个操作的分区情况:
(这里因为当时跑任务时没有截图,所以用别的任务的截图来说明,分区是一样的)
stage1.读csv文件的分区
在这里插入图片描述
stage2.读内存表tmp的分区
在这里插入图片描述
stage3.读数仓中表的分区
在这里插入图片描述
stage4.进行join的分区
在这里插入图片描述
当时我在代码里设置了一个参数:spark.default.parallelism=800
就是因为这个参数让我对stage3,stage4的分区产生很大的疑惑,为什么我这个参数没起作用?按理说stage3,stage4的分区应该是800才对啊?

其实:
spark.default.parallelism这个参数只有在处理RDD时才会起作用,对Spark SQL的无效;
所以stage3,stage4的分区才没有改变。但是他们的分区又是由谁决定的呢?该怎么更改呢?

这里我们要注意到

stage3时从hive中读取的,这里的分区数其实是取决于你hdfs上的文件数的,例如你这个表有两个分区,每个分区有10个文件,那么你这个步骤的task就是20;这个没办法用参数更改,只能控制写到hive中的文件数量来达到控制;

而stage4是进行join的分区,我们要了解到一个参数spark.sql.shuffle.partitions 当join或者聚合产生shuffle操作时, partitions的数量,这个参数是对sparks SQL专用的设置,默认值就是200;

所以到这里我们知道了每个阶段为什么有那么多分区以及如何更改分区了。

问题Ⅲ:broadcast join不生效

我们都知道,sparksql里有一个优化,就是当一个小表join一个大表的时候,spark会把小表广播到各个节点,这样就能避免shuffle。
我这个需求就是一个典型的小表join大表,所以当我满怀信心的运行之后,没过多久就收来了这样的错误:(简单理解就是shuffle时数据太大了)

FetchFailed(BlockManagerId(149, hadoop848.bx.com, 11681, None),
shuffleId=135, mapId=12, reduceId=154, message=
org.apache.spark.shuffle.FetchFailedException: Failed to connect to
hadoop848.bx.com/10.88.69.188:11681 at
org.apache.spark.storage.ShuffleBlockFetcherIterator.throwFetchFailedException(ShuffleBlockFetcherIterator.scala:513)…

可我明明是小表join大表,应该自动用的是BroadcastJoin啊,为什么还会有shuffle呢?
问题的原因就在于读csv的表是不能进行BroadcastJoin的。

官网的原话是这样的:
The BROADCAST hint guides Spark to broadcast each specified table when joining them with another table or view. When Spark deciding the join methods, the broadcast hash join (i.e., BHJ) is preferred, even if the statistics is above the configuration spark.sql.autoBroadcastJoinThreshold. When both sides of a join are specified, Spark broadcasts the one having the lower statistics. Note Spark does not guarantee BHJ is always chosen, since not all cases (e.g. full outer join) support BHJ. When the broadcast nested loop join is selected, we still respect the hint.
注意: 确定broadcast hash join的决定性因素是hive的表统计信息一定要准确。并且,由于视图是没有表统计信息的,所以所有的视图在join时都不会被广播。所以至少要有一张hive表。

就是说在spark中size的估算表示为statistics类,仅对hive relation 有效,因为其最初是从hive元数据库中读取所需的统计值的.因此对于jdbc relation等来说,无法触发broadcast join

所以我们把csv的数据先写到了hive,然后从hive读,发现已经变成了BroadcastJoin,任务完美运行。

问题Ⅳ:sparksql关于stage的划分

这个并不算一个影响任务的问题,是当时我在找为什么不进行BroadcastJoin时困惑我的一个地方,我总感觉web ui 上的stage划分乱乱的,于是就梳理了一下,弄清楚这个可以更方便我们找到问题所在,
但是这个问题要配合实例说明,我当时没有截图,这里就偷懒借用大佬的文章来说明了(因为大佬文章需要付费,所以我这里就复制粘贴了,方便没有付费的同学查看)

以下是复制粘贴的大佬文章:

问题是这样的,从oracle中读取一个表,然后对某个字段进行重分区,再从oracle中读取另外一张表,同样也对某一个字段进行重分区,最后进行两个df的join操作,然后提交,他对ui上面显示的stage有点疑惑,其实这是一个很简单的逻辑处理,我尽量模仿他的代码逻辑,我是从mysql中读取的数据.下面先看下代码:

package spark
 
import java.io.File
import org.apache.spark.sql.{DataFrame, SparkSession}
 
object sparkMysql {
  def main(args: Array[String]): Unit = {
    val warehouseLocation = new File("hdfs://cluster/hive/warehouse").getAbsolutePath
    val spark = SparkSession
      .builder()
      .appName("Spark SQL basic example").config("spark.sql.warehouse.dir", warehouseLocation).enableHiveSupport().getOrCreate()
    /* .master("local[4]")
     .appName("Spark SQL basic example")
     .getOrCreate()*/
    //代表了shuffle read task的并行度,该值默认是200
    //spark.conf.set("spark.sql.shuffle.partitions","100")
    spark.conf.set("spark.sql.autoBroadcastJoinThreshold","1")
    val frame = getDataFromMysql("test",spark)
    //val v_1 = frame.repartition(frame("price"))
    val frame_2 = getDataFromMysql("TabName",spark)
    //val v_2 = frame_2.repartition(frame_2("Name"))
    val j = frame.join(frame_2,Seq("id"),"left")
    println(j.count())
    Thread.sleep(100000)
  }
  def getDataFromMysql(tableName:String,spark:SparkSession):DataFrame = {
    spark.read.format("jdbc")
      .option("url", "jdbc:mysql://****?autoReconnect=true&characterEncoding=utf-8&allowMultiQueries=true")
      .option("user", "***")
      .option("password", "****")
      .option("dbtable", tableName)
      .option("driver", "com.mysql.jdbc.Driver")
      .load()
  }
}

代码很简单就不在说了,从上面可以看到只有一个action算子count,所以这段代码就只会启动一个job,下面看ui截图.
在这里插入图片描述

从上面这张图可以看到这个job分为4个stage,一共有203个task,接下来详细介绍为什么会是这么多,点击stage进去后,看下面图:

在这里插入图片描述
先看DAG图,可以看到这个job有4个stage,然后看下面的每一个stage,我们发现stage0,stage1是在同一时间启动的,他们两个分别指的是读取mysql数据的两张表,这两个stage是并行的,他们的分区都是1,用时3s,然后第三个stage是在11秒的时候启动的,这个是串行的,对应代码是join操作,分区为200,为什么是200?是有这个参数控制的spark.sql.shuffle.partitions,代表了shuffle read task的并行度,该值默认是200,我们可以在代码里面设置这个并发度,代码里面有注释,最后一个stage是join后的操作并发度为1,所有哟共就有1+1+200+1个task,这就是为什么有203个task的原因,因为join操作有shuffle产生,所以会划分为stage3和stage4.说到这儿相信大家就非常清楚了.

也能说明一个问题:
1.Stage 可以并行执行的
2.存在依赖的Stage 必须在依赖的Stage执行完成后才能执行下一个Stage
3.Stage的并行度取决于资源数

原文地址:
https://blog.csdn.net/xianpanjia4616/article/details/84796858

另外关于BroadcastJoin的分析可以看:
https://blog.csdn.net/dabokele/article/details/65963401

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值