Spark閉包清理類ClosureCleaner簡析

Spark閉包清理類ClosureCleaner簡析

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。

手動碼字不易,請大家尊重勞動成果,謝謝

作者:http://blog.csdn.net/wang_wbq

從6月初開始因爲一些工作上的事情,已經好久沒有寫博客了,這次把之前Spark源碼閱讀中深入瞭解的Spark閉包清理類ClosureCleaner簡單介紹下,將知識留個檔以便以後忘記了還有個地方來還原下思路。

Scala閉包機制回顧

在之前文章Spark閉包清理類ClosureCleaner簡析中已經簡單介紹了Scala的閉包實現方式,即用$outer字段來從閉包中引用外部的變量。

另外在另一篇文章慎用Scala中的return表達式中,介紹了在lambda表達式中,return語句的含義是NonLocalReturn而不是僅僅退出該lambda表達式,因此在lambda表達式中,return語句是很危險的,並且隨時可能引起嚴重的後果。

Spark算子

這裏並不會詳細講解Spark的RDD算子,我們僅僅從最簡單的一個算子map入手,來看下你在rdd.map(func)中填入的func函數是如何運行在各個執行機之上的。

我們先從RDD類的map源碼看起:

def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}

def mapPartitions[U: ClassTag](f: Iterator[T] => Iterator[U], preservesPartitioning: Boolean = false): RDD[U] = withScope {
    val cleanedF = sc.clean(f)
    new MapPartitionsRDD(this, (context, index, iter) => cleanedF(iter), preservesPartitioning)
}

在這裏多列出了下mapPartition的源碼,原因是:當初我還是個Spark小白的時候,閱讀各種博客,在文章中經常會看到一個spark效率優化點,就是使用mapPartition而不是map可以提高運行效率然而從源碼來看,兩者的效率在大多數情況下應該是一致的。除非某些特殊情況:你自己寫了Partitioner做分區,並且需要對整個分區的元素集合做某種操作,這種情況下必須用mapPartition。

我們回到map方法中來,它一共只有兩句:

1、第一句對我們輸出的func方法做了clean,我們下一節會詳細介紹這個方法。
2、構造MapPartitionsRDD類來存放當前的RDD計算函數

從這裏可以看出RDD的原理其實就是一個類似鏈表的存儲結構,鏈表中存儲的就是該鏈節點應該對上個節點的結果所做的操作函數

在我們的func函數被存在了RDD裏之後,它就會最終被Action算子觸發,進入自己該在的Stage裏,並被Spark序列化taskBinary = sc.broadcast(taskBinaryBytes)後存在Task類中下發給各個Executor節點運行。

這裏有個重要的一點,Stage會被序列化成二進制,這說明其所有的成員都應該能被序列化,否則不可能在Executor中將其還原出來。因此,我們在寫Spark代碼的時候經常會碰到的org.apache.spark.SparkException: Task not serializable問題,就是因爲我們在func閉包裏引用了不可序列化的對象導致的。如果我們確實在閉包裏使用了這些對象,那沒話說,就是你的鍋,自己改成可序列化得類去。但是還有些我們根本沒用到的類,由於Scala的閉包機制,自動給我們引進來了,導致類的不可序列化,這種情況可就讓我們爲難了,舉個栗子:

object TestObject {
  def run(): Int = {
    var nonSer = new NonSerializable
    val x = 5
    withSpark(new SparkContext("local", "test")) { sc =>
      val nums = sc.parallelize(Array(1, 2, 3, 4))
      nums.map(_ + x).reduce(_ + _)
    }
  }
}

以上我引用了Spark測試代碼中的一個類來進行解釋。我們傳入map算子的閉包中,引用了外部的變量x,x爲val整數,從之前的文章中分析,它會被直接存進閉包裏,因此在閉包裏不會存在$outer引用外部,因此並沒啥問題。所以這是假栗子,僅僅幫助大家回顧下閉包而已。

再看個真正的栗子:

class TestClass extends Serializable {
  var nonSer = new NonSerializable
  var x = 5
  def getX: Int = x

  def run(): Int = {

    withSpark(new SparkContext("local", "test")) { sc =>
      val nums = sc.parallelize(Array(1, 2, 3, 4))
      nums.map(_ + getX).reduce(_ + _)
    }
  }
}

在傳入map的閉包裏,我們引用了外部TestClass類的getX方法,因此在閉包裏會存在外部類的引用,這時如果我們直接對這個閉包做序列化,那由於閉包->$outer->nonSer間接包含了不可序列化對象,因此肯定會造成序列化失敗異常。

Spark閉包清理類ClosureCleaner

爲了解決以上問題,spark引入了閉包清理類,即在map的第一行調用了val cleanF = sc.clean(f)方法來實現的。下面我們深入該方法來一探究竟。

閉包清理最終調用了ClosureCleaner.clean(f, checkSerializable)方法來實現對f函數的閉包清理。

其內部代碼涉及到的點很多,本文就不詳細進行介紹了,以下介紹幾個關鍵的技術點供大家來理解:

1、前一節提到return表達式是非常危險的,因此代碼中對return做了判斷,一旦發現return出現就會拋出異常快速失敗:

if (op == NEW && tp.contains("scala/runtime/NonLocalReturnControl")) {
            throw new ReturnStatementInClosureException
          }

2、既然Scala的閉包是使用成員變量$outer來引用的,並且一個成員變量是否被用到必然要從字節碼層面來進行判斷,因此Spark閉包清理類中大量使用了org.apache.xbean.asm5包來遍歷所有用到的方法的字節碼,通過GETFIELDINVOKEVIRTUALINVOKESPECIALJVM操作碼來找到所引用的對象、方法、內部使用的類。

3、以傳入clean的方法func爲基準,向內查找找出所有內部使用到的對象所對應的成員變量和方法。

4、向外$outer查找所有外部所引用到的類,並將所有這些類克隆一份,但保持所有成員變量爲初始值。

5、使用在內部查找到的所有引用變量值填充外部的克隆類成員變量。並將外部的$outer引用關係按照原始形勢鏈接起來。

6、最終,使用反射將func$outer成員換成對應的克隆後的克隆$outer對象

經過以上操作,就完成了對外部閉包引用的清理。簡而言之就是,克隆個新的,用到的就填上,沒用到的對象就是null了,這樣就避免了不可序列化對象影響到了Spark的任務序列化功能。

不過這個閉包清理類還並不是那麼完善,如SPARK-22328中的以下對話就隱藏了一個如果外部引用對象存在父類時的問題,當時我對這種場景以及延伸場景進行了驗證,確實無法正確處理。
還有就是用方法獲取了一個外部類,但是只用到了類中的一個可序列化對象,而該類存在其他不可序列化對象,這種情況下,序列化也是失敗的。因爲1、確實使用到了這個對象,因此對象會被賦值。2、無法簡單從字節碼中識別出改引用對象,因此代碼中也沒對該場景做特殊處理。


cloud-fan on 25 Oct 2017 Contributor
Assume we have class A and B having the same parent class P. P has 2 fields a and b. The closure accessed A.a and B.b, so when we clone A object, we should only set field a, when we clone B object, we should only set field b. However here seems we set field a and b for A and B object, which is sub-optimal.


cloud-fan on 25 Oct 2017 Contributor
Seems this is also a issue for the outerClasses, maybe I missed something…


viirya on 25 Oct 2017 Contributor
Seems that is true. For a closure that only accessed A.a, we clone the whole A object which contains both a and b fields. This is the fact in existing ClosureCleaner.


viirya on 25 Oct 2017 Contributor
As this is not a regression, IIUC, will it block this change?


cloud-fan on 25 Oct 2017 Contributor
yea let’s leave it

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章