大數據:Spark Core(三)Executor上是如何launch task

1. 啓動任務

在前面一篇博客中(Driver 啓動、分配、調度Task)介紹了Driver是如何調動、啓動任務的,Driver向Executor發送了LaunchTask的消息,Executor接收到了LaunchTask的消息後,進行了任務的啓動,在CoarseGrainedExecutorBackend.scala
    case LaunchTask(data) =>
      if (executor == null) {
        exitExecutor(1, "Received LaunchTask command but executor was null")
      } else {
        val taskDesc = ser.deserialize[TaskDescription](data.value)
        logInfo("Got assigned task " + taskDesc.taskId)
        executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,
          taskDesc.name, taskDesc.serializedTask)
      }

接收消息,反序列化了TaskDescription的對象


在TaskDescription反序列化了taskId, executeId, name,index, attemptNumber, serializedTask屬性,其中serializedTask是ByteBuffer。
Executor的launchTask方法
 def launchTask(
      context: ExecutorBackend,
      taskId: Long,
      attemptNumber: Int,
      taskName: String,
      serializedTask: ByteBuffer): Unit = {
    val tr = new TaskRunner(context, taskId = taskId, attemptNumber = attemptNumber, taskName,
      serializedTask)
    runningTasks.put(taskId, tr)
    threadPool.execute(tr)
  }
方法中通過線程池中啓動了線程運行TaskRunner的任務
private val threadPool = ThreadUtils.newDaemonCachedThreadPool("Executor task launch worker")
關於線程池,在executor啓動的是一個無固定大小線程數量限制的線程池,也就是說在executor的設計中,啓動的任務數量是完全由Driver來管控

2. 任務的運行

前面提到了TaskDescription中的serializedTask是個bytebuffer, 裏面的結構如下圖所示:





分別是task所依賴的文件的數量,文件的名字,時間戳,Jar的數量,Jar的名字,Jar的時間戳,屬性,subBuffer是個bytebuffer

2.1 加載Jars文件

Driver所運行的class等包括依賴的Jar文件在Executor上並不存在,Executor首先要fetch所依賴的jars,也就是TaskDescription中serializedTask中的jar部分
在上面的結構描述中,jar相關的只是numJars,jarName,timestamp並沒有jar的內容,也就是在LaunchTask裏的消息中並不攜帶Jar的內容,原因也很容易理解,rpc的消息體必須簡單高效
  • timestamp:這是用於判斷文件的時間戳,在相同文件名的情況下只有新的才需要重新fetch
  • jarName: 這裏的JarName是網絡文件名:spark://192.168.121.101:37684/jars/spark-examples_2.11-2.1.0.jar

通常在相同的Driver在起多個任務的時候,任務的所依賴的jar是基本相同的,所以沒必要每個Task都重新fetch相同的jars
for ((name, timestamp) <- newJars) {
        val localName = name.split("/").last
        val currentTimeStamp = currentJars.get(name)
          .orElse(currentJars.get(localName))
          .getOrElse(-1L)
        if (currentTimeStamp < timestamp) {
          logInfo("Fetching " + name + " with timestamp " + timestamp)
          // Fetch file with useCache mode, close cache for local mode.
          Utils.fetchFile(name, new File(SparkFiles.getRootDirectory()), conf,
            env.securityManager, hadoopConf, timestamp, useCache = !isLocal)
          currentJars(name) = timestamp
          // Add it to our class loader
          val url = new File(SparkFiles.getRootDirectory(), localName).toURI.toURL
          if (!urlClassLoader.getURLs().contains(url)) {
            logInfo("Adding " + url + " to class loader")
            urlClassLoader.addURL(url)
          }
        }
在Utils.fetchFile裏還做了一層cache,受參數控制
spark.files.useFetchCache
而在fetchFile的緩存中,緩存的文件被保存在executor的臨時文件夾中,例如

/tmp/spark-e9555893-6556-4a56-a692-54a984c3addb/executor-4b9581ca-fe9f-4e96-9db0-192146158a44/spark-bf41fdbd-a84e-473a-aa60-76480745b50b

緩存文件的命名規則:
s"${url.hashCode}${timestamp}_cache"
爲了避免同時線程安全問題,可能存在多個任務Fetch相同的文件,FetchFile使用了文件鎖,並且是細粒度的文件鎖,只增對相同的文件
1. 相同的文件名,這裏的文件名也是網絡文件名
2. 相同的時間戳
整個完整的流程如下
  1. 檢查本地是否有相同的緩存文件
  2. 如果沒有,先Fetch文件從Driver中獲取,通過URL:(

    spark://192.168.121.101:37684/jars/spark-examples_2.11-2.1.0.jar

    )複製到本地的緩存文件
  3. 複製本地緩存文件到工作目錄 /work/app-ID/executorid/
  4. 設置工作目錄文件具有可執行權限
最後通過urlClassLoader去loader這個jar文件

2.2 運行task


前面所提到的subBuffer實際上就是Task的序列化對象,通過反序列化可以獲取到Driver生成的Task
在Executor.scala裏的run方法中

 val res = task.run(
            taskAttemptId = taskId,
            attemptNumber = attemptNumber,
            metricsSystem = env.metricsSystem)

最後調用了task.run的方法,在task的run方法,所有繼承了Task的類都只需要實現runTask的方法

2.3 反序列化RDD,Dependency

RDD是算子,Dependency是依賴,這是在Executor需要的運算,但是在前面的序列化對象中,並沒有看到有RDD,Dep的屬性,那麼RDD,Dep是怎麼傳遞到Task裏進行運算的呢?
在DAG裏生成的task就是ShuffleMapTask, ResultTask,下面以ShuffleMapTask爲例,在runTask裏
 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
也就是基於taskBinary.value來進行反序列化獲得,在來看taskBinary成員
taskBinary: Broadcast[Array[Byte]],
  /** Get the broadcasted value. */
  def value: T = {
    assertValid()
    getValue()
  }
在前面博客章節中關於Spark Storage管理中提到在集羣下使用的是TorrentBroadcast
@transient private lazy val _value: T = readBroadcastBlock()
在前面的storage 系列(一)裏面已經談到過當本地的broadcastId不存在的時候,會嘗試去遠端(也就是Driver)獲取內容,這裏的BroadcastId格式是
broadcast_executorID
博客中也提到了同一個Executor擁有一個Block,一個大Block也存在多個Piece的小Block, 也就是格式
broadcast_executorID_pieceid
val blocks = readBlocks().flatMap(_.getChunks())
          logInfo("Reading broadcast variable " + id + " took" + Utils.getUsedTimeMs(startTimeMs))

          val obj = TorrentBroadcast.unBlockifyObject[T](
            blocks, SparkEnv.get.serializer, compressionCodec)
          // Store the merged copy in BlockManager so other tasks on this executor don't
          // need to re-fetch it.
          val storageLevel = StorageLevel.MEMORY_AND_DISK
          if (!blockManager.putSingle(broadcastId, obj, storageLevel, tellMaster = false)) {
            throw new SparkException(s"Failed to store $broadcastId in BlockManager")
          }

在遠端獲取多個piece塊後,在blockManager裏會合成一個以broadcast_executorID爲key的大block塊保存在blockManager裏,作爲緩存同一個executor下的其他運行的task直接使用blockManager裏的塊,而不在需要遠端在去獲取block。
在這裏blockManager同時也保存着每個piece的block快,主要考慮到TorrentBroadcast的時候,Executor也可以作爲一個傳播block塊的節點,而不只是Driver的單個節點。
Block裏面的內容反序列化後生成RDD和Dependency對象。

2.4 序列化RDD,Dependency

前面講了executor的反序列化的過程,當然序列化過程是在Driver中做的,回到DAGScheduler.scala的submitMissingTasks函數中
 var taskBinary: Broadcast[Array[Byte]] = null
    try {
      // For ShuffleMapTask, serialize and broadcast (rdd, shuffleDep).
      // For ResultTask, serialize and broadcast (rdd, func).
      val taskBinaryBytes: Array[Byte] = stage match {
        case stage: ShuffleMapStage =>
          JavaUtils.bufferToArray(
            closureSerializer.serialize((stage.rdd, stage.shuffleDep): AnyRef))
        case stage: ResultStage =>
          JavaUtils.bufferToArray(closureSerializer.serialize((stage.rdd, stage.func): AnyRef))
      }

      taskBinary = sc.broadcast(taskBinaryBytes)
    } catch {
      // In the case of a failure during serialization, abort the stage.
      case e: NotSerializableException =>
        abortStage(stage, "Task not serializable: " + e.toString, Some(e))
        runningStages -= stage

        // Abort execution
        return
      case NonFatal(e) =>
        abortStage(stage, s"Task serialization failed: $e\n${Utils.exceptionString(e)}", Some(e))
        runningStages -= stage
        return
    }
看到序列化的是Stage的rdd和shuffleDependency, 其中是Stage裏的rdd就是shuffleDep.rdd也就是ShuffledRDD裏prev的RDD

3 總結:

  • TaskDescription 只是包含了任務需要的文件列表,jar文件,配置相關屬性,並沒有這些具體的文件
  • 具體的文件下載路徑是Driver直接在TaskDescription中的serializedTask提供的
  • 具體要運行的Task是通過serializedTask中的subbuffer中反序列化的
  • Task中依賴的RDD,Dependency是從BlockManager從Driver的Block快中獲取進行反序列化
  • ShuffleMapTask裏依賴的的RDD是ShuffledRDD的前一個RDD,而Dependency就是ShuffleDependency











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