在慢速缓存期间运行org.apache.spark.executor.CoarseGrainedExecutorBackend 的进程的jstack 显示以下内容:
"Executor task launch worker-4" #76 daemon prio=5 os_prio=0 tid=0x00000000030a4800 nid=0xdfb runnable [0x00007fa5f28dd000]
java.lang.Thread.State: RUNNABLE
at java.util.IdentityHashMap.resize(IdentityHashMap.java:481)
at java.util.IdentityHashMap.put(IdentityHashMap.java:440)
at org.apache.spark.util.SizeEstimator$SearchState.enqueue(SizeEstimator.scala:176)
at org.apache.spark.util.SizeEstimator$.visitArray(SizeEstimator.scala:251)
at org.apache.spark.util.SizeEstimator$.visitSingleObject(SizeEstimator.scala:211)
at org.apache.spark.util.SizeEstimator$.org$apache$spark$util$SizeEstimator$$estimate(SizeEstimator.scala:203)
at org.apache.spark.util.SizeEstimator$$anonfun$sampleArray$1.apply$mcVI$sp(SizeEstimator.scala:284)
at scala.collection.immutable.Range.foreach$mVc$sp(Range.scala:141)
at org.apache.spark.util.SizeEstimator$.sampleArray(SizeEstimator.scala:276)
at org.apache.spark.util.SizeEstimator$.visitArray(SizeEstimator.scala:260)
at org.apache.spark.util.SizeEstimator$.visitSingleObject(SizeEstimator.scala:211)
at org.apache.spark.util.SizeEstimator$.org$apache$spark$util$SizeEstimator$$estimate(SizeEstimator.scala:203)
at org.apache.spark.util.SizeEstimator$.estimate(SizeEstimator.scala:70)
at org.apache.spark.util.collection.SizeTracker$class.takeSample(SizeTracker.scala:78)
at org.apache.spark.util.collection.SizeTracker$class.afterUpdate(SizeTracker.scala:70)
at org.apache.spark.util.collection.SizeTrackingVector.$plus$eq(SizeTrackingVector.scala:31)
at org.apache.spark.storage.MemoryStore.unrollSafely(MemoryStore.scala:285)
at org.apache.spark.CacheManager.putInBlockManager(CacheManager.scala:171)
at org.apache.spark.CacheManager.getOrCompute(CacheManager.scala:78)
at org.apache.spark.rdd.RDD.iterator(RDD.scala:268)
at org.apache.spark.scheduler.ResultTask.runTask(ResultTask.scala:66)
at org.apache.spark.scheduler.Task.run(Task.scala:89)
at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:214)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
"Executor task launch worker-5" #77 daemon prio=5 os_prio=0 tid=0x00007fa6218a9800 nid=0xdfc runnable [0x00007fa5f34e7000]
java.lang.Thread.State: RUNNABLE
at java.util.IdentityHashMap.put(IdentityHashMap.java:428)
at org.apache.spark.util.SizeEstimator$SearchState.enqueue(SizeEstimator.scala:176)
at org.apache.spark.util.SizeEstimator$$anonfun$visitSingleObject$1.apply(SizeEstimator.scala:224)
at org.apache.spark.util.SizeEstimator$$anonfun$visitSingleObject$1.apply(SizeEstimator.scala:223)
at scala.collection.immutable.List.foreach(List.scala:318)
at org.apache.spark.util.SizeEstimator$.visitSingleObject(SizeEstimator.scala:223)
at org.apache.spark.util.SizeEstimator$.org$apache$spark$util$SizeEstimator$$estimate(SizeEstimator.scala:203)
at org.apache.spark.util.SizeEstimator$.estimate(SizeEstimator.scala:70)
at org.apache.spark.util.collection.SizeTracker$class.takeSample(SizeTracker.scala:78)
at org.apache.spark.util.collection.SizeTracker$class.afterUpdate(SizeTracker.scala:70)
at org.apache.spark.util.collection.SizeTrackingVector.$plus$eq(SizeTrackingVector.scala:31)
at org.apache.spark.storage.MemoryStore.unrollSafely(MemoryStore.scala:285)
at org.apache.spark.CacheManager.putInBlockManager(CacheManager.scala:171)
at org.apache.spark.CacheManager.getOrCompute(CacheManager.scala:78)
at org.apache.spark.rdd.RDD.iterator(RDD.scala:268)
at org.apache.spark.scheduler.ResultTask.runTask(ResultTask.scala:66)
at org.apache.spark.scheduler.Task.run(Task.scala:89)
at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:214)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
SizeEstimator 作为缓存表面上已经在内存中的东西的主要成本之一是有意义的,因为对未知对象进行正确的大小估计可能相当困难;如果查看visitSingleObject 方法,您会发现它严重依赖反射,调用getClassInfo 访问运行时类型信息;不仅遍历了完整的对象层次结构,而且每个嵌套成员都会根据IdentityHashMap 进行检查,以检测哪些引用引用了相同的具体对象实例,因此堆栈跟踪显示了这些 IdentityHashMap 操作中的大量时间。
在您的示例对象的情况下,您基本上将每个项目作为从包装整数到包装整数的映射列表;大概 Scala 的内部映射实现也包含一个数组,这解释了 visitSingleObject -> List.foreach -> visitSingleObject -> visitSingleObject 调用层次结构。无论如何,在这种情况下,要访问 很多 个内部对象,并且 SizeEstimators 为每个采样的对象设置一个新的 IdentityHashMap。
在您测量的情况下:
profile( rdd.cache.count )
这不算在执行缓存逻辑,因为 RDD 已经成功缓存,所以 Spark 足够聪明,不会重新运行缓存逻辑。实际上,您可以通过直接分析新的 RDD 创建和缓存来独立于额外的“map(identity)”转换来隔离缓存逻辑的确切成本;这是我的 Spark 会话,从您的最后几行开始:
scala> profile( rdd.count )
time = 91ms
res1: Long = 1000
scala> profile( rdd.map(identity).count )
time = 112ms
res2: Long = 1000
scala> profile( rdd.cache.count )
time = 59ms
res3: Long = 1000
scala> profile( rdd.map(identity).cache.count )
time = 6564ms
res4: Long = 1000
scala> profile( sc.parallelize(1 to n).map( k => bigContent() ).count )
time = 14990ms
res5: Long = 1000
scala> profile( sc.parallelize(1 to n).map( k => bigContent() ).cache.count )
time = 22229ms
res6: Long = 1000
scala> profile( sc.parallelize(1 to n).map( k => bigContent() ).map(identity).cache.count )
time = 21922ms
res7: Long = 1000
所以你可以看到,缓慢并不是因为你运行了map 转换,而是在这种情况下,~6s 似乎是计算 1000 缓存逻辑的基本成本当每个对象具有大约 1,000,000 到 10,000,000 个内部对象时的对象(取决于 Map 实现的布局方式;例如,顶部堆栈跟踪中的额外 visitArray 嵌套暗示 HashMap impl 具有嵌套数组,这对于每个哈希表条目内的典型密集线性探测数据结构)。
对于您的具体用例,如果可能,您应该在延迟缓存方面犯错,因为缓存中间结果会产生开销,如果您真的不打算将中间结果用于大量单独的下游转换。但是正如您在问题中提到的那样,如果您确实使用一个 RDD 来分支到多个不同的下游转换,那么如果原始转换非常昂贵,您可能确实需要缓存步骤。
解决方法是尝试使用更适合恒定时间计算的内部数据结构(例如基元数组),您可以在其中节省大量成本以避免迭代大量数字包装器对象的数量,并取决于它们在 SizeEstimator 中的反射。
我尝试了 Array[Array[Int]] 之类的方法,尽管开销仍然不为零,但对于类似的数据大小,它的性能要好 10 倍:
scala> def bigContent2() = (1 to 1000).map( i => (1 to 1000).toArray ).toArray
bigContent2: ()Array[Array[Int]]
scala> val rdd = sc.parallelize(1 to n).map( k => bigContent2() ).cache
rdd: org.apache.spark.rdd.RDD[Array[Array[Int]]] = MapPartitionsRDD[23] at map at <console>:28
scala> rdd.count // to trigger caching
res16: Long = 1000
scala>
scala> // profiling
scala> profile( rdd.count )
time = 29ms
res17: Long = 1000
scala> profile( rdd.map(identity).count )
time = 42ms
res18: Long = 1000
scala> profile( rdd.cache.count )
time = 34ms
res19: Long = 1000
scala> profile( rdd.map(identity).cache.count )
time = 763ms
res20: Long = 1000
为了说明反射任何更高级对象的成本有多糟糕,如果我删除最后一个 toArray 并最终使每个 bigContent 成为 scala.collection.immutable.IndexedSeq[Array[Int]],则性能回到 ~2x 以内原IndexSeq[Map[Int,Int]]案例的缓慢:
scala> def bigContent3() = (1 to 1000).map( i => (1 to 1000).toArray )
bigContent3: ()scala.collection.immutable.IndexedSeq[Array[Int]]
scala> val rdd = sc.parallelize(1 to n).map( k => bigContent3() ).cache
rdd: org.apache.spark.rdd.RDD[scala.collection.immutable.IndexedSeq[Array[Int]]] = MapPartitionsRDD[27] at map at <console>:28
scala> rdd.count // to trigger caching
res21: Long = 1000
scala>
scala> // profiling
scala> profile( rdd.count )
time = 27ms
res22: Long = 1000
scala> profile( rdd.map(identity).count )
time = 39ms
res23: Long = 1000
scala> profile( rdd.cache.count )
time = 37ms
res24: Long = 1000
scala> profile( rdd.map(identity).cache.count )
time = 2781ms
res25: Long = 1000
正如评论部分所讨论的,您还可以考虑使用 MEMORY_ONLY_SER StorageLevel,只要有一个高效的序列化器,它很可能比 SizeEstimator 中使用的递归反射便宜;为此,您只需将cache() 替换为persist(StorageLevel.MEMORY_ONLY_SER);正如this other question 中提到的,cache() 在概念上与persist(StorageLevel.MEMORY_ONLY) 相同。
import org.apache.spark.storage.StorageLevel
profile( rdd.map(identity).persist(StorageLevel.MEMORY_ONLY_SER).count )
我实际上在 Spark 1.6.1 和 Spark 2.0.0-preview 上都试过这个,运行时集群配置的其他一切都完全相同(分别使用Google Cloud Dataproc 的“1.0”和“预览”图像版本)。不幸的是,MEMORY_ONLY_SER 技巧在 Spark 1.6.1 中似乎没有帮助:
scala> profile( rdd.map(identity).persist(StorageLevel.MEMORY_ONLY_SER).count )
time = 6709ms
res19: Long = 1000
scala> profile( rdd.map(identity).cache.count )
time = 6126ms
res20: Long = 1000
scala> profile( rdd.map(identity).persist(StorageLevel.MEMORY_ONLY).count )
time = 6214ms
res21: Long = 1000
但在 Spark 2.0.0-preview 中,它似乎将性能提高了 10 倍:
scala> profile( rdd.map(identity).persist(StorageLevel.MEMORY_ONLY_SER).count )
time = 500ms
res18: Long = 1000
scala> profile( rdd.map(identity).cache.count )
time = 5353ms
res19: Long = 1000
scala> profile( rdd.map(identity).persist(StorageLevel.MEMORY_ONLY).count )
time = 5927ms
res20: Long = 1000
不过,这可能会因您的对象而异;只有在序列化本身不使用大量反射的情况下,才能预期加速;如果您能够有效地使用Kryo serialization,那么您很可能会看到使用MEMORY_ONLY_SER 对这些大型对象的改进。