其他更多java基礎文章:
java基礎學習(目錄)
1. RDD概述
RDD 是 Spark 的計算模型。RDD(Resilient Distributed Dataset)叫做彈性的分散式資料集合,是 Spark 中最基本的資料抽象,它代表一個不可變、只讀的,被分割槽的資料集。操作 RDD 就像操作本地集合一樣,有很多的方法可以呼叫,使用方便,而無需關心底層的排程細節。
2. RDD的建立
Spark Core為我們提供了三種建立RDD的方式,包括:
- 使用程式中的集合建立RDD
- 使用HDFS檔案建立RDD
2.1 Spark初始化
spark程式需要做的第一件事情,就是建立一個SparkContext物件,它將告訴spark如何訪問一個叢集,而要建立一個SparkContext物件,你首先要建立一個SparkConf物件,該物件訪問了你的應用程式的資訊,比如下面的程式碼:
SparkConf conf=new SparkConf();
conf.set("引數", "引數值"); //因為jvm無法獲得足夠的資源
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App",conf);
複製程式碼
2.2 使用程式中的集合建立RDD
List<Integer> data = Arrays.asList(1, 2, 3, 4, 5);
//並行集合,是通過對於驅動程式中的集合呼叫JavaSparkContext.parallelize來構建的RDD
JavaRDD<Integer> distData = sc.parallelize(data);
複製程式碼
2.3 使用HDFS檔案建立RDD
//通過hdfs上的檔案定義一個RDD 這個資料暫時還沒有載入到記憶體,也沒有在上面執行動作,lines僅僅指向這個檔案
JavaRDD<String> lines = sc.textFile("hdfs://master:9000/testFile/README.md");
複製程式碼
3. RDD的兩種運算元
RDD支援兩種型別的操作運算元:Transformation與Action。
3.1 Transformation
Transformation操作會由一個RDD生成一個新的 RDD。Transformation操作是延遲計算的,也就是說從一個RDD轉換生成另一個RDD的轉換操作不是馬上執行,需要等到Actions操作時,才真正開始運算。
3.2 Action
Action操作會對 RDD 計算出一個結果,並把結果返回到驅動器程式中,或把結果儲存到外部儲存系統(如 HDFS)中。
例如,first() 就是的一個行動操作,它會返回 RDD 的第一個元素。
result = testlines.first()
複製程式碼
transformations操作和Action操作的區別在於Spark計算RDD 的方式不同。對於在任何時候transformations得到的新的RDD,Spark只會惰性計算。只有在一個Action操作中用到時,才會真正計算。這種策略也是spark效能高的部分原因。
比如,我們讀取一個文字檔案建立一個RDD,然後把其中包含spark的行篩選出來。如果Spark在我們執行lines = sc.textFile(test.txt) 時就把檔案中所有的行都讀取到記憶體中並儲存起來,記憶體開銷會很大,而我們接下來的操作會篩選掉其中的很多資料。相反, 如果Spark 在知道了完整的轉化操作鏈之後,它就可以只計算求結果時真正需要的資料。
事實上,在執行行動操作 first()時,Spark也只是掃描檔案直到找到第一個匹配的行為止,而不是讀取整個檔案。
3.3 RDD常用運算元
4. RDD的持久化機制
RDD還有一個叫持久化的機制,就是在不同操作間,持久化(或快取)一個資料集在記憶體中。當你持久化一個RDD,每一個結點都將把它的計算分塊結果儲存在記憶體中,並在對此資料集(或者衍生出的資料集)進行的其它動作中重用。這將使得後續的動作(action)變得更加迅速。快取是用Spark構建迭代演算法的關鍵。RDD的快取能夠在第一次計算完成後,將計算結果儲存到記憶體、本地檔案系統或者Tachyon(分散式記憶體檔案系統)中。通過快取,Spark避免了RDD上的重複計算,能夠極大地提升計算速度。在Spark應用程式的調優中就會考慮到RDD的持久化的機制。
4.1 RDD持久化機制
- Spark非常重要的一個功能特性就是可以將RDD 持久化在記憶體中,當對RDD執行持久化操作時,每個節點都會將自己操作的RDD的partition持久化到記憶體中,並且在之後對該RDD的反覆使用中,直接使用記憶體快取的partition,這樣的話,對於針對一個RDD反覆執行多個操作的場景,就只要對RDD計算一次即可,後面直接使用該RDD ,而不需要計算多次該RDD
- 要持久化一個RDD,只要呼叫其cache()或者persist()方法即可。但是並不是這兩個方法被呼叫時立即快取,在該RDD第一次被計算出來時(觸發後面的action時),該RDD將會被快取在計算節點的記憶體中,並供後面重用。而且Spark的持久化機制還是自動容錯的,如果持久化的RDD的任何partition丟失了,那麼Spark會自動通過其源RDD,使用transformation操作重新計算該partition。
- cache()和persist()的區別在於,cache()是persist()的一種簡化方式,cache()的底層就是呼叫的persist()的無參版本,同時就是呼叫persist(MEMORY_ONLY),將資料持久化到記憶體中。如果需要從記憶體中去除快取,那麼可以使用unpersist()方法。
4.2 RDD的持久化的級別
順便看一下RDD都有哪些快取級別,檢視 StorageLevel 類的原始碼:
object StorageLevel {
val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(false, false, true, false)
......
}
複製程式碼
檢視其構造引數
class StorageLevel private(
private var _useDisk: Boolean,
private var _useMemory: Boolean,
private var _useOffHeap: Boolean,
private var _deserialized: Boolean,
private var _replication: Int = 1)
extends Externalizable {
......
def useDisk: Boolean = _useDisk
def useMemory: Boolean = _useMemory
def useOffHeap: Boolean = _useOffHeap
def deserialized: Boolean = _deserialized
def replication: Int = _replication
......
}
複製程式碼
可以看到StorageLevel類的主構造器包含了5個引數:
- useDisk:使用硬碟(外存)
- useMemory:使用記憶體
- useOffHeap:使用堆外記憶體,這是Java虛擬機器裡面的概念,堆外記憶體意味著把記憶體物件分配在Java虛擬機器的堆以外的記憶體,這些記憶體直接受作業系統管理(而不是虛擬機器)。這樣做的結果就是能保持一個較小的堆,以減少垃圾收集對應用的影響。
- deserialized:反序列化,其逆過程式列化(Serialization)是java提供的一種機制,將物件表示成一連串的位元組;而反序列化就表示將位元組恢復為物件的過程。序列化是物件永久化的一種機制,可以將物件及其屬性儲存起來,並能在反序列化後直接恢復這個物件。序列化方式儲存物件可以節省磁碟或記憶體的空間,一般 序列化:反序列化=1:3
- replication:備份數(在多個節點上備份)
理解了這5個引數,StorageLevel 的12種快取級別就不難理解了。例如:
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
複製程式碼
就表示使用這種快取級別的RDD將儲存在硬碟以及記憶體中,使用序列化(在硬碟中),並且在多個節點上備份2份(正常的RDD只有一份)
注意: 持久化的單位為Partition
注意: 當使用RDD的MEMORY_ONLY進行持久化的時候,當記憶體空間不夠的時候,不會報OOM,它會選擇最小的partiton來持久化在記憶體,當重新的使用RDD時候,其他的partition會根據依賴關係重新計算
4.3 選擇持久化級別
Spark的多個儲存級別意味著在記憶體利用率和cpu利用效率間的不同權衡。我們推薦通過下面的過程選擇一個合適的儲存級別:
- 如果你的RDD適合預設的儲存級別(MEMORY_ONLY),就選擇預設的儲存級別。因為這是cpu利用率最高的選項,會使RDD上的操作儘可能的快。
- 如果不適合用預設的級別,選擇MEMORY_ONLY_SER。選擇一個更快的序列化庫提高物件的空間使用率,但是仍能夠相當快的訪問。
- 除非函式計算RDD的花費較大或者它們需要過濾大量的資料,不要將RDD儲存到磁碟上,否則,重複計算一個分割槽就會和重磁碟上讀取資料一樣慢。
- 如果你希望更快的錯誤恢復,可以利用重複儲存級別。所有的儲存級別都可以通過重複計算丟失的資料來支援完整的容錯,但是重複的資料能夠使你在RDD上繼續執行任務,而不需要重複計算丟失的資料。
- 在擁有大量記憶體的環境中或者多應用程式的環境中,OFF_HEAP具有如下優勢:
- 它執行多個執行者共享Tachyon中相同的記憶體池
- 它顯著地減少垃圾回收的花費
- 如果單個的執行者崩潰,快取的資料不會丟失
4.4 checkPoint
當業務場景非常的複雜的時候,RDD的lineage(血統)依賴會非常的長,一旦血統較後面的RDD資料丟失的時候,Spark會根據血統依賴重新的計算丟失的RDD,這樣會造成計算的時間過長,Spark提供了一個叫checkPoint的運算元來解決這樣的業務場景。
- 為當前RDD設定檢查點。該函式將會建立一個二進位制的檔案,並儲存到checkpoint目錄中,該目錄是用SparkContext.setCheckpointDir()設定的。在checkpoint的過程中,該RDD的所有依賴於父RDD中的資訊將全部被移出。對RDD進行checkpoint操作並不會馬上被執行,必須執行Action操作才能觸發。
4.4.1 checkPoint優點
- 持久化在HDFS上,HDFS預設的3副本備份使得持久化的備份資料更加的安全
- 切斷RDD的依賴關係:當業務場景複雜的時候,RDD的依賴關係非常的長的時候,當靠後的RDD資料丟失的時候,會經歷較長的重新計算的過程,採用checkPoint會轉為依賴checkPointRDD,可以避免長的lineage重新計算。
4.4.2 checkPoint的原理
- 當finalRDD執行Action類運算元計算job任務的時候,Spark會從finalRDD從後往前回溯檢視哪些RDD使用了checkPoint運算元
- 將使用了checkPoint的運算元標記
- Spark會自動的啟動一個job來重新計算標記了的RDD,並將計算的結果存入HDFS,然後切斷RDD的依賴關係
- 建議在執行checkpoint()方法之前先對rdd進行persisted操作。 在checkPoint的RDD之前先cache RDD,那麼Spark就不用啟動一個job來計算checkPoint的RDD,而是將持久化到記憶體的資料直接拷貝到HDFS,進而提高Spark的計算速度,提高應用程式的效能
5. 共享變數
Spark兩種共享變數:廣播變數(broadcast variable)與累加器(accumulator)
累加器用來對資訊進行聚合,而廣播變數用來高效分發較大的物件。
5.1 廣播變數
有時會變數是在driver端建立的,但是因為需要在excutor端使用,所以driver會把變數以task的形式傳送到excutor端,如果有很多個task,就會有很多給excutor端攜帶很多個變數,如果這個變數非常大的時候,就可能會造成記憶體溢位(如下圖所示)。這個時候就引出了廣播變數。
使用廣播變數後:int factor = 3;
final Broadcast<Integer> factorBroadcast = sc.broadcast(factor);
...
//在RDD計算中
int factor = factorBroadcast.value();
複製程式碼
另外,為了確保所有的節點獲得相同的變數,物件factorBroadcast廣播後只讀不能夠被修改。
注意事項:
- 能不能將一個RDD使用廣播變數廣播出去?
不能,因為RDD是不儲存資料的。可以將RDD的結果廣播出去。 廣播變數只能在Driver端定義,不能在Executor端定義。
5.2 累加器
Spark提供的Accumulator,主要用於多個節點對一個變數進行共享性的操作。Accumulator只提供了累加的功能,但是卻給我們提供了多個task對一個變數並行操作的功能。task只能對Accumulator進行累加操作,不能讀取它的值。只有Driver程式可以讀取Accumulator的值。
final Accumulator<Integer> sum = sc.accumulator(0);
...
//RDD計算中
sum.add(1);
...
//Driver端
System.out.println(sum.value());
複製程式碼
累加器只能由Spark內部進行更新,並保證每個任務在累加器的更新操作僅執行一次,也就是說重啟任務也不應該更新。在轉換操作中,使用者必須意識到任務和作業的排程過程重新執行會造成累加器的多次更新。
累加器同樣具有Spark懶載入的求值模型。如果它們在RDD的操作中進行更新,它們的值只在RDD進行行動操作時才進行更新。