Spark--RDD詳解

RDD容錯機制

分佈式系統通常在一個機器集羣上運行,同時運行的幾百臺機器中某些出問題的概率大大增加,所以容錯設計是分佈式系統的一個重要能力。

Spark以前的集羣容錯處理模型,像MapReduce,將計算轉換爲一個有向無環圖(DAG)的任務集合,這樣可以通過重複執行DAG裏的一部分任務來完成容錯恢復。但是由於主要的數據存儲在分佈式文件系統中,沒有提供其他存儲的概念,容錯過程需要在網絡上進行數據複製,從而增加了大量的消耗。所以,分佈式編程中經常需要做檢查點,即將某個時機的中間數據寫到存儲(通常是分佈式文件系統)中。

RDD也是一個DAG,每一個RDD都會記住創建該數據集需要哪些操作,跟蹤記錄RDD的繼承關係,這個關係在Spark裏面叫lineage(血緣關係)。當一個RDD的某個分區丟失時,RDD是有足夠的信息記錄其如何通過其他RDD進行計算,且只需重新計算該分區,這是Spark的一個創新。

RDD的緩存

概述
相比Hadoop MapReduce來說,Spark計算具有巨大的性能優勢,其中很大一部分原因是Spark對於內存的充分利用,以及提供的緩存機制。

RDD持久化(緩存)
持久化在早期被稱作緩存(cache),但緩存一般指將內容放在內存中。雖然持久化操作在絕大部分情況下都是將RDD緩存在內存中,但一般都會在內存不夠時用磁盤頂上去(比操作系統默認的磁盤交換性能高很多)。當然,也可以選擇不使用內存,而是僅僅保存到磁盤中。所以,現在Spark使用持久化(persistence)這一更廣泛的名稱。

如果一個RDD不止一次被用到,那麼就可以持久化它,這樣可以大幅提升程序的性能,甚至達10倍以上。

默認情況下,RDD只使用一次,用完即扔,再次使用時需要重新計算得到,而持久化操作避免了這裏的重複計算,實際測試也顯示持久化對性能提升明顯,這也是Spark剛出現時被人稱爲內存計算框架的原因。

假設首先進行了RDD0→RDD1→RDD2的計算作業,那麼計算結束時,RDD1就已經緩存在系統中了。在進行RDD0→RDD1→RDD3的計算作業時,由於RDD1已經緩存在系統中,因此RDD0→RDD1的轉換不會重複進行,計算作業只須進行RDD1→RDD3的計算就可以了,因此計算速度可以得到很大提升。

在這裏插入圖片描述

持久化的方法是調用persist()函數,除了持久化至內存中,還可以在persist()中指定storage level參數使用其他的類型,具體如下:

1)MEMORY_ONLY : 將 RDD 以反序列化的 Java 對象的形式存儲在 JVM 中. 如果內存空間不夠,部分數據分區將不會被緩存,在每次需要用到這些數據時重新進行計算. 這是默認的級別。

cache()方法對應的級別就是MEMORY_ONLY級別

2)MEMORY_AND_DISK:將 RDD 以反序列化的 Java 對象的形式存儲在 JVM 中。
如果內存空間不夠,將未緩存的數據分區存儲到磁盤,在需要使用這些分區時從磁盤讀取。

3)MEMORY_ONLY_SER :將 RDD 以序列化的 Java 對象的形式進行存儲(每個分區爲一個 byte 數組)。這種方式會比反序列化對象的方式節省很多空間,尤其是在使用 fast serialize時會節省更多的空間,但是在讀取時會使得 CPU 的 read 變得更加密集。如果內存空間不夠,部分數據分區將不會被緩存,在每次需要用到這些數據時重新進行計算。

4)MEMORY_AND_DISK_SER :類似於 MEMORY_ONLY_SER ,但是溢出的分區會存儲到磁盤,而不是在用到它們時重新計算。如果內存空間不夠,將未緩存的數據分區存儲到磁盤,在需要使用這些分區時從磁盤讀取。

5)DISK_ONLY:只在磁盤上緩存 RDD。

6)MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. :與上面的級別功能相同,
只不過每個分區在集羣中兩個節點上建立副本。

7)OFF_HEAP 將數據存儲在 off-heap memory 中。使用堆外內存,這是Java虛擬機裏面的概念,堆外內存意味着把內存對象分配在Java虛擬機的堆以外的內存,這些內存直接受操作系統管理(而不是虛擬機)。使用堆外內存的好處:可能會利用到更大的內存存儲空間。但是對於數據的垃圾回收會有影響,需要程序員來處理

注意,可能帶來一些GC回收問題。

Spark 也會自動持久化一些在 shuffle 操作過程中產生的臨時數據(比如 reduceByKey),即便是用戶並沒有調用持久化的方法。這樣做可以避免當 shuffle 階段時如果一個節點掛掉了就得重新計算整個數據的問題。如果用戶打算多次重複使用這些數據,我們仍然建議用戶自己調用持久化方法對數據進行持久化。

使用緩存

scala> import org.apache.spark.storage._
scala> val rdd1=sc.makeRDD(1 to 5)
scala> rdd1.cache //cache只有一種默認的緩存級別,即MEMORY_ONLY
scala> rdd1.persist(StorageLevel.MEMORY_ONLY)

緩存數據的清除
Spark 會自動監控每個節點上的緩存數據,然後使用 least-recently-used (LRU) 機制來處理舊的緩存數據。如果你想手動清理這些緩存的 RDD 數據而不是去等待它們被自動清理掉,
可以使用 RDD.unpersist( ) 方法。

RDD緩存機制的作用:

當把某個分區的數據緩存之後,以後如果需要 重複用到此RDD數據,不需要重新計算,提高性能

從數據容錯角度來看,當一個DAG的計算鏈很長時,比如有100個RDD,可以選擇在RDD10,RDD20,RDD30.。。做一次緩存,可以避免某個分區數據丟失,倒是整個計算鏈重新計算。

Checkpoint機制

checkpoint的意思就是建立檢查點,類似於快照,例如在spark計算裏面 計算流程DAG特別長,服務器需要將整個DAG計算完成得出結果,但是如果在這很長的計算流程中突然中間算出的數據丟失了,spark又會根據RDD的依賴關係從頭到尾計算一遍,這樣子就很費性能,當然我們可以將中間的計算結果通過cache或者persist放到內存或者磁盤中,但是這樣也不能保證數據完全不會丟失,存儲的這個內存出問題了或者磁盤壞了,也會導致spark從頭再根據RDD計算一遍,所以就有了checkpoint,其中checkpoint的作用就是將DAG中比較重要的中間數據做一個檢查點將結果存儲到一個高可用的地方

代碼示例:

object Driver2 {

def main(args: Array[String]): Unit = {

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

val sc=new SparkContext(conf)
sc.setCheckpointDir("hdfs://hadoop01:9000/check01")

val data=sc.textFile("d://data/word.txt")
data.cache()
data.checkpoint()

val wordcount=data.flatMap {_.split(" ")}.map {(_,1)}.reduceByKey(_+_)

wordcount.cache()
wordcount.checkpoint()

wordcount.foreach{println}

}
}

總結:Spark的CheckPoint機制很重要,也很常用,尤其在機器學習中的一些迭代算法中很常見。比如一個算法迭代10000次,如果不適用緩衝機制,如果某分區數據丟失,會導致整個計算鏈重新計算,所以引入緩存機制。但是光引入緩存,也不完全可靠,比如緩存丟失或緩存存儲不下,也會導致重新計算,所以使用CheckPoint機制再做一層保證。
補充:檢查目錄的路徑,一般都是設置到HDFS上

Spark共享變量

Spark程序的大部分操作都是RDD操作,通過傳入函數給RDD操作函數來計算。這些函數在不同的節點上併發執行,但每個內部的變量有不同的作用域,不能相互訪問,所以有時會不太方便,Spark提供了兩類共享變量供編程使用——廣播變量和計數器。

  1. 廣播變量
    這是一個只讀對象,在所有節點上都有一份緩存,創建方法是SparkContext.broadcast(),比如:
    scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
    broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)
    scala> broadcastVar.value
    res0: Array[Int] = Array(1, 2, 3)
    注意,廣播變量是隻讀的,所以創建之後再更新它的值是沒有意義的,一般用val修飾符來定義廣播變量。

  2. 計數器
    計數器只能增加,是共享變量,用於計數或求和。
    計數器變量的創建方法是SparkContext.accumulator(v, name),其中v是初始值,name是名稱。

示例如下:
scala> val accum = sc.accumulator(0, “My Accumulator”)
accum: org.apache.spark.Accumulator[Int] = 0
scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum += x)
scala> accum.value
res1: Int = 10

spark解決數據傾斜問題

文件1
id name
1 tom
2 rose

文件2
id school sno
1 s1 211
2 s2 222
3 s3 233
4 s2 244
期望得到的數據 :
1 tom s1
2 rose s2

將少量的數據轉化爲Map進行廣播,廣播會將此 Map 發送到每個節點中,如果不進行廣播,每個task執行時都會去獲取該Map數據,造成了性能浪費。

完整代碼
import org.apache.spark.{SparkContext, SparkConf}
import scala.collection.mutable.ArrayBuffer

object joinTest extends App{

val conf = new SparkConf().setMaster(“local[2]”).setAppName(“test”)
val sc = new SparkContext(conf)

/**

  • map-side-join
  • 取出小表中出現的用戶與大表關聯後取出所需要的信息
  • */
    val people_info = sc.parallelize(Array((“1”,“tom”),(“2”,“rose”))).collectAsMap()
    val student_all = sc.parallelize(Array((“1”,“s1”,“211”),
    (“1”,“s2”,“222”),
    (“1”,“s3”,“233”),
    (“1”,“s2”,“244”)))

//將需要關聯的小表進行廣播
val people_bc = sc.broadcast(people_info)

/**

  • 使用mapPartition而不是用map,減少創建broadCastMap.value的空間消耗
  • 同時匹配不到的數據也不需要返回()
  • */
    val res = student_all.mapPartitions(iter =>{
    //獲取小表的數據
    val stuMap = people_bc.value
    val arrayBuffer = ArrayBuffer(String,String,String)
    //做兩表的操作
    iter.foreach{case (idCard,school,sno) =>{
    if(stuMap.contains(idCard)){
    arrayBuffer.+= ((idCard, stuMap.getOrElse(idCard,""),school))
    }
    }}
    arrayBuffer.iterator
    })

在這裏插入圖片描述

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