Spark的算子的分類
從大方向來說,Spark 算子大致可以分為以下兩類:
1)Transformation 變換/轉換算子:這種變換并不觸發提交作業,完成作業中間過程處理。
Transformation 操作是延遲計算的,也就是說從一個RDD 轉換生成另一個 RDD 的轉換操作不是馬上執行,需要等到有 Action 操作的時候才會真正觸發運算。
2)Action 行動算子:這類算子會觸發 SparkContext 提交 Job 作業。
Action 算子會觸發 Spark 提交作業(Job),并將數據輸出 Spark系統。
從小方向來說,Spark 算子大致可以分為以下三類:
1)Value數據類型的Transformation算子,這種變換并不觸發提交作業,針對處理的數據項是Value型的數據。
2)Key-Value數據類型的Transfromation算子,這種變換并不觸發提交作業,針對處理的數據項是Key-Value型的數據對。
3)Action算子,這類算子會觸發SparkContext提交Job作業。
1)Value數據類型的Transformation算子
一、輸入分區與輸出分區一對一型
1、map算子
2、flatMap算子
3、mapPartitions算子
4、glom算子
二、輸入分區與輸出分區多對一型
5、union算子
6、cartesian算子
三、輸入分區與輸出分區多對多型
7、grouBy算子
四、輸出分區為輸入分區子集型
8、filter算子
9、distinct算子
10、subtract算子
11、sample算子
? ? ? 12、takeSample算子
? 五、Cache型
13、cache算子
14、persist算子
2)Key-Value數據類型的Transfromation算子
一、輸入分區與輸出分區一對一
15、mapValues算子
二、對單個RDD或兩個RDD聚集
單個RDD聚集
16、combineByKey算子
17、reduceByKey算子
18、partitionBy算子
? 兩個RDD聚集
? ? ? ? ? ? ??19、Cogroup算子
三、連接
20、join算子
21、leftOutJoin和 rightOutJoin算子
?3)Action算子
一、無輸出
? ? ? ? ? ? ? ?22、foreach算子
二、HDFS
23、saveAsTextFile算子
24、saveAsObjectFile算子
三、Scala集合和數據類型
25、collect算子
26、collectAsMap算子
? ? ? ? ? ? ? 27、reduceByKeyLocally算子
? ? ? ? ? 28、lookup算子
29、count算子
30、top算子
31、reduce算子
32、fold算子
33、aggregate算子
1. Transformations 算子
(1)?map
將原來 RDD 的每個數據項通過?map 中的用戶自定義函數 f?映射轉變為一個新的元素。源碼中 map 算子相當于初始化一個 RDD, 新 RDD 叫做 MappedRDD(this, sc.clean(f))。
圖 1中每個方框表示一個 RDD 分區,左側的分區經過用戶自定義函數 f:T->U?映射為右側的新 RDD 分區。但是,實際只有等到 Action算子觸發后,這個 f 函數才會和其他函數在一個stage 中對數據進行運算。在圖 1 中的第一個分區,數據記錄 V1 輸入 f,通過 f 轉換輸出為轉換后的分區中的數據記錄 V’1。
(2)?flatMap
將原來 RDD 中的每個元素通過函數 f 轉換為新的元素,并將生成的 RDD 的每個集合中的元素合并為一個集合,內部創建 FlatMappedRDD(this,sc.clean(f))。
圖 2 表 示 RDD 的 一 個 分 區 ,進 行 flatMap函 數 操 作, flatMap 中 傳 入 的 函 數 為 f:T->U,?T和 U 可以是任意的數據類型。將分區中的數據通過用戶自定義函數 f 轉換為新的數據。外部大方框可以認為是一個 RDD 分區,小方框代表一個集合。 V1、 V2、 V3 在一個集合作為 RDD 的一個數據項,可能存儲為數組或其他容器,轉換為V’1、 V’2、 V’3 后,將原來的數組或容器結合拆散,拆散的數據形成為 RDD 中的數據項。
(3)?mapPartitions
mapPartitions 函 數 獲 取 到 每 個 分 區 的 迭 代器,在 函 數 中 通 過 這 個 分 區 整 體 的 迭 代 器 對整 個 分 區 的 元 素 進 行 操 作。 內 部 實 現 是 生 成
MapPartitionsRDD。圖 3 中的方框代表一個 RDD 分區。圖 3 中,用戶通過函數 f (iter)=>iter.f ilter(_>=3) 對分區中所有數據進行過濾,大于和等于 3 的數據保留。一個方塊代表一個 RDD 分區,含有 1、 2、 3 的分區過濾只剩下元素 3。
(4)glom
glom函數將每個分區形成一個數組,內部實現是返回的GlommedRDD。 圖4中的每個方框代表一個RDD分區。圖4中的方框代表一個分區。 該圖表示含有V1、 V2、 V3的分區通過函數glom形成一數組Array[(V1),(V2),(V3)]。
(5)?union
使用 union 函數時需要保證兩個 RDD 元素的數據類型相同,返回的 RDD 數據類型和被合并的 RDD 元素數據類型相同,并不進行去重操作,保存所有元素。如果想去重
可以使用 distinct()。同時 Spark 還提供更為簡潔的使用 union 的 API,通過 ++ 符號相當于 union 函數操作。
圖 5 中左側大方框代表兩個 RDD,大方框內的小方框代表 RDD 的分區。右側大方框代表合并后的 RDD,大方框內的小方框代表分區。
含有V1、V2、U1、U2、U3、U4的RDD和含有V1、V8、U5、U6、U7、U8的RDD合并所有元素形成一個RDD。V1、V1、V2、V8形成一個分區,U1、U2、U3、U4、U5、U6、U7、U8形成一個分區。
(6)?cartesian
對 兩 個 RDD 內 的 所 有 元 素?進 行 笛 卡 爾 積 操 作。 操 作 后, 內 部 實 現 返 回CartesianRDD。圖6中左側大方框代表兩個 RDD,大方框內的小方框代表 RDD 的分區。右側大方框代表合并后的 RDD,大方框內的小方框代表分區。圖6中的大方框代表RDD,大方框中的小方框代表RDD分區。
例 如: V1 和 另 一 個 RDD 中 的 W1、 W2、 Q5 進 行 笛 卡 爾 積 運 算 形 成 (V1,W1)、(V1,W2)、 (V1,Q5)。
(7)?groupBy
groupBy :將元素通過函數生成相應的 Key,數據就轉化為 Key-Value 格式,之后將 Key 相同的元素分為一組。
函數實現如下:
1)將用戶函數預處理:
val cleanF = sc.clean(f)
2)對數據 map 進行函數操作,最后再進行 groupByKey 分組操作。
this.map(t => (cleanF(t), t)).groupByKey(p)
其中, p 確定了分區個數和分區函數,也就決定了并行化的程度。
圖7 中方框代表一個 RDD 分區,相同key 的元素合并到一個組。例如 V1 和 V2 合并為 V, Value 為 V1,V2。形成 V,Seq(V1,V2)。
圖 7?groupBy 算子對 RDD 轉換
(8)?filter
filter 函數功能是對元素進行過濾,對每個 元 素 應 用 f 函 數, 返 回 值 為 true 的 元 素 在RDD 中保留,返回值為 false 的元素將被過濾掉。 內 部 實 現 相 當 于 生 成 FilteredRDD(this,sc.clean(f))。
下面代碼為函數的本質實現:
deffilter(f:T=>Boolean):RDD[T]=newFilteredRDD(this,sc.clean(f))
圖 8 中每個方框代表一個 RDD 分區, T 可以是任意的類型。通過用戶自定義的過濾函數 f,對每個數據項操作,將滿足條件、返回結果為 true 的數據項保留。例如,過濾掉 V2 和 V3 保留了 V1,為區分命名為 V’1。
圖 8 ?filter 算子對 RDD 轉換
(9)distinct
distinct將RDD中的元素進行去重操作。圖9中的每個方框代表一個RDD分區,通過distinct函數,將數據去重。 例如,重復數據V1、 V1去重后只保留一份V1。
圖9 ?distinct算子對RDD轉換
(10)subtract
subtract相當于進行集合的差操作,RDD 1去除RDD 1和RDD 2交集中的所有元素。圖10中左側的大方框代表兩個RDD,大方框內的小方框代表RDD的分區。 右側大方框
代表合并后的RDD,大方框內的小方框代表分區。 V1在兩個RDD中均有,根據差集運算規則,新RDD不保留,V2在第一個RDD有,第二個RDD沒有,則在新RDD元素中包含V2。
圖10 ? subtract算子對RDD轉換
(11)?sample
sample 將 RDD 這個集合內的元素進行采樣,獲取所有元素的子集。用戶可以設定是否有放回的抽樣、百分比、隨機種子,進而決定采樣方式。內部實現是生成 SampledRDD(withReplacement, fraction, seed)。
函數參數設置:
‰ withReplacement=true,表示有放回的抽樣。
‰ withReplacement=false,表示無放回的抽樣。
圖 11中 的 每 個 方 框 是 一 個 RDD 分 區。 通 過 sample 函 數, 采 樣 50% 的 數 據。V1、 V2、 U1、 U2、U3、U4 采樣出數據 V1 和 U1、 U2 形成新的 RDD。
圖11 ?sample 算子對 RDD 轉換
(12)takeSample
takeSample()函數和上面的sample函數是一個原理,但是不使用相對比例采樣,而是按設定的采樣個數進行采樣,同時返回結果不再是RDD,而是相當于對采樣后的數據進行
Collect(),返回結果的集合為單機的數組。
圖12中左側的方框代表分布式的各個節點上的分區,右側方框代表單機上返回的結果數組。 通過takeSample對數據采樣,設置為采樣一份數據,返回結果為V1。
圖12 ? takeSample算子對RDD轉換
(13)?cache
cache?將 RDD 元素從磁盤緩存到內存。 相當于 persist(MEMORY_ONLY) 函數的功能。
圖13 中每個方框代表一個 RDD 分區,左側相當于數據分區都存儲在磁盤,通過 cache 算子將數據緩存在內存。
圖 13 Cache 算子對 RDD 轉換
(14)?persist
persist 函數對?RDD 進行緩存操作。數據緩存在哪里依據 StorageLevel 這個枚舉類型進行確定。 有以下幾種類型的組合(見10), DISK 代表磁盤,MEMORY 代表內存, SER 代表數據是否進行序列化存儲。
下面為函數定義, StorageLevel 是枚舉類型,代表存儲模式,用戶可以通過圖 14-1 按需進行選擇。
persist(newLevel:StorageLevel)
圖 14-1 中列出persist 函數可以進行緩存的模式。例如,MEMORY_AND_DISK_SER 代表數據可以存儲在內存和磁盤,并且以序列化的方式存儲,其他同理。
圖 14-1 ?persist 算子對 RDD 轉換
圖 14-2 中方框代表 RDD 分區。 disk 代表存儲在磁盤, mem 代表存儲在內存。數據最初全部存儲在磁盤,通過 persist(MEMORY_AND_DISK) 將數據緩存到內存,但是有的分區無法容納在內存,將含有 V1、 V2、 V3 的RDD存儲到磁盤,將含有U1,U2的RDD仍舊存儲在內存。
? ? ? 圖 14-2 ? Persist 算子對 RDD 轉換
(15)?mapValues
mapValues :針對(Key, Value)型數據中的 Value 進行 Map 操作,而不對 Key 進行處理。
? ? 圖 15 中的方框代表 RDD 分區。 a=>a+2 代表對 (V1,1) 這樣的 Key Value 數據對,數據只對 Value 中的 1 進行加 2 操作,返回結果為 3。
圖 15 ? mapValues 算子 RDD 對轉換
(16)?combineByKey
下面代碼為 combineByKey 函數的定義:
combineByKey[C](createCombiner:(V) C,
mergeValue:(C, V) C,
mergeCombiners:(C, C) C,
partitioner:Partitioner,
mapSideCombine:Boolean=true,
serializer:Serializer=null):RDD[(K,C)]
說明:
‰ createCombiner: V => C, C 不存在的情況下,比如通過 V 創建 seq C。
‰ mergeValue: (C, V) => C,當 C 已經存在的情況下,需要 merge,比如把 item V
加到 seq C 中,或者疊加。
mergeCombiners: (C, C) => C,合并兩個 C。
‰ partitioner: Partitioner, Shuff le 時需要的 Partitioner。
‰ mapSideCombine : Boolean = true,為了減小傳輸量,很多 combine 可以在 map
端先做,比如疊加,可以先在一個 partition 中把所有相同的 key 的 value 疊加,
再 shuff le。
‰ serializerClass: String = null,傳輸需要序列化,用戶可以自定義序列化類:
例如,相當于將元素為 (Int, Int) 的 RDD 轉變為了 (Int, Seq[Int]) 類型元素的 RDD。圖 16中的方框代表 RDD 分區。如圖,通過 combineByKey, 將 (V1,2), (V1,1)數據合并為( V1,Seq(2,1))。
圖 16? comBineByKey 算子對 RDD 轉換
(17)?reduceByKey
reduceByKey 是比 combineByKey 更簡單的一種情況,只是兩個值合并成一個值,( Int, Int V)to (Int, Int C),比如疊加。所以 createCombiner reduceBykey 很簡單,就是直接返回 v,而 mergeValue和 mergeCombiners 邏輯是相同的,沒有區別。
函數實現:
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)]
= {
combineByKey[V]((v: V) => v, func, func, partitioner)
}
圖17中的方框代表 RDD 分區。通過用戶自定義函數 (A,B) => (A + B) 函數,將相同 key 的數據 (V1,2) 和 (V1,1) 的 value 相加運算,結果為( V1,3)。
圖 17?reduceByKey 算子對 RDD 轉換
(18)partitionBy
partitionBy函數對RDD進行分區操作。
函數定義如下。
partitionBy(partitioner:Partitioner)
如果原有RDD的分區器和現有分區器(partitioner)一致,則不重分區,如果不一致,則相當于根據分區器生成一個新的ShuffledRDD。
圖18中的方框代表RDD分區。 通過新的分區策略將原來在不同分區的V1、 V2數據都合并到了一個分區。
圖18 partitionBy算子對RDD轉換
(19)Cogroup
cogroup函數將兩個RDD進行協同劃分,cogroup函數的定義如下。
cogroup[W](other: RDD[(K, W)], numPartitions: Int): RDD[(K, (Iterable[V], Iterable[W]))]
對在兩個RDD中的Key-Value類型的元素,每個RDD相同Key的元素分別聚合為一個集合,并且返回兩個RDD中對應Key的元素集合的迭代器。
(K, (Iterable[V], Iterable[W]))
其中,Key和Value,Value是兩個RDD下相同Key的兩個數據集合的迭代器所構成的元組。
圖19中的大方框代表RDD,大方框內的小方框代表RDD中的分區。 將RDD1中的數據(U1,1)、 (U1,2)和RDD2中的數據(U1,2)合并為(U1,((1,2),(2)))。
圖19 ?Cogroup算子對RDD轉換
(20)?join
join 對兩個需要連接的 RDD 進行 cogroup函數操作,將相同 key 的數據能夠放到一個分區,在 cogroup 操作之后形成的新 RDD 對每個key 下的元素進行笛卡爾積的操作,返回的結果再展平,對應 key 下的所有元組形成一個集合。最后返回 RDD[(K, (V, W))]。
下 面 代 碼 為 join 的 函 數 實 現, 本 質 是通 過 cogroup 算 子 先 進 行 協 同 劃 分, 再 通 過flatMapValues 將合并的數據打散。
this.cogroup(other,partitioner).f latMapValues{case(vs,ws) =>?for(v<-vs;w<-ws)yield(v,w) }
圖 20是對兩個 RDD 的 join 操作示意圖。大方框代表 RDD,小方框代表 RDD 中的分區。函數對相同 key 的元素,如 V1 為 key 做連接后結果為 (V1,(1,1)) 和 (V1,(1,2))。
圖 20 ? join 算子對 RDD 轉換
(21)eftOutJoin和rightOutJoin
LeftOutJoin(左外連接)和RightOutJoin(右外連接)相當于在join的基礎上先判斷一側的RDD元素是否為空,如果為空,則填充為空。 如果不為空,則將數據進行連接運算,并
返回結果。
下面代碼是leftOutJoin的實現。
if (ws.isEmpty) {
vs.map(v => (v, None))
} else {
for (v <- vs; w <- ws) yield (v, Some(w))
}
2. Actions 算子
本質上在 Action 算子中通過 SparkContext 進行了提交作業的 runJob 操作,觸發了RDD DAG 的執行。
例如, Action 算子 collect 函數的代碼如下,感興趣的讀者可以順著這個入口進行源碼剖析:
/**
* Return an array that contains all of the elements in this RDD.
*/
def collect(): Array[T] = {
/* 提交 Job*/
val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
Array.concat(results: _*)
}
(22)?foreach
foreach 對 RDD 中的每個元素都應用 f 函數操作,不返回 RDD 和 Array, 而是返回Uint。圖22表示 foreach 算子通過用戶自定義函數對每個數據項進行操作。本例中自定義函數為 println(),控制臺打印所有數據項。
圖 22 foreach 算子對 RDD 轉換
(23)?saveAsTextFile
函數將數據輸出,存儲到 HDFS 的指定目錄。
下面為 saveAsTextFile 函數的內部實現,其內部
通過調用 saveAsHadoopFile 進行實現:
this.map(x => (NullWritable.get(), new Text(x.toString))).saveAsHadoopFile[TextOutputFormat[NullWritable, Text]](path)
將 RDD 中的每個元素映射轉變為 (null, x.toString),然后再將其寫入 HDFS。
圖 23中左側方框代表 RDD 分區,右側方框代表 HDFS 的 Block。通過函數將RDD 的每個分區存儲為 HDFS 中的一個 Block。
圖 23 ? saveAsHadoopFile 算子對 RDD 轉換
(24)saveAsObjectFile
saveAsObjectFile將分區中的每10個元素組成一個Array,然后將這個Array序列化,映射為(Null,BytesWritable(Y))的元素,寫入HDFS為SequenceFile的格式。
下面代碼為函數內部實現。
map(x=>(NullWritable.get(),new BytesWritable(Utils.serialize(x))))
圖24中的左側方框代表RDD分區,右側方框代表HDFS的Block。 通過函數將RDD的每個分區存儲為HDFS上的一個Block。
圖24 saveAsObjectFile算子對RDD轉換
(25)?collect
collect 相當于 toArray, toArray 已經過時不推薦使用, collect 將分布式的 RDD 返回為一個單機的 scala Array 數組。在這個數組上運用 scala 的函數式操作。
圖 25中左側方框代表 RDD 分區,右側方框代表單機內存中的數組。通過函數操作,將結果返回到 Driver 程序所在的節點,以數組形式存儲。
圖 25 ? Collect 算子對 RDD 轉換
(26)collectAsMap
collectAsMap對(K,V)型的RDD數據返回一個單機HashMap。 對于重復K的RDD元素,后面的元素覆蓋前面的元素。
圖26中的左側方框代表RDD分區,右側方框代表單機數組。 數據通過collectAsMap函數返回給Driver程序計算結果,結果以HashMap形式存儲。
圖26 CollectAsMap算子對RDD轉換
(27)reduceByKeyLocally
實現的是先reduce再collectAsMap的功能,先對RDD的整體進行reduce操作,然后再收集所有結果返回為一個HashMap。
(28)lookup
下面代碼為lookup的聲明。
lookup(key:K):Seq[V]
Lookup函數對(Key,Value)型的RDD操作,返回指定Key對應的元素形成的Seq。 這個函數處理優化的部分在于,如果這個RDD包含分區器,則只會對應處理K所在的分區,然后返回由(K,V)形成的Seq。 如果RDD不包含分區器,則需要對全RDD元素進行暴力掃描處理,搜索指定K對應的元素。
圖28中的左側方框代表RDD分區,右側方框代表Seq,最后結果返回到Driver所在節點的應用中。
圖28 ?lookup對RDD轉換
(29)?count
count 返回整個 RDD 的元素個數。
內部函數實現為:
defcount():Long=sc.runJob(this,Utils.getIteratorSize_).sum
圖 29中,返回數據的個數為 5。一個方塊代表一個 RDD 分區。
?圖29 count 對 RDD 算子轉換
(30)top
top可返回最大的k個元素。 函數定義如下。
top(num:Int)(implicit ord:Ordering[T]):Array[T]
相近函數說明如下。
·top返回最大的k個元素。
·take返回最小的k個元素。
·takeOrdered返回最小的k個元素,并且在返回的數組中保持元素的順序。
·first相當于top(1)返回整個RDD中的前k個元素,可以定義排序的方式Ordering[T]。
返回的是一個含前k個元素的數組。
(31)reduce
reduce函數相當于對RDD中的元素進行reduceLeft函數的操作。 函數實現如下。
Some(iter.reduceLeft(cleanF))
reduceLeft先對兩個元素進行reduce函數操作,然后將結果和迭代器取出的下一個元素進行reduce函數操作,直到迭代器遍歷完所有元素,得到最后結果。在RDD中,先對每個分區中的所有元素的集合分別進行reduceLeft。 每個分區形成的結果相當于一個元素,再對這個結果集合進行reduceleft操作。
例如:用戶自定義函數如下。
f:(A,B)=>(A._1+”@”+B._1,A._2+B._2)
圖31中的方框代表一個RDD分區,通過用戶自定函數f將數據進行reduce運算。 示例
最后的返回結果為V1@[1]V2U!@U2@U3@U4,12。
圖31 reduce算子對RDD轉換
(32)fold
fold和reduce的原理相同,但是與reduce不同,相當于每個reduce時,迭代器取的第一個元素是zeroValue。
圖32中通過下面的用戶自定義函數進行fold運算,圖中的一個方框代表一個RDD分區。 讀者可以參照reduce函數理解。
fold((”V0@”,2))( (A,B)=>(A._1+”@”+B._1,A._2+B._2))
圖32 ?fold算子對RDD轉換
(33)aggregate
aggregate先對每個分區的所有元素進行aggregate操作,再對分區的結果進行fold操作。
aggreagate與fold和reduce的不同之處在于,aggregate相當于采用歸并的方式進行數據聚集,這種聚集是并行化的。 而在fold和reduce函數的運算過程中,每個分區中需要進行串行處理,每個分區串行計算完結果,結果再按之前的方式進行聚集,并返回最終聚集結果。
函數的定義如下。
aggregate[B](z: B)(seqop: (B,A) => B,combop: (B,B) => B): B
圖33通過用戶自定義函數對RDD 進行aggregate的聚集操作,圖中的每個方框代表一個RDD分區。
rdd.aggregate(”V0@”,2)((A,B)=>(A._1+”@”+B._1,A._2+B._2)),(A,B)=>(A._1+”@”+B_1,A._@+B_.2))
最后,介紹兩個計算模型中的兩個特殊變量。
廣播(broadcast)變量:其廣泛用于廣播Map Side Join中的小表,以及廣播大變量等場景。 這些數據集合在單節點內存能夠容納,不需要像RDD那樣在節點之間打散存儲。
Spark運行時把廣播變量數據發到各個節點,并保存下來,后續計算可以復用。 相比Hadoo的distributed cache,廣播的內容可以跨作業共享。 Broadcast的底層實現采用了BT機制。
②代表V。
③代表U。
accumulator變量:允許做全局累加操作,如accumulator變量廣泛使用在應用中記錄當前的運行指標的情景。