这是关于处理情况的一个非常常见的火花相关问题,哪一段代码被执行在哪个火花公园(执行人/司机)。 有了这段代码,我有点惊讶为什么我没有得到我期待的值:
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
找到要播放的代码答案 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
操作。 (另请参阅here和here)
因此,在您的第一个示例中,时间顺序发生的是:
在第二种情况下,在将计数器发送到服务器之前,调用val testInvalidDataEntries: Int = convertedDataTest.count...
执行实际映射(并在此过程中,计数器的增量)。
所以,懒惰会使你的2个样本表现不同。
(这也是为什么,从理论上讲,一般而言,我们倾向于在函数编程导向语言中的map
操作中不会产生副作用,因为结果依赖于执行的顺序,而纯粹的函数风格应该防止这种情况。)
计算失败的一种方法可能是使用Spark Accumulator累积结果,并在RDD完成终端操作后在驱动程序端执行更新。