Part 1
1. Spark計算模型
1.1 Spark程序模型
首先通過一個簡單的實例了解Spark的程序模型。
1)SparkContext中的textFile函數從HDFS讀取日志文件,輸出變量file。
valfile=sc.textFile("hdfs://xxx")
2)RDD中的filter函數過濾帶“ERROR”的行,輸出errors(errors也是一個RDD)。
valerrors=file.filter(line=>line.contains("ERROR")
3)RDD的count函數返回“ERROR”的行數:
errors.count()。
其示意圖如下:
從圖中可以看到,RDD由很多partition組成,一個partition對應到物理模塊上算是一個block,由block
manager統一管理。
Spark程序模型的主要思想是RDD(Resilient
Distributed Dataset),把所有計算的數據保存在分布式的內存中。在迭代計算中,通常情況下,都是對同一的數據集做反復的迭代計算,數據保存在內存中,將大大提高性能。RDD就是數據partition方式保存在cluster的內存中。操作有兩種:transformation和action,transform就是把一種RDD轉換為另一個RDD,和Hadoop的map操作很類似,只是定義operator比較豐富(map,join,filter,groupByKey等操作),action就類似于hadoop的reduce,其輸出是一個aggregation函數的值如count,或者是一個集合(collection)。
1.2 彈性分布式數據集(RDD)
RDD是Spark的核心數據結構,通過RDD的依賴關系形成Spark的調度順序。通過對RDD的操作形成整個Spark程序。
1.2.1?RDD的四種創建方式
1)從Hadoop文件系統(或與Hadoop兼容的其他持久化存儲系統,如Hive、Cassandra、Hbase)輸入(如HDFS)創建。
2)從父RDD轉換得到新的RDD。
3)調用SparkContext方法的parallelize,將Driver上的數據集并行化,轉化為分布式的RDD。
4)更改RDD的持久性(persistence),例如cache()函數,默認RDD計算后會在內存中清除。通過cache函數將計算后的RDD緩存在內存中。
1.2.2?RDD的兩種操作算子
對于RDD可以有兩種計算操作算子:Transformation(變換)與Action(行動)。
1)Transformation(變換)。Transformation操作是延遲計算的,也就是說從一個RDD轉換生成另一個RDD的轉換操作不是馬上執行,需要等到有Actions操作時,才真正觸發運算。
2)Action(行動)Action算子會觸發Spark提交作業(Job),并將數據輸出到Spark系統。
1.2.3?RDD的重要內部屬性
1)分區列表。
2)計算每個分片的函數。
3)對父RDD的依賴列表。
4)對Key-Value對數據類型RDD的分區器,控制分區策略和分區數。
5)每個數據分區的地址列表(如HDFS上的數據塊的地址)。
1.2.4?RDD與DSM的對比
通過上述表格可以看到,RDD有更好的容錯性,采用血統機制后,可以不用回滾程序實現容錯。
RDD和DSM對比主要有如下兩個優勢:
1)對于RDD中的批量操作,運行時將根據數據存放的位置來調度任務,從而提高性能。
2)對于掃描類型操作,如果內存不足以緩存整個RDD,就進行部分緩存,將內存容納不下的分區存儲到磁盤上。
1.3 Spark的數據存儲
Spark數據存儲的核心是彈性分布式數據集(RDD)。RDD可以被抽象地理解為一個大的數組(Array),但是這個數組是分布在集群上的。邏輯上RDD的每個分區叫一個Partition。
在Spark的執行過程中,RDD經歷一個個的Transfomation算子之后,最后通過Action算子進行觸發操作。
RDD之間通過Lineage產生依賴關系,這個關系在容錯中有很重要的作用。
RDD會被劃分成很多的分區分布到集群的多個節點中。分區是個邏輯概念,變換前后的新舊分區在物理上可能是同一塊內存存儲。這是很重要的優化,以防止函數式數據不變性導致的內存需求無限擴張。
在上圖中,在物理上,RDD對象實質上是一個元數據結構,存儲著Block、Node等的映射關系,以及其他的元數據信息。
每個Block中存儲著RDD所有數據項的一個子集。
1.4 Spark的算子作用與分類
1.4.1 算子的作用
算子是RDD中定義的函數,可以對RDD中的數據進行轉換和操作。
1)輸入:在Spark程序運行中,數據從外部數據空間(如分布式存儲:textFile讀取HDFS等,parallelize方法輸入Scala集合或數據)輸入Spark,數據進入Spark運行時數據空間,轉化為Spark中的數據塊,通過BlockManager進行管理。
2)運行:在Spark數據輸入形成RDD后便可以通過變換算子,如fliter等,對數據進行操作并將RDD轉化為新的RDD,通過Action算子,觸發Spark提交作業。如果數據需要復用,可以通過Cache算子,將數據緩存到內存。
3)輸出:程序運行結束數據會輸出Spark運行時空間,存儲到分布式存儲中(如saveAsTextFile輸出到HDFS),或Scala數據或集合中(collect輸出到Scala集合,count返回Scala int型數據)。
1.4.2 算子的分類
大致可以分為三大類算子。
1)Value數據類型的Transformation算子,這種變換并不觸發提交作業,針對處理的數據項是Value型的數據。
2)Key-Value數據類型的Transfromation算子,這種變換并不觸發提交作業,針對處理的數據項是Key-Value型的數據對。
3)Action算子,這類算子會觸發SparkContext提交Job作業。
1.4.3 Value型的Transformation算子
這種算子可以根據RDD變換算子的輸入分區與輸出分區關系分為以下幾種類型:
1)輸入分區與輸出分區一對一型。
2)輸入分區與輸出分區多對一型。
3)輸入分區與輸出分區多對多型。
4)輸出分區為輸入分區子集型。
5)還有一種特殊的輸入與輸出分區一對一的算子類型:Cache型。Cache算子對RDD分區進行緩存。
1.4.3.1 輸入分區與輸出分區一對一型:
(1)Map
源碼中的map算子相當于初始化一個RDD,新RDD叫作
MappedRDD(this, sc.clean(f))。
(2)flatMap
將原來RDD中的每個元素通過函數f轉換為新的元素,并將生成的RDD的每個集合中的元素合并為一個集合。內部創建FlatMappedRDD(this, sc.clean(f))。
(3)mapPartitions
mapPartitions函數獲取到每個分區的迭代器,在函數中通過這個分區整體的迭代器對整個分區的元素進行操作。內部實現是生成MapPartitionsRDD。
(4)glom
glom函數將每個分區形成一個數組,內部實現是返回的GlommedRDD。
圖3-7中的每個方框代表一個RDD分區。
1.4.3.2 輸入分區與輸出分區多對一型:
(1)union
使用union函數時需要保證兩個RDD元素的數據類型相同,返回的RDD數據類型和被合并的RDD元素數據類型相同,并不進行去重操作,保存所有元素。如果想去重,可以使用distinct()。++符號相當于union函數操作。
(2)cartesian
對兩個RDD內的所有元素進行笛卡爾積操作。操作后,內部實現返回CartesianRDD。
1.4.3.3 輸入分區與輸出分區多對多型:
groupBy:
將元素通過函數生成相應的Key,數據就轉化為Key-Value格式,之后將Key相同的元素分為一組。
實現:
②sc.clean( )函數將用戶函數預處理:valcleanF = sc.clean(f)
②對數據map進行函數操作,最后再對groupByKey進行分組操作。this.map(t => (cleanF(t), t)).groupByKey(p)
1.4.3.4 輸出分區為輸入分區子集型:
(1)filter
filter的功能是對元素進行過濾,對每個元素應用f函數,返回值為true的元素在RDD中保留,返回為false的將過濾掉。內部實現相當于生成FilteredRDD(this,sc.clean(f))。
deffilter(f:T=>Boolean):RDD[T]=new FilteredRDD(this,sc.clean(f))
(2)distinct
distinct將RDD中的元素進行去重操作。
(3)subtract
subtract相當于進行集合的差操作,RDD 1去除RDD 1和RDD 2交集中的所有元素。
(4)sample
sample將RDD這個集合內的元素進行采樣,獲取所有元素的子集。用戶可以設定是否有放回的抽樣、百分比、隨機種子,進而決定采樣方式。內部實現是生成
SampledRDD(withReplacement,fraction, seed)
函數參數設置如下:
withReplacement=true,表示有放回的抽樣
withReplacement=false,表示無放回的抽樣
(5)takesample
takeSample()函數和上面的sample函數是一個原理,但是不使用相對比例采樣,而是按設定的采樣個數進行采樣,同時返回結果不再是RDD,而是相當于對采樣后的數據進行Collect(),返回結果的集合為單機的數組。
1.4.3.5 Cache型:
(1)cache
cache將RDD元素從磁盤緩存到內存,相當于persist(MEMORY_ONLY)函數的功能。
(2)persist
persist函數對RDD進行緩存操作。數據緩存在哪里由StorageLevel枚舉類型確定。
persist(newLevel:StorageLevel)
part 2
2. Spark I/O機制
2.1 序列化
2.1.1 序列化的含義和目的
含義:序列化是將對象轉換為字節流,本質上可以理解為將鏈表存儲的非連續空間的數據存儲轉化為連續空間存儲的數組中。這樣就可以將數據進行流式傳輸或者塊存儲。相反,反序列化就是將字節流轉化為對象。
目的:進程間通信:不同節點之間進行數據傳輸。
數據持久化存儲到磁盤:本地節點將對象寫入磁盤。
2.1.2 兩種序列化方式對比
Java序列化:在默認情況下,Spark采用Java的ObjectOutputStream序列化一個對象。該方式適用于所有實現了java.io.Serializable的類。Java序列化非常靈活,但是速度較慢,在某些情況下序列化的結果也比較大。
Kryo序列化:Spark也能使用Kryo(版本2)序列化對象。Kryo不但速度極快,而且產生的結果更為緊湊(通常能提高10倍)。Kryo的缺點是不支持所有類型,為了更好的性能,你需要提前注冊程序中所使用的類(class)。
2.2 壓縮
當大片連續區域進行數據存儲并且存儲區域中數據重復性高的狀況下,數據適合進行壓縮。
序列化后的數據可以壓縮,使數據緊縮,減少空間開銷。
2.2.1 兩種壓縮方式對比
Snappy的目標是在合理的壓縮量的情況下,提高壓縮速度,因此壓縮比并不是很高。根據數據集的不同,壓縮比能達到20%~100%。Snappy通常在達到相當壓縮的情況下,要比同類的LZO、LZF、FastLZ和QuickLZ等快速的壓縮算法快,LZF提供了更高的壓縮比。
2.2.2 序列化與壓縮
在分布式計算中,序列化和壓縮是兩個重要的手段。Spark通過序列化將鏈式分布的數據轉化為連續分布的數據,這樣就能夠進行分布式的進程間數據通信,或者在內存進行數據壓縮等操作,提升Spark的應用性能。通過壓縮,能夠減少數據的內存占用,以及IO和網絡數據傳輸開銷。
2.3 Spark塊管理
整體的I/O管理分為以下兩個層次:
1)通信層:I/O模塊也是采用Master-Slave結構來實現通信層的架構,Master和Slave之間傳輸控制信息、狀態信息。
2)存儲層:Spark的塊數據需要存儲到內存或者磁盤,有可能還需傳輸到遠端機器,這些是由存儲層完成的。
2.3.1 實體與類
如圖中所示,在Storage模塊中,根據層次劃分有如下模塊:
(1)管理和接口
BlockManager:當其他模塊要和storage模塊進行交互時,storage模塊提供了統一的操作類BlockManager,外部類與storage模塊打交道都需要調用BlockManager相應接口來實現。
(2)通信層
·BlockManagerMasterActor:從主節點創建,從節點通過這個Actor的引用向主節點傳遞消息和狀態。
·BlockManagerSlaveActor:在從節點創建,主節點通過這個Actor的引用向從節點傳遞命令,控制從節點的塊讀寫。
·BlockManagerMaster:對Actor通信進行管理。
(3)數據讀寫層
·DiskStore:提供Block在磁盤上以文件形式讀寫的功能。
·MemoryStore:提供Block在內存中的Block讀寫功能。
·ConnectionManager:提供本地機器和遠端節點進行網絡傳輸Block的功能。
·BlockManagerWorker:對遠端數據的異步傳輸進行管理。
整體的數據存儲通信仍相當于Master-Slave模型,節點之間傳遞消息和狀態,Master節點負責總體控制,Slave節點接收命令、匯報狀態。
2.3.2 讀寫流程
(1)數據寫入
1)RDD調用compute()方法進行指定分區的寫入。
2)CacheManager中調用BlockManager判斷數據是否已經寫入,如果未寫則寫入。
3)BlockManager中數據與其他節點同步。
4)BlockManager根據存儲級別寫入指定的存儲層。
5)BlockManager向主節點匯報存儲狀態
(2)數據讀取
在RDD類中,通過compute方法調用iterator讀寫某個分區(Partition),作為數據讀取的入口。分區是邏輯概念,在物理上是一個數據塊(block)。
(3)讀取邏輯
通過BlockManager讀取代碼進入讀取邏輯
1)本地讀取。
在本地同步讀取數據塊,首先看能否在內存讀取數據塊,如果不能讀取,則看能否從Tachyon讀取數據塊,如果仍不能讀取,則看能否從磁盤讀取數據塊。
2)遠程讀取。
遠程獲取調用路徑,然后getRemote調用doGetRemote,通過BlockManagerWorker.syncGetBlock從遠程獲取數據。
其中Tachyon是一個分布式內存文件系統,可以在集群里以訪問內存的速度來訪問存在tachyon里的文件。把Tachyon是架構在最底層的分布式文件存儲和上層的各種計算框架之間的一種中間件。主要職責是將那些不需要落地到DFS里的文件,落地到分布式內存文件系統中,來達到共享內存,從而提高效率。同時可以減少內存冗余,GC時間等。
在BlockManagerWorker中調用syncGetBlock獲取遠端數據塊,這里使用了Future模型。Future本身是一種被廣泛運用的并發設計模式,可在很大程度上簡化需要數據流同步的并發應用開發。
該模型是將異步請求和代理模式聯合的模型產物。
客戶端發送一個長時間的請求,服務端不需等待該數據處理完成便立即返回一個偽造的代理數據(相當于商品訂單,不是商品本身),用戶也無需等待,先去執行其他的若干操作后,再去調用服務器已經完成組裝的真實數據。該模型充分利用了等待的時間片段。
2.3.3 數據塊讀寫管理
數據塊的讀寫,如果在本地內存存在所需數據塊,則先從本地內存讀取,如果不存在,則看本地的磁盤是否有數據,如果仍不存在,再看網絡中其他節點上是否有數據,即數據有3個類別的讀寫來源。
(1)MemoryStore內存塊讀寫
進行塊讀寫是線程間同步的。通過entries.synchronized控制多線程并發讀寫,防止出現異常。
PutBlock對象用來確保只有一個線程寫入數據塊。這樣確保數據讀寫且線程安全的。示例代碼如下:
Private val putLock = new Object()
內存Block塊管理是通過鏈表來實現的
(2)DiskStore磁盤塊寫入
在DiskStore中,一個Block對應一個文件。在diskManager中,存儲blockId和一個文件路徑映射。數據塊的讀寫入相當于讀寫文件流。