Spark簡明筆記

Vincent_西北偏北1467772663000發表於2018-11-24

一、Spark結構

Spark簡明筆記

  • 使用java、scala、python任意一種語言編寫的Spark應用叫Driver
  • Driver程式一般負責初始SparkContext,然後通過SparkContext與整個叢集通訊,進行分散式計算,比如通過SparkContext建立RDD。鑑於Driver行駛的地位,其角色上有可叫central coordinator
  • SparkContext與叢集通訊的方式
    1. 第一步先通過Cluster Manager申請計算資源Executor
    2. 第二步,SparkContext與Executor直接通訊,將分散式計算程式傳送到每個Executor
    3. 第三步,SparkContext傳送當前要執行的計算Task給Executor執行
  • Worker Node是Spark叢集中的某個具體節點,也叫slave
  • Executor是在Worker Node上開的一個應用執行器,他會在worknode上起一個JVM, 他可以執行多個Task, Executor是應用隔離的。也即一個Executor只能屬於某一個Spark應用,這樣Spark叢集才能同時服務多個Spark應用,互不干擾。
  • Executor有點像Java中的工作執行緒一樣,可以執行SparkContext發來的多個任務。不同的是Executor是一個獨立的JVM程式
  • Cluster Manager是有多種型別,可以是Spark自帶的Standalone 叢集,也可以是YARN或Mesos叢集

二、如何部署Spark程式

以scala為例,我們通過IDE編寫Spark應用後,將其打包成jar包,然後使用spark-submit程式進行部署

 ./bin/spark-submit \
  --class <main-class> \
  --master <master-url> \
  --deploy-mode <deploy-mode> \
  --conf <key>=<value> \
  ... # other options
  <application-jar> \
  [application-arguments]
複製程式碼
  • --class: 應用的主方法入口
  • --master: cluster manager叢集地址,可以是local,也可以是yarn或mesos,或者spark自帶的standalone 地址 
  • --deploy-mode: Whether to deploy your driver on the worker nodes (cluster) or locally as an external client (client) (default: client)
  • --conf: Arbitrary Spark configuration property in key=value format. For values that contain spaces wrap “key=value” in quotes (as shown).
  • application-jar: Path to a bundled jar including your application and all dependencies. The URL must be globally visible inside of your cluster, for instance, an hdfs:// path or a file:// path that is present on all nodes.
  • application-arguments: Arguments passed to the main method of your main class, if any

2.1 上述master引數可配置的值如下

Spark簡明筆記

2.2 Spark配置優先順序

優先順序從高到低依次是:

  • 直接在程式碼中通過SparkConf控制,比如指定cluster manager的master引數,可以在程式碼中配置

    val conf = new SparkConf().setAppName("WordCount").setMaster("local");

  • 在命令中指定,比如:

    ./bin/spark-submit
    --class org.apache.spark.examples.SparkPi
    --master yarn
    --deploy-mode cluster \ # can be client for client mode --executor-memory 20G
    --num-executors 50
    /path/to/examples.jar
    1000

  • 在spark的安裝目錄下,通過spark-defaults.conf配置。

三、RDD

RDD是一個統一分散式資料抽象資料集。其下對應實際的資料儲存介質,可能是檔案,也可以是hadoop。通過RDD可以進行tranformation和action操作,從而實現分散式計算。

3.1 關鍵資料結構

一個RDD具有以下固定的資料結構

  • 需要應用的計算操作,也即transformation
  • 當前RDD對應的分割槽列表。因為資料是分割槽儲存的
  • 當前RDD依賴的父資料集。每個RDD都維護一個其依賴關係,這就構成了一個親緣圖譜叫做DAG(Directed Acyclic Graph),中文稱作有向無環圖。

總結來說,一個RDD的關鍵資訊無非是,定義了資料來源,資料分佈儲存的情況,以及準備執行的計算邏輯。通過這些新,我們可以構建一個圖,圖的兩個vertex分別是RDD,edge為computation

  private[spark] def conf = sc.conf
  // =======================================================================
  // Methods that should be implemented by subclasses of RDD
  // =======================================================================

  /**
   * :: DeveloperApi ::
   * Implemented by subclasses to compute a given partition.
   */
  @DeveloperApi
  def compute(split: Partition, context: TaskContext): Iterator[T]//當前RDD需要執行的計算

  /**
   * Implemented by subclasses to return the set of partitions in this RDD. This method will only
   * be called once, so it is safe to implement a time-consuming computation in it.
   *
   * The partitions in this array must satisfy the following property:
   *   `rdd.partitions.zipWithIndex.forall { case (partition, index) => partition.index == index }`
   */
  protected def getPartitions: Array[Partition]//當前RDD對應的分割槽

  /**
   * Implemented by subclasses to return how this RDD depends on parent RDDs. This method will only
   * be called once, so it is safe to implement a time-consuming computation in it.
   */
  protected def getDependencies: Seq[Dependency[_]] = deps//當前RDD依賴的父親資料集

  /**
   * Optionally overridden by subclasses to specify placement preferences.
   */
  protected def getPreferredLocations(split: Partition): Seq[String] = Nil

  /** Optionally overridden by subclasses to specify how they are partitioned. */
  @transient val partitioner: Option[Partitioner] = None

  // =======================================================================
  // Methods and fields available on all RDDs
  // =======================================================================

  /** The SparkContext that created this RDD. */
  def sparkContext: SparkContext = sc

  /** A unique ID for this RDD (within its SparkContext). */
  val id: Int = sc.newRddId()

  /** A friendly name for this RDD */
  @transient var name: String = null&emsp;//當前RDD的名稱
複製程式碼

####3.2 RDD 特點

  • 分散式
  • 不可變性,一個RDD生成後,就不可變,所有Transformation操作,都是在原RDD基礎上生成新的RDD。
  • 自動容錯特性,Spark RDD記錄了資料譜系資訊(Data lineage),也即check point。這樣在某步失敗後,可以直接重試那一步,而不用所有計算過程重來。譜系資訊記錄了,輸入的資料,以及處理函式。由於RDD的不可變特性,以及處理函式的冪等性,使得整個重試不會有side effect。依然能保持計算的一致性
  • 沒有效能優化 DataFrame會根據使用者的sql,自動做效能優化。而RDD要求使用者自己組織transformation atcion程式碼,可能使用者組織的不合理,會導致資料頻繁在叢集間移動
  • 沒有結構化資訊 DataFrame有欄位的名稱,型別等資訊,但RDD沒有
  • Lazy Comuting.只有action時,前面所有的transformation動作才會執行。這節約了空間和時間。試想,如果每個transformation都單獨執行一次,那每一次的計算排程都有時間成本,以及中間結果的儲存成本

四、Spark的計算流程

  • driver建立RDD
  • RDD通過SparkContext的runJob方法,提交一次資料計算
  • SparkContext最終又交由DAGScheduler的runJob進行計算job執行
  • DAGScheduler使用handleJobSubmitted方法處理job,第一步是根據RDD中的DAG構建Stage列表。不涉及資料移動的transformation會被放到一個stage裡面,比如filter和map操作,他們可以並行的在各分割槽中執行。第二步通過submitStage提交Stage到叢集
  • DAGScheduler submitStage再呼叫submitMissingTasks方法。submitMissingTasks會將stage轉化成task
  • task最後通過TaskScheduler提交到spark叢集的worknode,進行實際執行

Spark簡明筆記

###五、RDD Transformation 將RDD進行一系列變換,生成新的RDD的過程,叫做Transformation。所有那些可以就地計算,而不需要資料遷移的transformation叫做Narrow Transformation。

####5.1 transformation大概原始碼 以map操作為例

  def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    //將傳進來的函式f進行clean,這裡先不深究,只需要知道clean後的函式,跟原函式功能相同
    
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
    //這裡返回MapPartitionsRDD物件,其構造引數為當前RDD和一個將f應用於迭代器的函式定義
  }
複製程式碼
  • 可以看到map方法並沒有執行任何函式,而只是將所有計算過程和資料包裝成MapPartitionsRDD後返回。這也就是transformation操作,lazy Computing的特點所在。
  • 所有tranformation返回的都是RDD,比如filter。其餘transfomation 函式原始碼大致同map類似,不再贅述

5.2 flatMap

map操作是將迭代RDD中的每個元素,然後將其做一定加工,返回的的依然是一個元素。而flapMap接受的函式引數的入參是RDD中的每個元素,但對該元素處理後,返回的是一個集合,而不是一個元素。flatMap原始碼如下:

  def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.flatMap(cleanF))
  }
複製程式碼

總結來說,map和flapMap的異同點如下: - map接受的函式引數簽名是:(f: T => U)而flatMap接受的函式引數簽名為:(f: T => TraversableOnce[U]),可以看到返回的是集合

  • map和flatMap的返回值都是RDD[T]。也即是說,flatMap擁有將多個集合資料合併,抹平的功效,從該函式的命名也可看出這一點,flat是平的意思

5.2 Narrow Transformations

Narrow Transformation操作有

Spark簡明筆記

5.3 Wide Transformations

有些計算,需要依賴其他節點資料,這種計算會導致資料移動,成為Wide Transformations。比如,基於某個key分類的操作GroupByKey,這個Key可能散落在不同的work node上,為了進行GroupByKey計算,需要計算節點間進行資料移動,比如將某個Key對應的資料,統一移動到一個節點上。Wide Transformation操作有如下:

Spark簡明筆記

六、RDD Action

所有Tranformation操作,都不會真正執行,直到Action操作被呼叫,Action操作返回是具體值,而不是RDD。這種特性成為Lazy Computing. Action操作觸發後,會將執行結果發給Driver 或者寫如到外部儲存。以下操作屬於Action操作: First(), take(), reduce(), collect(), count()

6.1 關鍵action原始碼

所有action操作,最終都會呼叫SparkContext的runJob方法。runJob有需多過載方法,以其中一個為例

def runJob[T, U: ClassTag](
      rdd: RDD[T],//需要處理的RDD資料
      processPartition: Iterator[T] => U,//需要在每個資料分割槽上進行的操作
      resultHandler: (Int, U) => Unit)//如何將上述每個分割槽處理後的結果進行處理
複製程式碼

可以看到runJob中體現了所有分散式計算理論架構,即MapReduce。其中processPartition定義每個分割槽要需要做的map操作,這一步將減少資料量,將map操作的結果做為輸入,傳進reduce操作,進行彙總處理。

6.2 aggregate

  def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U = withScope {
    // Clone the zero value since we will also be serializing it as part of tasks
    var jobResult = Utils.clone(zeroValue, sc.env.serializer.newInstance())//1
    val cleanSeqOp = sc.clean(seqOp)
    val cleanCombOp = sc.clean(combOp)
    val aggregatePartition = (it: Iterator[T]) => it.aggregate(zeroValue)(cleanSeqOp, cleanCombOp)//2
    val mergeResult = (index: Int, taskResult: U) => jobResult = combOp(jobResult, taskResult)//3
    sc.runJob(this, aggregatePartition, mergeResult)//4
    jobResult//5
  }
複製程式碼
  1. 定義一個結果彙總變數,它將儲存aggregate方法最終的返回結果,初始值為zeroValue
  2. 在每個RDD資料分割槽上,使用迭代器,應用aggregate方法,初始值為都為zeroValue
  3. 對每個分割槽的結果,使用combOp方法進行彙總計算。輸入index為分割槽的編號,taskResult為上一步每個分割槽計算後的結果,同彙總變數jobResult再來進行combOp計算。從第一步可知,jobResult的初始值為zeroValue
  4. 將上述兩個函式作為入參,傳遞給sc.runJob方法,在spark叢集進行執行
  5. 返回結果

舉例:

    val inputrdd = sc.parallelize(List(("maths", 21),("english", 22),("science", 31)),2)
    val result = inputrdd.aggregate(3)(
      (acc, value) => {
        println(acc+":"+value)
        (acc + value._2)
      },
      (acc1, acc2) => (acc1 * acc2)
    )
    println(result)//輸出4032
複製程式碼

解釋:

  • 上述RDD,被切分成兩個分割槽。第一個分割槽資料是("maths", 21) ,另一個是:("english", 22),("science", 31)

  • (acc + value._2)是每個分割槽要執行的操作,迭代器帶入zeroValue=3後,兩個分片的計算中間值如下

    3+21=24//分割槽1 3+22+31=56//分割槽2

  • 最後將每個分割槽結果帶入(acc1 * acc2)函式,從aggregate原始碼得知,結果計算也要運用zeroValue,在這裡也就是3.於是最終步執行的計算如下:

    32456=4032

6.3 fold

fold函式同aggregate類似,同樣是呼叫SparkContext的runJob函式,只不過fold只接受一個值引數,和一個函式引數,其內部在呼叫runJob時,分割槽計算和結果計算都使用同樣的函式。原始碼如下:

  def fold(zeroValue: T)(op: (T, T) => T): T = withScope {
    // Clone the zero value since we will also be serializing it as part of tasks
    var jobResult = Utils.clone(zeroValue, sc.env.closureSerializer.newInstance())
    val cleanOp = sc.clean(op)
    val foldPartition = (iter: Iterator[T]) => iter.fold(zeroValue)(cleanOp)
    val mergeResult = (index: Int, taskResult: T) => jobResult = op(jobResult, taskResult)
    sc.runJob(this, foldPartition, mergeResult)
    jobResult
  }
複製程式碼

舉例:

val inputrdd = sc.parallelize(List(("maths", 21),("english", 22),("science", 31)),2)//1
val result = inputrdd.fold(("test",3))(
  (acc, ele) => {
    println(acc+":"+ele)
    ("result",acc._2 + ele._2)
  }
)
println(result)//輸出:(result,83)
複製程式碼

假設註釋1中切分的2個分割槽為("maths", 21)和("english", 22),("science", 31),那麼執行過程如下:

  1. 3+21=24
  2. 3+22+31=56
  3. 3+24+56=83

6.4 reduce

reduce同樣呼叫了SparkContext的runJob函式,但reduce接收的引數在fold上進一步簡化,少了zeroValue引數,只接收一個函式引數即可。同樣該引數,在呼叫runJob時,即作為分割槽收斂的函式,記作為分割槽彙總計算的函式

  def reduce(f: (T, T) => T): T = withScope {
    val cleanF = sc.clean(f)
    val reducePartition: Iterator[T] => Option[T] = iter => {
      if (iter.hasNext) {
        Some(iter.reduceLeft(cleanF))
      } else {
        None
      }
    }
    var jobResult: Option[T] = None
    val mergeResult = (index: Int, taskResult: Option[T]) => {
      if (taskResult.isDefined) {
        jobResult = jobResult match {
          case Some(value) => Some(f(value, taskResult.get))
          case None => taskResult
        }
      }
    }
    sc.runJob(this, reducePartition, mergeResult)
    // Get the final result out of our Option, or throw an exception if the RDD was empty
    jobResult.getOrElse(throw new UnsupportedOperationException("empty collection"))
  }
複製程式碼

舉例:

    val inputrdd = sc.parallelize(List(("maths", 21),("english", 22),("science", 31)),2)
    val result = inputrdd.reduce(
      (acc, ele) => {
        println(acc+":"+ele)
        ("result",acc._2 + ele._2)
      }
    )
    println(result)//結果為:(result,74)
複製程式碼

6.5 collect&top

collect和top方法都會將資料收集到driver本地,前者是收集全部,後者是收集指定條數。所以最好知道收集的資料集較小時使用。否則會有很大的效能問題,比如大數量的傳輸,以及driver本地的記憶體壓力

6.6 reduce和reduceByKey

前者是action操作,後者是transformation操作

###七、RDD cache優化 RDD的資料,來至於外部儲存介質,比如磁碟。而每一次用該RDD,都要去磁碟載入,這有時間和效能上的損耗。可以使用rdd的cahce方法,將該RDD快取到記憶體,這樣後續重複使用該RDD時,直接去記憶體拿。 cache的幾個級別

  • MEMORY_ONLY 只快取到記憶體,記憶體裝不下的部分,下次用到時再重新計算
  • MEMORY_AND_DISK 快取到記憶體,記憶體裝不下的,快取到磁碟。這樣,下次需要時,不在記憶體部分的資料直接從磁碟獲取就行,不用重新計算
  • MEMORY_ONLY_SER 只快取到記憶體,但為了節約空間,將快取物件序列化後儲存
  • MEMORY_AND_DISK_SER 快取到記憶體,裝不下的資料快取到磁碟,都是用序列化方式儲存
  • DISK_ONLY 只快取到磁碟

7.1 stage

按資料是否在分割槽間遷移,來劃分stage。一個stage有多個task,他們會併發的在不同的分割槽上執行相同的計算程式碼。比如緊鄰的map和filter就會被劃在同一個stage,因為他們可以併發在各分割槽上執行,而不需要資料移動。而reduceByKey則會單獨成為一個stage,因為其涉及到資料移動

八、RDD lineage & DAG

RDD 從一個RDD轉化成另一個RDD時,每一步都會記錄上一個RDD關係。於是這形成一個血統譜系。具體

    val wordCount1 = sc.textFile("InputText").flatMap(_.split("\\s+")).map((_, 1)).reduceByKey(_ + _)
    println(wordCount1.toDebugString)
複製程式碼

最終輸出:

(1) ShuffledRDD[4] at reduceByKey at SparkTest.scala:124 []
 +-(1) MapPartitionsRDD[3] at map at SparkTest.scala:124 []
    |  MapPartitionsRDD[2] at flatMap at SparkTest.scala:124 []
    |  InputText MapPartitionsRDD[1] at textFile at SparkTest.scala:124 []
    |  InputText HadoopRDD[0] at textFile at SparkTest.scala:124 []
複製程式碼

可以看到結果以倒序的方式輸出,有點像java異常時,打出的依賴棧。從最近的依賴點,一直回溯

九、DataFrame

在RDD上進一步封裝的資料結構。這種資料結構可以使用SparkSql去操作處理資料,這降低了對分散式資料集的使用難度。因為你只要會sql,就可以進行一些處理

十、GraphX

###十一、 如何調優 一個Spark應用最會對應多個JVM程式。分散式driver,以及該應用在每個worknode上起的JVM程式,由於driver擔任的協調者角色,實際執行是worknode上的EXECUTOR,所以對於JVM的調優,主要指對Executor的調優。這些JVM程式彼此會通訊,比如資料shuffle。所以優化Spark應用的思路主要從以下個方面入手:

  • 做個一個JVM應用,需要關注JVM的垃圾回收情況,各年齡帶的記憶體分配。這個需要基於具體應用具體分析
  • 由於多個JVM程式之間設計跨網路,跨機器的資料傳輸,那麼需要考慮如何減小傳輸資料量。比如將資料序列化
  • 對於Spark計算框架本身的特點,還有對資料量較大的輸入,採用提高併發度,來切分輸入大小。頻繁使用的RDD,進行快取,減小叢集重複計算載入的開銷。將各分割槽都要用到的公共大變數,提前brodcast到各叢集等

11.1 序列化

通過sparkConf conf.set(“spark.serializer”, “org.apache.spark.serializer.KyroSerializer”)來配置,指定資料物件的序列化方式

十二、參考資料

data-flair.training/blogs/spark…

spark.apache.org/docs/1.6.1/

相關文章