1.RDD 概述
1.1 什麼是 RDD ?
RDD(Resilient Distributed Dataset) 叫著 彈性分散式資料集 ,是Spark 中最基本的抽象,它代表一個不可變、可分割槽、裡面元素可以平行計算的集合。
RDD 具有資料流模型特點:自動容錯、位置感知性排程和可伸縮。
RDD 允許使用者在執行多個查詢時,顯示地將工作集快取在記憶體中,後續的查詢能夠重用工作集,這將會極大的提升查詢的效率。
我們可以認為 RDD 就是一個代理,我們操作這個代理就像操作本地集合一樣,不需去關心任務排程、容錯等問題。
1.2 RDD 的屬性
在 RDD 原始碼中這樣來描述 RDD
* - A list of partitions
* - A function for computing each split
* - A list of dependencies on other RDDs
* - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
* - Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
複製程式碼
- 一組分片(Partition),即資料集的基本組成單位。 對於RDD來說,每個分片都會被一個計算任務處理,並決定平行計算的粒度。使用者可以在建立RDD 的時候指定RDD的分片個數,如果沒有指定,那麼就會採用預設值。預設值就是程式所分配到的CPU Cores 的數目;
- 對於RDD來說,每個分片都會被一個計算任務處理,並決定平行計算的粒度。使用者可以在建立RDD 的時候指定RDD的分片個數,如果沒有指定,那麼就會採用預設值。預設值就是程式所分配到的CPU Cores 的數目;
- RDD 之間互相存在依賴關係。 RDD 的每次轉換都會生成一個新的 RDD ,所以 RDD 之前就會形成類似於流水線一樣的前後依賴關係。在部分分割槽資料丟失時,Spark 可以通過這個依賴關係重新計算丟失部分的分割槽資料,而不是對 RDD 的所有分割槽進行重新計算。
- 一個Partitioner ,即 RDD 的分片函式 。當前Spark 中實現了兩種型別的分片函式,一個是基於雜湊的 HashPartitioner ,另外一個是基於範圍的 RangePartitioner。只有對於key-value的RDD ,才會有 Partitioner,非 key-value 的RDD 的 Partitioner 的值是None。Partitioner 函式不但決定了RDD 本身的分片數量,也決定了 Parent RDD Shuffle 輸出時的分片數量。
- 一個列表,儲存存取每個Partition 的優先位置(preferred location)。 對於一個HDFS 檔案來說,這個列表儲存的就是每個 Partition 所在的塊位置。安裝“移動資料不如移動計算”的理念,Spark 在進行任務排程的時候,會盡可能地將計算任務分配到其所要處理資料塊的儲存位置。
2 建立 RDD
2.1 由一個存在的 Scala 集合進行建立
#通過並行化scala集合建立RDD,一般在測試的時候使用
scala> var rdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24
複製程式碼
2.2 由外部的儲存系統的資料集建立,包括本地的檔案系統,還有所有 Hadoop 支援的資料集,比如 HDFS、Cassandra、Hbase
var rdd1 = sc.textFile("/root/words.txt")
var rdd2 = sc.textFile("hdfs:192.168.80.131:9000/words.text")
複製程式碼
2.3 呼叫一個已經存在了的RDD 的 Transformation,會生成一個新的 RDD。
3 RDD 的程式設計 API
3.1 Transformation
這種 RDD 中的所有轉換都是延遲載入的,也就是說,他們並不會直接就計算結果。相反的,他們只是記住這些應用到基礎資料集(例如一個檔案)上的轉換動作。只有當發生一個返回結果的 Driver 的動作時,這些操作才會真正的執行。這種設計會讓Spark 更加有效率的執行。
常用的 Transformation 操作:
轉換 | 含義 |
---|---|
map(func) | 返回一個新的RDD,該RDD由每一個輸入元素經過func函式轉換後組成 |
filter(func) | 返回一個新的RDD,該RDD由經過func函式計算後返回值為true的輸入元素組成 |
flatMap(func) | 類似於map,但是每一個輸入元素可以被對映為0或多個輸出元素(所以func應該返回一個序列,而不是單一元素) |
mapPartitions(func) | 類似於map,但獨立地在RDD的每一個分片上執行,因此在型別為T的RDD上執行時,func的函式型別必須是Iterator[T] => Iterator[U] |
mapPartitionsWithIndex(func) | 類似於mapPartitions,但func帶有一個整數參數列示分片的索引值,因此在型別為T的RDD上執行時,func的函式型別必須是(Int, Interator[T]) => Iterator[U] |
sample(withReplacement, fraction, seed) | 根據fraction指定的比例對資料進行取樣,可以選擇是否使用隨機數進行替換,seed用於指定隨機數生成器種子 |
union(otherDataset) | 對源RDD和引數RDD求並集後返回一個新的RDD |
intersection(otherDataset) | 對源RDD和引數RDD求交集後返回一個新的RDD |
distinct([numTasks])) | 對源RDD進行去重後返回一個新的RDD |
groupByKey([numTasks]) | 在一個(K,V)的RDD上呼叫,返回一個(K, Iterator[V])的RDD |
reduceByKey(func, [numTasks]) | 在一個(K,V)的RDD上呼叫,返回一個(K,V)的RDD,使用指定的reduce函式,將相同key的值聚合到一起,與groupByKey類似,reduce任務的個數可以通過第二個可選的引數來設定 |
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) | 先按分割槽聚合 再總的聚合 每次要跟初始值交流 例如:aggregateByKey(0)(+,+) 對k/y的RDD進行操作 |
sortByKey([ascending], [numTasks]) | 在一個(K,V)的RDD上呼叫,K必須實現Ordered介面,返回一個按照key進行排序的(K,V)的RDD |
sortBy(func,[ascending], [numTasks]) | 與sortByKey類似,但是更靈活 |
join(otherDataset, [numTasks]) | 在型別為(K,V)和(K,W)的RDD上呼叫,返回一個相同key對應的所有元素對在一起的(K,(V,W))的RDD |
cogroup(otherDataset, [numTasks]) | 在型別為(K,V)和(K,W)的RDD上呼叫,返回一個(K,(Iterable,Iterable))型別的RDD |
cartesian(otherDataset) | 笛卡爾積 |
pipe(command, [envVars]) | 呼叫外部程式 |
coalesce(numPartitions) | 重新分割槽 第一個引數是要分多少區,第二個引數是否shuffle 預設false ;少分割槽變多分割槽 true ; 多分割槽變少分割槽 false |
repartition(numPartitions) | 重新分割槽 必須shuffle 引數是要分多少區 少變多 |
repartitionAndSortWithinPartitions(partitioner) | 重新分割槽+排序 比先分割槽再排序效率高 對K/V的RDD進行操作 |
3.2 Action
觸發程式碼的執行操作,我們一個Spark 應用,至少需要一個 Action 操作。
動作 | 含義 |
---|---|
reduce(func) | 通過func函式聚集RDD中的所有元素,這個功能必須是課交換且可並聯的 |
collect() | 在驅動程式中,以陣列的形式返回資料集的所有元素 |
count() | 返回RDD的元素個數 |
first() | 返回RDD的第一個元素(類似於take(1)) |
take(n) | 返回一個由資料集的前n個元素組成的陣列 |
takeSample(withReplacement,num, [seed]) | 返回一個陣列,該陣列由從資料集中隨機取樣的num個元素組成,可以選擇是否用隨機數替換不足的部分,seed用於指定隨機數生成器種子 |
takeOrdered(n, [ordering]) | |
saveAsTextFile(path) | 將資料集的元素以textfile的形式儲存到HDFS檔案系統或者其他支援的檔案系統,對於每個元素,Spark將會呼叫toString方法,將它裝換為檔案中的文字 |
saveAsSequenceFile(path) | 將資料集中的元素以Hadoop sequencefile的格式儲存到指定的目錄下,可以使HDFS或者其他Hadoop支援的檔案系統。 |
saveAsObjectFile(path) | |
countByKey() | 針對(K,V)型別的RDD,返回一個(K,Int)的map,表示每一個key對應的元素個數。 |
foreach(func) | 在資料集的每一個元素上,執行函式func進行更新。 |
foreachPartition(func) | 在每個分割槽上,執行函式 func |
3.3 Spark WordCount 程式碼示例
執行流程圖:
pom.xml 依賴
<!-- 匯入scala的依賴 -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 匯入spark的依賴 -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 指定hadoop-client API的版本 -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.6.0</version>
</dependency>
複製程式碼
scala 版本程式碼實現:
package com.zhouq.spark
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* scala 版本實現 wc
*
*/
object ScalaWordCount {
def main(args: Array[String]): Unit = {
//這行程式碼是因為我在windows 上直接跑,需要去讀取 hadoop 上的檔案,設定我的使用者名稱。如果是linux 環境可以不設定。視情況而定
System.setProperty("HADOOP_USER_NAME", "root")
//建立spark 配置,設定應用程式名字
// val conf = new SparkConf().setAppName("scalaWordCount")
val conf = new SparkConf().setAppName("scalaWordCount").setMaster("local[4]")
// conf.set("spark.testing.memory","102457600")
//建立spark 執行的入口
val sc = new SparkContext(conf)
//指定以後從哪裡讀取資料建立RDD (彈性分散式資料集)
//取到一行資料
val lines: RDD[String] = sc.textFile(args(0))
//切分壓平
val words: RDD[String] = lines.flatMap(_.split(" "))
//按單詞和一組合
val wordAndOne: RDD[(String, Int)] = words.map((_, 1))
//按key 進行聚合
val reduced: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _)
// 排序, false 表示倒序
val sorted = reduced.sortBy(_._2, false)
//將結果儲存到hdfs中
sorted.saveAsTextFile(args(1))
//釋放資源
sc.stop()
}
}
複製程式碼
Java7 版本:
package com.zhouq.spark;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.PairFunction;
import scala.Tuple2;
import java.util.Arrays;
import java.util.Iterator;
/**
* Java 版WordCount
*/
public class JavaWordCount {
public static void main(String[] args) {
SparkConf conf = new SparkConf().setAppName("JavaWordCount");
//建立SparkContext
JavaSparkContext jsc = new JavaSparkContext(conf);
//指定讀取資料的位置
JavaRDD<String> lines = jsc.textFile(args[0]);
//切分壓平
JavaRDD<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
@Override
public Iterator<String> call(String line) throws Exception{
return Arrays.asList(line.split(" ")).iterator();
}
});
//將單詞進行組合 (a,1),(b,1),(c,1),(a,1)
JavaPairRDD<String, Integer> wordAndOne = words.mapToPair(new PairFunction<String, String, Integer>() {
@Override
public Tuple2<String, Integer> call(String tp) throws Exception {
return new Tuple2<>(tp, 1);
}
});
//先交換再排序,因為 只有groupByKey 方法
JavaPairRDD<Integer, String> swaped = wordAndOne.mapToPair(new PairFunction<Tuple2<String, Integer>, Integer, String>() {
@Override
public Tuple2<Integer, String> call(Tuple2<String, Integer> tp) throws Exception {
// return new Tuple2<>(tp._2, tp._1);
return tp.swap();
}
});
//排序
JavaPairRDD<Integer, String> sorted = swaped.sortByKey(false);
//再次交換順序
JavaPairRDD<String, Integer> result = sorted.mapToPair(new PairFunction<Tuple2<Integer, String>, String, Integer>() {
@Override
public Tuple2<String, Integer> call(Tuple2<Integer, String> tp) throws Exception {
return tp.swap();
}
});
//輸出到hdfs
result.saveAsTextFile(args[1]);
jsc.stop();
}
}
複製程式碼
Java8 版本:
package com.zhouq.spark;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.PairFunction;
import scala.Tuple2;
import java.util.Arrays;
/**
* Java Lambda 表示式版本的 WordCount
*/
public class JavaLambdaWordCount {
public static void main(String[] args) {
SparkConf conf = new SparkConf().setAppName("JavaWordCount");
//建立SparkContext
JavaSparkContext jsc = new JavaSparkContext(conf);
//指定讀取資料的位置
JavaRDD<String> lines = jsc.textFile(args[0]);
//切分壓平
// lines.flatMap(line -> Arrays.asList(line.split(" ")).iterator());
JavaRDD<String> words = lines.flatMap((FlatMapFunction<String, String>) line -> Arrays.asList(line.split(" ")).iterator());
//將單詞進行組合 (a,1),(b,1),(c,1),(a,1)
// words.mapToPair(tp -> new Tuple2<>(tp,1));
JavaPairRDD<String, Integer> wordAndOne = words.mapToPair((PairFunction<String, String, Integer>) tp -> new Tuple2<>(tp, 1));
//先交換再排序,因為 只有groupByKey 方法
// swaped.mapToPair(tp -> tp.swap());
JavaPairRDD<Integer, String> swaped = wordAndOne.mapToPair((PairFunction<Tuple2<String, Integer>, Integer, String>) tp -> {
// return new Tuple2<>(tp._2, tp._1);
return tp.swap();
});
//排序
JavaPairRDD<Integer, String> sorted = swaped.sortByKey(false);
//再次交換順序
// sorted.mapToPair(tp -> tp.swap());
JavaPairRDD<String, Integer> result = sorted.mapToPair((PairFunction<Tuple2<Integer, String>, String, Integer>) tp -> tp.swap());
//輸出到hdfs
result.saveAsTextFile(args[1]);
jsc.stop();
}
}
複製程式碼
4 RDD 的依賴關係
RDD 和它依賴的 父 RDD(可能有多個) 的關係有兩種不同的型別,即 窄依賴(narrow dependency)和寬依賴(wide dependency)。
窄依賴:窄依賴指的是每一個父 RDD 的 Partition 最多被子 RDD 的一個分割槽使用。可以比喻為獨生子女。 寬依賴:寬依賴是多個字 RDD 的Partition 會依賴同一個父 RDD 的 Partition
5 RDD 的持久化
5.1 RDD 的 cache(持久化)
Spark中最重要的功能之一是跨操作在記憶體中持久化(或快取)資料集。當您持久儲存RDD時,每個節點都會儲存它在記憶體中計算的任何分割槽,並在該資料集(或從中派生的資料集)的其他操作中重用它們。這使得未來的行動更快(通常超過10倍)。快取是迭代演算法和快速互動使用的關鍵工具。
您可以使用persist()或cache()方法標記要保留的RDD 。第一次在動作中計算它,它將保留在節點的記憶體中。Spark的快取是容錯的 - 如果丟失了RDD的任何分割槽,它將使用最初建立它的轉換自動重新計算。
5.2 什麼時候我們需要持久化?
- 要求的計算速度快
- 叢集的資源要足夠大
- 重要: cache 的資料會多次觸發Action
- 建議先進行資料過濾,然後將縮小範圍後的資料再cache 到記憶體中.
5.3 如何使用
使用 rdd.persist()或者rdd.cache()
val lines: RDD[String] = sc.textFile("hdfs://xxx/user/accrss")
//使用cache 方法來快取資料到記憶體
val cache = lines.cache()
//注意檢視下面兩次count 的時間
cached.count
cached.count
複製程式碼
5.4 資料快取的儲存級別 StorageLevel
我們在 StorageLevel.scala 原始碼中可以看到:
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(true, true, true, false, 1)
複製程式碼
解釋一下各個引數的意思:
第一個參數列示: 放到磁碟 第二個參數列示: 放到記憶體 第三個參數列示: 磁碟中的資料是否以Java 物件的方式儲存,true 表示是, false表示以序列化的方式存放 第四個參數列示: 記憶體中的資料是否以Java 物件的方式儲存,true 表示是, false表示以序列化的方式存放 第五個參數列示: 存放幾份資料(目的是為了怕executor 出現故障導致分割槽資料丟失,當重新分配任務時,去另外的機器讀取備份資料進行重新計算)
OFF_HEAP : 堆外記憶體,以序列化的格式儲存RDD到Tachyon(一個分散式記憶體儲存系統)中
5.5 如何選擇儲存級別
Spark的多個儲存級別意味著在記憶體利用率和cpu利用效率間的不同權衡。我們推薦通過下面的過程選擇一個合適的儲存級別:
- 如果你的RDD適合預設的儲存級別(MEMORY_ONLY),就選擇預設的儲存級別。因為這是cpu利用率最高的選項,會使RDD上的操作儘可能的快。
- 如果不適合用預設的級別,選擇MEMORY_ONLY_SER。選擇一個更快的序列化庫提高物件的空間使用率,但是仍能夠相當快的訪問。
- 除非函式計算RDD的花費較大或者它們需要過濾大量的資料,不要將RDD儲存到磁碟上,否則,重複計算一個分割槽就會和重磁碟上讀取資料一樣慢。
- 如果你希望更快的錯誤恢復,可以利用重複(replicated)儲存級別。所有的儲存級別都可以通過重複計算丟失的資料來支援完整的容錯,但是重複的資料能夠使你在RDD上繼續執行任務,而不需要重複計算丟失的資料。
- 在擁有大量記憶體的環境中或者多應用程式的環境中,OFF_HEAP具有如下優勢:
- 它執行多個執行者共享Tachyon中相同的記憶體池
- 它顯著地減少垃圾回收的花費
- 如果單個的執行者崩潰,快取的資料不會丟失
5.6 刪除 cache
Spark自動的監控每個節點快取的使用情況,利用最近最少使用原則刪除老舊的資料。如果你想手動的刪除RDD,可以使用 RDD.unpersist()方法
5.7 RDD 的 checkpoint機制
我們除了把資料快取到記憶體中,還可以把資料快取到HDFS 中,保證中間資料不丟失.
什麼時候我們需要做chechpoint?
- 做複雜的迭代計算,要求保證資料安全,不丟失
- 對速度要求不高(跟 cache 到記憶體進行對比)
- 將中間結果儲存到 hdfs 中
怎麼做 checkpoint ?
首先設定 checkpoint 目錄,然後再執行計算邏輯,再執行 checkpoint() 操作。
下面程式碼使用cache 和 checkpoint 兩種方式實現計算每門課最受歡迎老師的 topN
package com.zhouq.spark
import java.net.URL
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* 求每門課程最受歡迎老師TopN --2
* -- 使用cache
* -- 使用checkpoint 一般設定hdfs 目錄
*/
object GroupFavTeacher2_cache_checkpoint {
def main(args: Array[String]): Unit = {
//前 N
val topN = args(1).toInt
//學科集合
val subjects = Array("bigdata", "javaee", "php")
val conf = new SparkConf().setAppName("FavTeacher").setMaster("local[4]")
//建立spark 執行入口
val sc = new SparkContext(conf)
//checkpoint 得先設定 sc 的checkpoint 的dir
// sc.setCheckpointDir("hdfs://hdfs://hadoop1:8020/user/root/ck20190215")
//指定讀取資料
val lines: RDD[String] = sc.textFile(args(0))
val subjectTeacherAndOne: RDD[((String, String), Int)] = lines.map(line => {
val index = line.lastIndexOf("/")
var teacher = line.substring(index + 1)
var httpHost = line.substring(0, index)
var subject = new URL(httpHost).getHost.split("[.]")(0)
((subject, teacher), 1)
})
//將學科,老師聯合當做key
val reduced: RDD[((String, String), Int)] = subjectTeacherAndOne.reduceByKey(_ + _)
//第一種使用cache RDD 把資料快取在記憶體中.標記為cache 的RDD 以後被反覆使用,才使用cache
val cached: RDD[((String, String), Int)] = reduced.cache()
//第二種 使用checkpoint,得先設定 sc 的 checkpointDir
// val cached: RDD[((String, String), Int)] = reduced.checkpoint()
/**
* 先對學科進行過濾,然後再進行排序,呼叫RDD 的sortBy進行排序,避免scala 的排序當資料量大時,記憶體不足的情況.
* take 是Action 操作,每次take 都會進行一次任務提交,具體檢視日誌列印情況
*/
for (sub <- subjects) {
//過濾出當前的學科
val filtered: RDD[((String, String), Int)] = cached.filter(_._1._1 == sub)
//使用RDD 的 sortBy ,記憶體+磁碟排序,避免scala 中的排序因記憶體不足導致異常情況.
//take 是Action 的,所以每次迴圈都會觸發一次提交任務,祥見日誌列印情況
val favTeacher: Array[((String, String), Int)] = filtered.sortBy(_._2, false).take(topN)
println(favTeacher.toBuffer)
}
/**
* 前面cache的資料已經計算完了,後面還有很多其他的指標要計算
* 後面計算的指標也要觸發很多次Action,最好將資料快取到記憶體
* 原來的資料佔用著記憶體,把原來的資料釋放掉,才能快取新的資料
*/
//把原來快取的資料釋放掉
cached.unpersist(true)
sc.stop()
}
}
複製程式碼
6 DAG 的生成
DAG(Directed Acyclic Graph)叫做有向無環圖,原始的RDD通過一系列的轉換就就形成了DAG,根據RDD之間的依賴關係的不同將DAG劃分成不同的Stage,對於窄依賴,partition的轉換處理在Stage中完成計算。對於寬依賴,由於有Shuffle的存在,只能在parent RDD處理完成後,才能開始接下來的計算,因此寬依賴是劃分Stage的依據。
微信公眾號文章連結:Spark RDD
有興趣歡迎關注,大家一起交流學習。