データスキュースパークのチューニング

データ・スキューとは何ですか?

次のように計算スパーク抽象

抽象.PNGを計算スパーク

データスキューを指す:ボトルネック工程の全データセットの処理速度の部分となるよう集中並列処理、データのサブセット(例えば、スパークカフカまたはパーティション)、他の部分よりも有意に高いです。

データは傾きを解決できない場合は、他の最適化手法のガードはショートボードの効果として、再び何も良いではない、タスクの効率性を確認しない最速のタスクを完了したが、1最も遅いれます。

帰結をダンプデータ:
  1. 直接データスキューは、状況につながる可能性があります。メモリまたはGCタイムアウトのうち
  2. タスクは必ずしも失敗しますが、非常に遅いではありません。(しかし、今、私はデータがほとんど失敗したスキュー遭遇)

データ概略斜視

image.png.png

図に示すように、
データ処理の大量にわたって個々 ShuffleMapTask2(データの980億個)。全ジョブの実行時間を遅くにつながります。
これは、タスクOOMマシンが配置されているにつながる、または非常にゆっくりと実行可能性があります。

原則傾け:

シャッフルを行う際に、同一の鍵は、このようなジョイン操作のためのキーに従って状重合又はように、処理すべきタスクのノードに各ノードに引かなければなりません。この時点で、データは、キーに対応する場合、特に、データスキューが生じます。

;例えば、個々のキーの数百に対応するデータのキー部分のほとんどが、数千人が、データの対応する部分を持って、そして最も可能性が高いそれは上で動作するため、少量のデータに割り当てられたタスクのみ、その後、1秒意志
かもしれませんが、個々のタスク12時間を実行するために、大量のデータに割り当てられています。
したがって、スパークジョブ実行の進捗状況は、そのタスクの決定により、最長実行時間です。
注:ステージタスク内のすべて同じに同じ計算を実行するので、差、この決定によって処理されるデータの異なる時間のかかるタスクタスク主要量の間の差を計算する別のコンピューティングノードの前提の排除。

傾斜位置を測位

オペレータが引き起こす可能性

オペレータは、(不完全な)トリガすることができます

タスクメモリオーバーフロー

这种情况下去定位出问题的代码(触发JOb的Action位)就比较容易了。

可以直接看 yarn-client 模式下本地 log 的异常栈,或者是通过 YARN 查看 yarn-cluster 模式下的 log 中的异常栈。

一般来说,通过异常栈信息就可以定位到你的代码中哪一行(触发JOb的Action位置)发生了内存溢出和溢出的Stage是哪一个。然后在那行代码附近找找,一般也会有 shuffle类算子,此时很可能就是这个算子导致了数据倾斜,但是是经工作中发现,这个定位具体行数还是比较困难,因为日志只会出现触发JOb的Action算子的代码行数,而一个Job可能有多可shuffle阶段,你要很了解任务的划分才有可能找对位置。

要注意的是,出现内存溢出不一定就是倾斜。这只是一种可能而已。

task 执行特别慢的情况

与上面类似,虽然不报错,但是程序就在这里停住了,某部分task一直没有完成。

为了进一步确定是否倾斜,最好的办法是去看web ui,查看当前task 所在Stage的所有task,看看执行特别慢的task 运行时间、所处理的数据量、GC等信息。

如果与其他task差异较大,说明出现了倾斜问题,那我们接下来就该去解决问题了。

key 的数据分布情况

我工作中因为权限、环境等各种问题,无法查看Web UI 所以对于定位GC、OOM的问题特别难受~~~。

所有有时候采用很笨的方法来确定一下是否数据倾斜

上述表格中列举了可能出现倾斜的算子,那么这些我们可以抽样统计一下该算子操作的key对应的数据量。如果key 的分布及不均匀,某种程度上也可以判定是出现了倾斜

df(dataFrame) 部分数据如下
+--------+-----------+------------+------+--------+----+
|  userid|  zubo_nums|  total_nums|  nums|     day|hour|
+--------+-----------+------------+------+--------+----+
| userid1| zubo_nums1| total_nums1| nums1|20190101|  00|
| userid2| zubo_nums2| total_nums2| nums2|20190101|  00|
| userid3| zubo_nums3| total_nums3| nums3|20190101|  00|
| userid4| zubo_nums4| total_nums4| nums4|20190101|  00|
| userid5| zubo_nums5| total_nums5| nums5|20190101|  00|
| userid6| zubo_nums6| total_nums6| nums6|20190101|  00|
| userid7| zubo_nums7| total_nums7| nums7|20190101|  00|
| userid8| zubo_nums8| total_nums8| nums8|20190101|  00|
| userid9| zubo_nums9| total_nums9| nums9|20190101|  00|
|userid10|zubo_nums10|total_nums10|nums10|20190101|  00|
+--------+-----------+------------+------+--------+----+
logger.info("\n df count=" +df.count())
df.sample(false,0.1).rdd.keyBy(row=>row.getAs("userid").toString).countByKey().foreach(println _)
df count=2058

多次抽样对比
(userid88,3)
(userid99,1)
(userid61,2)
(userid50,2)
(userid34,2)
(userid1,33)
(userid39,4)
(userid83,3)
--------------------
(userid61,1)
(userid50,1)
(userid34,1)
(userid1,35)
(userid83,2)
(userid17,1)
(userid69,2)
---------
(userid99,2)
(userid61,1)
(userid50,2)
(userid34,2)
(userid1,25)
(userid39,1)
(userid83,1)
(userid94,2)
(userid17,1)

从上述抽样结果接可以看出,userid1这个key数量明显多余其他key。
多次抽样也可以看出,这样统计一定程度上可以反应倾斜的问题并且可以确定倾斜的key,这样对于我们后续解决倾斜问题有一定的帮助。


解决数据倾斜

从源端数据解决

下面距两个例子说明:

kafka数据源
我们一般通过 DirectStream 方式读取 Kafka数据。

由于 Kafka 的每一个 Partition 对应 Spark 的一个Task(Partition),所以 Kafka 内相关 Topic 的各Partition 之间数据是否平衡,直接决定 Spark处理该数据时是否会产生数据倾斜。

Kafka 某一 Topic 内消息在不同 Partition之间的分布,主要由 Producer 端所使用的Partition 实现类决定。

如果使用随机 Partitioner,则每条消息会随机发送到一个 Partition 中,从而从概率上来讲,各Partition间的数据会达到平衡。此时源 Stage(直接读取 Kafka 数据的 Stage)不会产生数据倾斜。

所以如果业务没有特别需求,我们可以在Producer端的 Partitioner 采用随机的方式,并且可以每个批次数据量适当增加 Partition 的数量,达到增加task目的。

但是很多业务要求将具备同一特征的数据顺序消费,此时就需要将具有相同特征的数据放于同一个 Partition 中。比如某个地市、区域的数据需要放在一个Partition 中,如果此时出现了数据倾斜,就只能采用其他的办法解决了。

hive数据源
如果数据源是来自hive,那么我们可以考虑在hive端就针对该key一次etl处理。

如果评估可行,那我们在Spark就可以在Spark端使用etl后的数据了,也就不用Spark中执行之前倾斜的部分的逻辑了。

优点:实现起来简单便捷,效果不错,完全规避掉了数据倾斜,Spark 作业的性能会大幅度提升。

缺点:治标不治本,我们只是把数据倾斜提前到了hive端,Hive ETL 中还是会发生数据倾斜,所以我们还是避免不了要在hive端处理倾斜问题。

适用情况:
因为本质上没有解决数据倾斜的问题,我们只有解决了Hive端数据倾斜的问题才算真正的解决这个问题。
所以当hive端的数据仅仅被调用一两次的时候,这样做性能提升不大;
但是当频繁的调用相关数据的时候,如果在Spark调用Hive etl后的数据是就不会出现数据倾斜的问题,这样性能的提升就非常可观了


调整并行度

原理:调整并行度,分散同一个 Task 的不同 Key 到更多的Task

注意:调整并行度不一定是增加,也可能是减少,目的是为了,分散同一个 Task 中倾斜 Key 到更多的Task,所以如果减少并行度可以实现,也是可以的

对于Spark Sql配置下列参数spark.sql.shuffle.partitions

对于RDD,可以对shuflle算子设置并行度,如

 rdd.map(p=>(p._1,1)).reduceByKey( (c1, c2)=>(c1+c2),1000)
 
 默认使用HashPartitioner,并行度默认为 spark.default.parallelism
 def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)] = self.withScope {
    reduceByKey(new HashPartitioner(numPartitions), func)
  }

优点:实现起来比较简单,理论上可以有效缓解和减轻数据倾斜的影响。

方案缺点:只是缓解了数据倾斜而已,没有彻底根除问题,对于某个key倾斜的情况毫无办法,因为无论你设置并行度为多少,相同的key总会在同一个partition中

一般如果出现数据倾斜,都可以通过这种方法先试验几次,如果问题未解决,再尝试其它方法。

适用场景少,只能将分配到同一 Task 的不同 Key 分散开,但对于同一 Key 倾斜严重的情况该方法并不适用。
并且该方法一般只能缓解数据倾斜,没有彻底消除问题。

根据我工作遇到倾斜问题的来看,这方法有一定效果但是作用不大,还没试过只调整并行度就直接解决的案例。


自定义分区函数

原理:使用自定义的 Partitioner(默认为 HashPartitioner),将原本被分配到同一个 Task 的不同 Key 分配到不同 Task。

class CustomerPartitioner(numParts: Int) extends Partitioner{
  override def numPartitions: Int = numParts

  override def getPartition(key: Any): Int = {
    //自定义分区
    val id: Int = key.toString.toInt
    //这里自定义分区的方式比较灵活,可以根据key的分布设计不同的计算方式
    if (id <= 10000) //10000 以内的id容易出现倾斜
      return new java.util.Random().nextInt(10000) % numPartitions
    else
      return id % numPartitions
  }
}
rdd.map(p=>(p._1,1)).groupByKey(new CustomerPartitioner(10))

适用场景:大量不同的 Key 被分配到了相同的 Task 造成该 Task 数据量过大。

优点:不影响原有的并行度设计。如果改变并行度,后续 Stage 的并行度也会默认改变,可能会影响后续 Stage。

缺点:适用场景有限,只能将不同 Key 分散开,对于同一 Key 对应数据集非常大的场景不适用。
效果与调整并行度类似,只能缓解数据倾斜而不能完全消除数据倾斜。
而且不够灵活,需要根据数据特点自定义专用的 Partitioner(即需要非常了解key的分分布)。


ReduceJoin转MapJoin(Broadcast )

原理:如果一个 RDD 是比较小的,则可以采用广播小 RDD 全量数据 +map 算子来实现与 join 同样的效果,也就是 map join,此时就不会发生 shuffle 操作,也就不会发生数据倾斜。

示意图

ReduceJoin转MapJoin.png

优点:对 join 操作导致的数据倾斜,效果非常好,因为根本就不会发生 shuffle,也就根本不会发生数据倾斜。

缺点:要求参与 Join的一侧数据集足够小,并且主要适用于 Join 的场景,不适合聚合的场景,适用条件有限。

通过 Spark 的 Broadcast 机制,将 Reduce 侧 Join 转化为 Map 侧 Join,避免 Shuffle 从而完全消除 Shuffle 带来的数据倾斜。

Web UI的DAG图如下

ReduceJoin.png

MapJoin
MapJoin.png

相关参数:

将 Broadcast 的阈值设置得足够大
SET spark.sql.autoBroadcastJoinThreshold=10485760

局部聚合+全局聚合

原理:将原本相同的 key 通过附加随机前缀的方式,变成多个不同的 key,就可以让原本被一个 task 处理的数据分散到多个 task 上去做局部聚合,进而解决单个 task 处理数据量过多的问题。接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果。

 rdd1
      .map(s=>(new Random().nextInt(100)+"_"+s._1,s._2))//添加前缀
      .reduceByKey(_+_)//局部聚合
      .map(s=>(s._1.split("_")(1),s._2))//去除前缀
      .reduceByKey(_+_)//全局聚合

+部分重合グローバル重合.PNG

适用场景:对 RDD 执行 reduceByKey 等聚合类 shuffle 算子或者在 Spark SQL 中使用 group by 语句进行分组聚合时,比较适用这种方案。

优点:对于聚合类的 shuffle 操作导致的数据倾斜,效果是非常不错的。通常都可以解决掉数据倾斜,或者至少是大幅度缓解数据倾斜,将 Spark 作业的性能提升数倍以上。

缺点:仅仅适用于聚合类的 shuffle 操作,适用范围相对较窄。如果是 join 类的 shuffle 操作,还得用其他的解决方案。


倾斜 key 增加随机前/后缀

实现原理:将倾斜的key 与非倾斜的key 分别与右表join,得到skewedJoinRDD和unskewedJoinRDD最后unoin得到最终结果

skewedJoinRDD部分实现步骤:

  1. 将 rddLeft 中倾斜的 key(即 userid1 与 userid2)对应的数据单独过滤出来,且加上 1 到 n 的随机前缀)形成单独的 left: RDD[(String, Int)]。
  2. 将 rddRight 中倾斜 key 对应的数据抽取出来,并通过 flatMap 操作将该数据集中每条数据均转换为 n 条数据(每条分别加上 1 到 n 的随机前缀),形成单独的 right: RDD[(String, String)]。
  3. 将 left 与 right 进行 Join,并将并行度设置为 n,且在 Join 过程中将随机前缀去掉,得到倾斜数据集的 Join 结果 skewedJoinRDD。

unskewedJoinRDD部分实现步骤:

  1. 将 rddLeft: RDD[(String, Int)] 中不包含倾斜 Key 的数据抽取出来作为单独的 leftUnSkewRDD。
  2. 对 leftUnSkewRDD 与原始的 rddRight: RDD[(String, String)] 进行Join,并行度也设置为 n,得到 Join 结果 unskewedJoinRDD。
  3. 通过 union 算子将 skewedJoinRDD 与 unskewedJoinRDD 进行合并,从而得到完整的 Join 结果集。

实现代码

  def prix(): Unit = {
    val sc = spark.sparkContext
    val rddLeft: RDD[(String, Int)] = srdd.rdd.keyBy(row => row.getAs("userid").toString).map(p => (p._1, 1))
    val rddRight: RDD[(String, String)] = srdd.rdd.keyBy(row => row.getAs("userid").toString).map(p => (p._1, p._2.getAs("nums").toString))
    val skewedKeySet = Set("userid1", "userid2") //倾斜的key

    val addList: Seq[Int] = 1 to 24 //右表前缀

    val skewedKey: Broadcast[Set[String]] = sc.broadcast(skewedKeySet) //广播倾斜key

    val addLisPrix: Broadcast[Seq[Int]] = sc.broadcast(addList) //广播右表前缀

    val left: RDD[(String, Int)] = rddLeft
      .filter(kv => skewedKey.value.contains(kv._1)) //左表筛选倾斜key
      .map(kv => (new Random().nextInt(24) + "," + kv._1, kv._2)) //倾斜key增加前缀

    val leftUnSkewRDD: RDD[(String, Int)] = rddLeft
      .filter(kv => !skewedKey.value.contains(kv._1)) //左表筛选非倾斜key
    val right: RDD[(String, String)] = rddRight
      .filter(kv => skewedKey.value.contains(kv._1)) //右表筛选倾斜key
      .map(kv => (addLisPrix.value.map(str => (str + "," + kv._1, kv._2)))) //右表倾斜key每个增加1 to 24 的前缀
      .flatMap(kv => kv.iterator)


    val skewedJoinRDD: RDD[(String, String)] = left
      .join(right, 100) //关联操作
      .mapPartitions(kv => kv.map(str => (str._1.split(",")(1), str._2._2))) //去除前缀

    val unskewedJoinRDD: RDD[(String, String)] = leftUnSkewRDD
      .join(rddRight, 100) //非倾斜关联操作
      .mapPartitions(kv => kv.map(str => (str._1, str._2._2)))
    
    //合并倾斜与非倾斜key
    skewedJoinRDD.union(unskewedJoinRDD).collect().foreach(println _)
  }

用场景:两张表都比较大,无法使用 Map 侧 Join。其中一个 RDD 有少数几个 Key 的数据量过大,另外一个 RDD 的 Key 分布较为均匀。

長所:参加に関して、地図側、参加は大規模なデータセットに適応することができます。
リソースが十分であれば、非傾斜部分の傾斜部のデータセットのデータセットが大幅に効率を向上させる、並列に実行されてもよいです。
そして、データのみのデータの拡張子は、リソース消費量の限られた上昇の一部を歪曲します。

短所:傾斜キーが非常に大きい場合は、拡張データの他の側面は、非常に大きいこの方式が適用されません。
また、この時、個別に非傾斜斜めキーキーで、あなたはオーバーヘッドが増加、二度設定データをスキャンする必要があります。


n個のランダムなプレフィックスのランダム傾き、小さなテーブルとテーブルをn倍に拡大しました

原理:キーがNにプレフィックス1によって異なる追加のランダム鍵にRDD含む方法傾斜しているが、プロセスの複数の分散タスクにこれらの処理後の「異なる鍵」であることができます。各レコードは、結合、次いで、N個の拡張非傾斜RDDに1で始まります

(この方法の変形形態もあり、キーの傾きは、nランダムプレフィックスから引き出され、小さなテーブルのn倍に拡大、追加、及び非傾斜同様の例で、別々に傾斜)

原則

  def prixAndMul(): Unit = {
    val sc = spark.sparkContext
    val rddLeft: RDD[(String, Int)] = srdd.rdd.keyBy(row => row.getAs("userid").toString).map(p => (p._1, 1))
    val rddRight: RDD[(String, String)] = srdd.rdd.keyBy(row => row.getAs("userid").toString).map(p => (p._1, p._2.getAs("nums").toString))
    val skewedKeySet = Set("userid1", "userid2") //倾斜的key

    val addList: Seq[Int] = 1 to 24 //右表前缀

    val addLisPrix: Broadcast[Seq[Int]] = sc.broadcast(addList) //广播右表前缀

    val left: RDD[(String, Int)] = rddLeft
      .map(kv => (new Random().nextInt(24) + "," + kv._1, kv._2)) //倾斜key增加前缀
    
    val right: RDD[(String, String)] = rddRight
      .map(kv => (addLisPrix.value.map(str => (str + "," + kv._1, kv._2)))) //右表倾斜key每个增加1 to 24 的前缀
      .flatMap(kv => kv.iterator)
    
    val resultRDD: RDD[(String, String)] = left
      .join(right, 100) //关联操作
      .mapPartitions(kv => kv.map(str => (str._1.split(",")(1), str._2._2))) //去除前缀
    
    resultRDD.collect().foreach(println _)
  }
  
  

メリット:基本的なデータは、参加の種類が傾斜しており、その効果は比較的大きな、非常に良好なパフォーマンスで処理することができます。

短所:プログラムが完全にデータスキューを回避するのではなく、データを容易にするために、より傾斜しています。全体RDDの拡大の必要性は、メモリリソースが厳しいです。

プログラムは、少なくとも、次に起動し、再最適化により、結局、実際の状況を見て、プログラムが完了、速度に実行できるようにすることができます。


いくつかの重要な傾斜リードをフィルタリング

データ要件が非常に厳しくないため、鍵は、直接濾過、傾きをサンプリングすることによって得ることができます


データについて柔軟なソリューションのさまざまな方法を使って、データの実際の状況に応じて、ではなく、固定溶液スキュー

おすすめ

転載: www.cnblogs.com/lillcol/p/11246231.html