一個供Executor執行的可執行的邏輯單元,Spark目前提供了兩類Task,分別爲ShuffleMapTask和ResultTask。Job會由一個或者多個Stage,一個Spark Job基於Stage構建成邏輯執行計劃和物理執行計劃。如: Job A={S1,S2,S3} 由三個Stage構成,那麼S1、S2會由ShuffleMapTasks構成,S3作爲Job的最後一個Stage由多個ResultTasks構成。Task在Driver側構建,在Executor側執行,執行完一個ResultTask會把Task的輸出發送給Driver側的應用程序,執行完一個ShuffleMapTask後基於Task的Partitioner把Task的輸出劃分多份進行存儲。
如上圖:一個Stage內只會存在同一種Task,Task數量與Stage的Partition數量保持一致(運行的Task數量可能會大於Partition數量)
構造函數:
private[spark] abstract class Task[T](
//Task對應的Stage唯一ID號
val stageId: Int,
//stageAttemptId表示Stage被執行的次數,第一次爲1
val stageAttemptId: Int,
//Task對應的partitionid,一個Task對一個唯一的partitionid
val partitionId: Int,
@transient var localProperties: Properties = new Properties,
// The default value is only used in tests.
serializedTaskMetrics: Array[Byte] =
SparkEnv.get.closureSerializer.newInstance().serialize(TaskMetrics.registered).array(),
val jobId: Option[Int] = None,
val appId: Option[String] = None,
val appAttemptId: Option[String] = None) extends Serializable {}
屬性:
//在MapOutputTracker中定義的,記錄Map側失敗的次數,由Driver側維護。Executor每次執行Task的時候會檢查loca epoch<epoch,會更新epoch,並清除Executor的緩存的mapStatuses。
//在Local Mode下epoch沒有意義
var epoch: Long = -1
// TaskContext在run方法中初始化,即在Executor側初始化
@transient var context: TaskContextImpl = _
TaskContextImpl記錄了一個Task的上下文信息,包括Task的重要屬性和執行結果。
上下文屬性:
屬性 |
說明 |
stageid |
Stage的唯一ID號 |
stageAttemptNumber |
Stage被執行次數,每失敗一次加一,由Driver側維護 |
partitionid |
當前Task所對應的Partitionid,一對一的關係 |
attemptNumber |
Task被執行的次數,每失敗一次加一,由Driver側維護 |
執行結果:
方法 |
說明 |
addTaskCompletionListener |
添加任務完成的事件,任務失敗後也會添加完成事件 |
addTaskFailureListener |
添加任務失敗事件 |
方法:
//由Executor執行,ShuffleMapTask、ResultTask分別實現了runTask,詳情請見Task執行小節
def runTask(context: TaskContext): T
綜上述,Spark中的Task主要信息包括 Stage、Partitioner、TaskContext等這些Spark Core的概念,不包括RDD和用戶的編程邏輯代碼。Driver通過Broadcast把RDD廣播給所有的Executor,通過Endpoint把Task點對點的發送到指定的Executor。
注意:RDD與Stage分開存在一個明顯的性能問題,Executor每次執行Task的時候都需要返序列化一次,下圖爲ShuffleMapTask中反序列化的代碼。
// RDD本身數據是通過BroadCast廣播出去給所有Executor,這裏通過BroadCast獲取RDD並反序列化
val (rdd, dep) = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])](
ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
_executorDeserializeTime = System.currentTimeMillis() - deserializeStartTime
_executorDeserializeCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
threadMXBean.getCurrentThreadCpuTime - deserializeStartCpuTime
} else 0L
我們都知道反序列化是一個很消耗CPU的操作,對於大數據計算框架CPU資源顯得更加珍貴。在DAGScheduler.submitMissingTasks方法中Spark的開發人員有考慮過把RDD放到Stage裏面以避免多次的反序列化。通過BroadCast廣播給所有的Executor,可以確保每個Task都可獨佔一個RDD序列化後的副本,在Task之間可以提供很強的隔離性。他們擔心用戶在RDD的Function閉包中通過對象引用修改對象的狀態,例如:Hadoop中JobConf/Configuration對象不是線程安全的。
// TODO: Maybe we can keep the taskBinary in Stage to avoid serializing it multiple times.
// Broadcasted binary for the task, used to dispatch tasks to executors. Note that we broadcast
// the serialized copy of the RDD and for each task we will deserialize it, which means each
// task gets a different copy of the RDD. This provides stronger isolation between tasks that
// might modify state of objects referenced in their closures. This is necessary in Hadoop
// where the JobConf/Configuration object is not thread-safe.
var taskBinary: Broadcast[Array[Byte]] = null