前言

最近 以为同事在调试 类似如下代码片段的时候 使用了 mapPartitionsWithIndex, 来进行输出上下文信息的调试  

业务代码中 一系列的 transformations 的处理之后, 使用了 sortByKey + take 进行 "分页" 处理 

然后 她在 这一系列的 transformations 操作中间增加了一系列的 mapPartitionsWithIndex 来进行输出上下文的信息, 进行调试  

但是 出乎他的意料的是 有一部分的 mapPartitionsWithIndex 竟然被打印了多次, 但是 有一部分又是正常的 

 

出现问题的代码如下, 导致出现业务数据错误的代码, 也能一眼就能看出是在 reduceByKey 的 func 存在问题  

package com.hx.test

import org.apache.spark.{SparkConf, SparkContext}

/**
  * Test01WordCount
  *
  * @version 1.0
  * @date 2020-04-12 11:00
  */
object Test01WordCount {

    // Test01WordCount
    def main(args: Array[String]): Unit = {
    
        val logFile = "resources/Test01WordCount.txt"
        val conf = new SparkConf().setAppName("Test01WordCount").setMaster("local[1]")
        val sc = new SparkContext(conf)
        
        val result = sc.textFile(logFile, 2)
        .flatMap(line => line.split(" "))
        .map(word => (word, (1, BigDecimal(10))))
        .reduceByKey((v1, v2) => (v1._1 + 1, v1._2 + v2._2))
        
        result.collect()
        .foreach(entry => println(entry._1, entry._2))
        
        System.in.read()
        sc.stop()
    
    }

}

 

以下调试基于 jdk1.8 + spark2.4.5 

 

 

测试代码

呵呵 当时看到这个, 我也感觉有些不可思议, 呵呵 只能之后复现一下, 调试一下了, 然后 我本地写了一份测试代码 大致如下 

package com.hx

import org.apache.spark.{SparkConf, SparkContext, TaskContext}

/**
  * Test22SortByThenTake
  *
  * @author Jerry.X.He
  * @version 1.0
  * @date 2020-09-27 14:43
  */
object Test22SortByThenTake {

  // Test22SortByThenTake
  def main(args: Array[String]): Unit = {

    val logFile = "resources/Test01WordCount.txt"
    val conf = new SparkConf().setAppName("Test22SortByThenTake").setMaster("local[1]")
    val sc = new SparkContext(conf)

    val result = sc.textFile(logFile, 10)
      .map(line => {
        var partitionId = TaskContext.getPartitionId()
        println(s" partitionId : $partitionId, line : $line ")
        line
      })
      .flatMap(line => line.split(" "))
      .map(word => (word, 1))
      .sortBy(entry => entry._1, true, 10)
      .take(10)

    result
      .foreach(entry => println(entry))

    System.in.read()
    sc.stop()

  }

}

然后 随便找了一个 测试文件来看, 结果 以上的 map(printFunc) 代码片段 果然是被执行了两次  

.map(word => {
        println(word)
        word
      })

 

然后 跟踪了一部分的代码之后, 构造了一下 在这种场景下面比较极端的情况, 构造了三个 runJob, 也就是 map(printFunc) 被执行了三次 

"resources/Test01WordCount.txt" 的数据如下 

001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100
123 124

 

输出日志信息如下 

看这里 也很奇怪, partition 0, 9 先执行了一次 map(printFunc) 之后, paritition 0 单独执行了一次 map(printFunc), 然后 partition 0, 9 又执行了一次 map(printFunc) 

呵呵 这个就有点奇怪了 

partitionId : 0, line : 001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 
 partitionId : 9, line : 123 124 
 partitionId : 0, line : 001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 
 partitionId : 0, line : 001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 
 partitionId : 9, line : 123 124 
(001,1)
(002,1)
(003,1)
(004,1)
(005,1)
(006,1)
(007,1)
(008,1)
(009,1)
(010,1)

 

来看一下 driver 这边的统计信息 

三次 runJob, 前两次是 sortBy 触发的, 之后一次是 take 触发的 

07 sortBy + take 操作之前的 Transformations 被执行多次_spark

 

 

sortBy 的处理方式 

07 sortBy + take 操作之前的 Transformations 被执行多次_transformations_02

这里 可以看到 sortBy 的具体的实现思路, 三步处理 

1. 将当前 RDD[T] 映射为 RDD[(K, T)] 

2. 当前 RDD[(K, T)] 进行根据 _._1 进行排序 

3. 然后在获取排序之后的 RDD[(K, T)] 的 _._2 就是根据 f(T) 排好序之后的 RDD[T] 

 

步骤1 

07 sortBy + take 操作之前的 Transformations 被执行多次_spark_03

 

步骤2

07 sortBy + take 操作之前的 Transformations 被执行多次_spark_04

 

步骤3 

07 sortBy + take 操作之前的 Transformations 被执行多次_数据_05

 

 

take 的处理方式

07 sortBy + take 操作之前的 Transformations 被执行多次_数据_06

第一次迭代会 迭代一个 partition 

然后 后面会根据特定的规则 来迭代之后的 N 个 partition 

迭代直到 获取到期望的元素 或者 所有的 partition 迭代完毕 

这里 可以看出 take 时可能进行多次 runJob(处理的不同的 partition)

 

 

map(printFunc) 的第一次执行

第一次 runJob, 从堆栈信息中 可以看到是在 创建 RangePartitioner 的时候, 采样 rdd 中的数据的时候, mapPartitionsWithIndex 之后的 collect 操作触发了一次 action 

07 sortBy + take 操作之前的 Transformations 被执行多次_数据_07

07 sortBy + take 操作之前的 Transformations 被执行多次_迭代_08

这里的采样的策略是 如果元素数量小于 n 返回所有的元素, 迭代前面的n个元素放到结果数组中, 然后 后面的元素idx随机计算一次, 如果落在 [0,n), 则替换掉结果集中的元素, 最后 返回结果集 

最后返回的是 当前partition的元素数量 + 取样的元素列表 

 

 

map(printFunc) 的第二次执行

07 sortBy + take 操作之前的 Transformations 被执行多次_transformations_09

从上图 可以看出 这次 runJob 是由于 存在分区数据不"平衡" 触发的一次 runJob 

提取的数据是 从这些不平衡的分区中以 fraction 作为因素抽样元素, 然后 作为之后的计算的参照 

可以看到这里 需要计算的 partition 只有一个(partition0)

 

 

map(printFunc) 的第三次执行

07 sortBy + take 操作之前的 Transformations 被执行多次_数据_10

take 触发的 action, 然后 依赖于前面的 HadoopRDD 以及后面的一系列的 Transformations 产生的 RDD 

 

 

driver 端的 SparkUI

07 sortBy + take 操作之前的 Transformations 被执行多次_transformations_11

通过上面的剖析, 我们应该知道 job0 对应于 创建 RangePartitioner 的时候抽样各个 partition 的时候触发的 runJob 

job1 对应于 partition0 数据不平衡, 然后单独 抽样的 partition0 

job2 对应于 take 触发的 action 

 

job0, job1 执行的时候 RDD 链里面都没有 ShuffleDependecy 关系的 RDD 

job2 的时候 sortBy 会创建一个 ShuffleRDD, 之前之后都不是 ShuffleDependecy 

因此 job0, job1 应该是只有一个 ResultStage, job2 会对应一个 ShuffleMapStage, 一个 ResultStage 

 

关于 job0 

07 sortBy + take 操作之前的 Transformations 被执行多次_数据_12

sortBy 中的是三个节点分别为 keyBy[K](f) "RDD.sortBy" 创建的节点 

RangePartitioner.sketch(rdd.map(_._1), sampleSizePerPartition) "Partitioner.rangeBounds" 创建的节点 

rdd.mapPartitionsWithIndex { (idx, iter) "Partitioner.sketch" 创建的节点 

 

另外一点是 为什么 task 创建了 11 个呢? 

我们来看一下 HadoopRDD 里面我们这里场景使用的 TextInputFormat, 407 字节, numSplits 为 10, 估算每一个 partition 40字节, 综合计算 最后是 11 个 partition, 所以 后面 DagScheduler 生成 Task 的时候生成的是 11 个 task 

07 sortBy + take 操作之前的 Transformations 被执行多次_迭代_13

 

关于 job1 

07 sortBy + take 操作之前的 Transformations 被执行多次_迭代_14

这里只计算 partition0, 只生成了一个 Task 

 

关于 job2

07 sortBy + take 操作之前的 Transformations 被执行多次_transformations_15

take 触发的 action 

sortBy 创建的 ShuffleRDD 之前的操作, 11 个 partition, 执行计算 

然后 之后 take 的时候, 需要 10 个元素, shuffle 之后再 partition0 里面都能获取到, 因此 只提交了一次 runJob 

 

shuffle 之后的 ShuflleRDD 之后的各个 partition 的数据如下 

0, (001,(001,1))
0, (002,(002,1))
0, (003,(003,1))
0, (004,(004,1))
0, (005,(005,1))
0, (006,(006,1))
0, (007,(007,1))
0, (008,(008,1))
0, (009,(009,1))
0, (010,(010,1))
0, (011,(011,1))

1, (012,(012,1))
1, (013,(013,1))
1, (014,(014,1))
1, (015,(015,1))
1, (016,(016,1))
1, (017,(017,1))
1, (018,(018,1))
1, (019,(019,1))
1, (020,(020,1))
1, (021,(021,1))

2, (022,(022,1))
2, (023,(023,1))
2, (024,(024,1))
2, (025,(025,1))
2, (026,(026,1))
2, (027,(027,1))
2, (028,(028,1))
2, (029,(029,1))
2, (030,(030,1))

2, (031,(031,1))
3, (032,(032,1))
3, (033,(033,1))
3, (034,(034,1))
3, (035,(035,1))
3, (036,(036,1))
3, (037,(037,1))
3, (038,(038,1))
3, (039,(039,1))
3, (040,(040,1))
3, (041,(041,1))

4, (042,(042,1))
4, (043,(043,1))
4, (044,(044,1))
4, (045,(045,1))
4, (046,(046,1))
4, (047,(047,1))
4, (048,(048,1))
4, (049,(049,1))
4, (050,(050,1))
4, (051,(051,1))

5, (052,(052,1))
5, (053,(053,1))
5, (054,(054,1))
5, (055,(055,1))
5, (056,(056,1))
5, (057,(057,1))
5, (058,(058,1))
5, (059,(059,1))
5, (060,(060,1))
5, (061,(061,1))
5, (062,(062,1))

6, (063,(063,1))
6, (064,(064,1))
6, (065,(065,1))
6, (066,(066,1))
6, (067,(067,1))
6, (068,(068,1))
6, (069,(069,1))
6, (070,(070,1))
6, (071,(071,1))
6, (072,(072,1))

7, (073,(073,1))
7, (074,(074,1))
7, (075,(075,1))
7, (076,(076,1))
7, (077,(077,1))
7, (078,(078,1))
7, (079,(079,1))
7, (080,(080,1))
7, (081,(081,1))
7, (082,(082,1))

8, (083,(083,1))
8, (084,(084,1))
8, (085,(085,1))
8, (086,(086,1))
8, (087,(087,1))
8, (088,(088,1))
8, (089,(089,1))
8, (090,(090,1))
8, (091,(091,1))
8, (092,(092,1))

9, (093,(093,1))
9, (094,(094,1))
9, (095,(095,1))
9, (096,(096,1))
9, (097,(097,1))
9, (098,(098,1))
9, (099,(099,1))
9, (100,(100,1))
9, (123,(123,1))
9, (124,(124,1))

 

然后我们修改一下 take 的元素的数量, take(20) 

07 sortBy + take 操作之前的 Transformations 被执行多次_数据_16

从这里可以看出 从 partition0 中 take 数据, 取了 11 个 

然后 需要获取 20 个元素, 继续下一轮迭代, numPartsToTry 计算为 2(job3创建了2个task)

然后 迭代 partition1, partition2 每个 partition 获取 9 个元素 

之后吧 partition1 中获取的 9 个元素 和 partition0 中获取的 11 个元素 merge 在一起 返回 

 

 

完