Spark mapPartitions闭包行为

时间:2017-06-05 14:35:50

标签: apache-spark spark-streaming

这是关于处理情况的一个非常常见的火花相关问题,哪一段代码被执行在哪个火花公园(执行人/司机)。 有了这段代码,我有点惊讶为什么我没有得到我期待的值:

1    stream
2      .foreachRDD((kafkaRdd: RDD[ConsumerRecord[String, String]]) => {
3        val offsetRanges = kafkaRdd.asInstanceOf[HasOffsetRanges].offsetRanges
4        import argonaut.Argonaut.StringToParseWrap
5
6        val rdd: RDD[SimpleData] = kafkaRdd.mapPartitions((records: Iterator[ConsumerRecord[String, String]]) => {
7          val invalidCount: AtomicLong = new AtomicLong(0)
8          val convertedData: Iterator[SimpleData] = records.map(record => {
9            val maybeData: Option[SimpleData] = record.value().decodeOption[SimpleData]
10           if (maybeData.isEmpty) {
11             logger.error("Cannot parse data from kafka: " + record.value())
12             invalidCount.incrementAndGet()
13           }
14           maybeData
15         })
16           .filter(_.isDefined)
17           .map(_.get)
18
19         val statsDClient = new NonBlockingStatsDClient("appName", "monitoring.host", 8125) // I know it should be a singleton :)
20         statsDClient.gauge("invalid-input-records", invalidCount.get())
21
22         convertedData
23       })
24
25       rdd.collect().length
26       stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
27     })

想法:从具有无效格式的kafka报告编号条目中获取JSON数据(如果有)。 我假设当我使用 mapPartitions 时,我将为每个分区执行内部代码。即我希望第7-22行将被包装/关闭-d并发送给执行程序执行。在这种情况下,我期待

  

invalidData

变量将在执行程序的执行范围内,并且如果在json->对象转换期间发生错误(第10-13行),则会更新。因为内部没有RDD或其他东西的概念 - 在常规条目上只有常规的scala迭代器。 在第19-20行中,statsd客户端发送到度量服务器 invalidData 值。 显然我总是得到'0'

但是,如果我将代码更改为:

1     stream
2       .foreachRDD((kafkaRdd: RDD[ConsumerRecord[String, String]]) => {
3         val offsetRanges = kafkaRdd.asInstanceOf[HasOffsetRanges].offsetRanges
4
5         // this is ugly we have to repeat it - but argonaut is NOT serializable...
6         val rdd: RDD[SimpleData] = kafkaRdd.mapPartitions((records: Iterator[ConsumerRecord[String, String]]) => {
7           import argonaut.Argonaut.StringToParseWrap
8            val convertedDataTest: Iterator[(Option[SimpleData], String)] = records.map(record => {
9             val maybeData: Option[SimpleData] = record.value().decodeOption[SimpleData]
10            (maybeData, record.value())
11          })
12
13          val testInvalidDataEntries: Int = convertedDataTest.count(record => {
14            val empty = record._1.isEmpty
15            if (empty) {
16              logger.error("Cannot parse data from kafka: " + record._2)
17            }
18            empty
19          })
20          val statsDClient = new NonBlockingStatsDClient("appName", "monitoring.host", 8125) // I know it should be a singleton :)
21          statsDClient.gauge("invalid-input-records", testInvalidDataEntries)
22
23          convertedDataTest
24            .filter(maybeData => maybeData._1.isDefined)
25            .map(data => data._1.get)
26        })
27
28        rdd.collect().length
29        stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
30      })

它按预期工作。即如果我隐含地计算无效条目,我就会期待价值。

不确定我明白为什么。想法?

可以在github

找到要播放的代码

1 个答案:

答案 0 :(得分:2)

原因其实非常简单,与Spark无关。

请参阅此Scala控制台示例,该示例根本不涉及Spark:

scala> val iterator: Iterator[String] = Seq("a", "b", "c").iterator
    iterator: Iterator[String] = non-empty iterator

scala> val count = new java.util.concurrent.atomic.AtomicInteger(0)
    count: java.util.concurrent.atomic.AtomicInteger = 0

scala> val mappedIterator = iterator.map(letter => {print("mapping!! "); count.incrementAndGet(); letter})
    mappedIterator: Iterator[String] = non-empty iterator

scala> count.get
    res3: Int = 0

看看我如何从一个迭代器和一个新的计数器开始,我在这个迭代器上映射但没有发生任何事情:println没有显示,并且计数仍为零。

但是当我实现mappedIterator的内容时:

scala> mappedIterator.next
    mapping!! res1: String = a

现在发生了一些事情,我得到了一个print和一个增强的计数器。

scala> count.get
    res2: Int = 1

在spark执行器的代码中也会发生同样的情况。

这是因为Scala iterators are lazy关于map操作。 (另请参阅herehere

因此,在您的第一个示例中,时间顺序发生的是:

  1. 您在orignal分区迭代器上定义转换(但您不会自己执行转换)
  2. 你推动你的计数器变量,它处于初始状态(因为没有变换发生)
  3. 您通过Spark转换就绪迭代器
  4. Spark实际上迭代了这个结果,因此映射发生了。但是你的副作用(第2步)已经完成了。
  5. 在第二种情况下,在将计数器发送到服务器之前,调用val testInvalidDataEntries: Int = convertedDataTest.count...执行实际映射(并在此过程中,计数器的增量)。

    所以,懒惰会使你的2个样本表现不同。

    (这也是为什么,从理论上讲,一般而言,我们倾向于在函数编程导向语言中的map操作中不会产生副作用,因为结果依赖于执行的顺序,而纯粹的函数风格应该防止这种情况。)

    计算失败的一种方法可能是使用Spark Accumulator累积结果,并在RDD完成终端操作后在驱动程序端执行更新。