RDD支持兩種操作:
轉換
:從現有的數據集創建一個新的數據集;動作
:在數據集上運行計算後,返回一個值給驅動程序。
例如,map 是一種轉換,它將數據集每一個元素都傳遞給函數,並返回一個新的分佈數據集表示結果,而 reduce 是一種動作,通過一些函數將所有的元素疊加起來,並將最終結果返回給運行程序。
Spark 中的 所有轉換都是惰性的
,也就是說,他們並不會直接計算結果。相反的,它們只是記住應用到基礎數據集上的這些轉換動作。只有當發生一個要求返回結果給運行程序的動作時,這些轉換纔會真正運行。
默認情況下,每一個轉換過的 RDD 都會在你運行一個動作時被重新計算。不過,你也可以使用 persist
或者 cache
方法,持久化一個 RDD 在內存中。在這種情況下,Spark 將會在集羣中,保存相關元素,下次你查詢這個 RDD 時,它將能更快速訪問。除了持久化到內存,Spark 也支持在磁盤上持久化數據集,或在節點之間複製數據集。
Scala 示例:
val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b) => a + b)
Java 示例:
JavaRDD<String> lines = sc.textFile("data.txt");
JavaRDD<Integer> lineLengths = lines.map(s -> s.length());
int totalLength = lineLengths.reduce((a, b) -> a + b);
Python 示例:
lines = sc.textFile("data.txt")
lineLengths = lines.map(lambda s: len(s))
totalLength = lineLengths.reduce(lambda a, b: a + b)
代碼說明:
- 第一行定義了一個基礎 RDD,但並沒有開始載入內存,僅僅將 lines 指向了這個file
- 第二行也僅僅定義了 linelengths 是作爲 map 的結果,但也沒有開始運行 map 這個過程
- 直到第三句話纔開始運行,各個 worker 節點開始運行自己的 map、reduce 過程
你也可以調用 lineLengths.persist()
來持久化 RDD。
除了使用 lambda 表達式,也可以通過函數來運行轉換或者動作,使用函數需要注意局部變量的作用域問題。
例如下面的 Python 代碼中的 field 變量:
class MyClass(object):
def __init__(self):
self.field = "Hello"
def doStuff(self, rdd):
field = self.field
return rdd.map(lambda s: field + x)
如果使用 Java 語言,則需要用到匿名內部類:
class GetLength implements Function<String, Integer> {
public Integer call(String s) { return s.length(); }
}
class Sum implements Function2<Integer, Integer, Integer> {
public Integer call(Integer a, Integer b) { return a + b; }
}
JavaRDD<String> lines = sc.textFile("data.txt");
JavaRDD<Integer> lineLengths = lines.map(new GetLength());
int totalLength = lineLengths.reduce(new Sum());
Spark 也支持鍵值對的操作,這在分組和聚合操作時候用得到。定義一個鍵值對對象時,需要自定義該對象的 equals() 和 hashCode() 方法。
在 Scala 中有一個 Tuple2 對象表示鍵值對,這是一個內置的對象,通過 (a,b)
就可以創建一個
Tuple2 對象。在你的程序中,通過導入 org.apache.spark.SparkContext._
就可以對 Tuple2 進行操作。對鍵值對的操作方法,可以查看 PairRDDFunctions
下面是一個用 scala 統計單詞出現次數的例子:
val lines = sc.textFile("data.txt")
val pairs = lines.map(s => (s, 1))
val counts = pairs.reduceByKey((a, b) => a + b)
接下來,你還可以執行 counts.sortByKey()
、 counts.collect()
等操作。
如果用 Java 統計,則代碼如下:
JavaRDD<String> lines = sc.textFile("data.txt");
JavaPairRDD<String, Integer> pairs = lines.mapToPair(s -> new Tuple2(s, 1));
JavaPairRDD<String, Integer> counts = pairs.reduceByKey((a, b) -> a + b);
用 Python 統計,代碼如下:
lines = sc.textFile("data.txt")
pairs = lines.map(lambda s: (s, 1))
counts = pairs.reduceByKey(lambda a, b: a + b)
測試
現在來結合上面的例子實現一個完整的例子。下面,我們來 分析 Nginx 日誌中狀態碼出現次數,並且將結果按照狀態碼從小到大排序。
先將測試數據上傳到 hdfs:
$ hadoop fs -put access.log
然後,編寫一個 python 文件,保存爲 SimpleApp.py:
from pyspark import SparkContext
logFile = "access.log"
sc = SparkContext("local", "Simple App")
logData = sc.textFile(logFile).cache()
counts = logData.map(lambda line: line.split()[8]).map(lambda word: (word, 1)).reduceByKey(lambda a, b: a + b).sortByKey(lambda x: x)
# This is just a demo on how to bring all the sorted data back to a single node.
# In reality, we wouldn't want to collect all the data to the driver node.
output = counts.collect()
for (word, count) in output:
print "%s: %i" % (word, count)
counts.saveAsTextFile("spark_results")
sc.stop()
接下來,運行下面代碼:
$ spark-submit --master local[4] SimpleApp.py
運行成功之後,你會在終端看到以下輸出:
200: 6827
206: 120
301: 7
304: 10
403: 38
404: 125
416: 1
並且,在hdfs 上 /user/spark/spark_results/part-00000 內容如下:
(u'200', 6827)
(u'206', 120)
(u'301', 7)
(u'304', 10)
(u'403', 38)
(u'404', 125)
(u'416', 1)
其實,這個例子和官方提供的例子很相像,具體請看 wordcount.py。
常見的轉換
轉換 | 含義 |
---|---|
map(func) |
返回一個新分佈式數據集,由每一個輸入元素經過func函數轉換後組成 |
filter(func) |
返回一個新數據集,由經過func函數計算後返回值爲 true 的輸入元素組成 |
flatMap(func) |
類似於 map,但是每一個輸入元素可以被映射爲0或多個輸出元素,因此 func 應該返回一個序列 |
mapPartitions(func) |
類似於 map,但獨立地在 RDD 的每一個分塊上運行,因此在類型爲 T 的 RDD 上運行時,func 的函數類型必須是 Iterator[T] ⇒ Iterator[U] |
mapPartitionsWithSplit(func) |
類似於 mapPartitions, 但 func 帶有一個整數參數表示分塊的索引值。因此在類型爲 T的RDD上運行時,func 的函數類型必須是 (Int, Iterator[T]) ⇒ Iterator[U] |
sample(withReplacement,fraction, seed) |
根據 fraction 指定的比例,對數據進行採樣,可以選擇是否用隨機數進行替換,seed 用於指定隨機數生成器種子 |
union(otherDataset) |
返回一個新的數據集,新數據集是由源數據集和參數數據集聯合而成 |
distinct([numTasks])) |
返回一個包含源數據集中所有不重複元素的新數據集 |
groupByKey([numTasks]) |
在一個鍵值對的數據集上調用,返回一個 (K,Seq[V]) 對的數據集 。注意:默認情況下,只有8個並行任務來做操作,但是你可以傳入一個可選的 numTasks 參數來改變它 |
reduceByKey(func, [numTasks]) |
在一個鍵值對的數據集上調用時,返回一個鍵值對的數據集,使用指定的 reduce 函數,將相同 key 的值聚合到一起。類似 groupByKey,reduce 任務個數是可以通過第二個可選參數來配置的 |
sortByKey([ascending], [numTasks]) |
在一個鍵值對的數據集上調用,K 必須實現 Ordered 接口,返回一個按照 Key 進行排序的鍵值對數據集。升序或降序由 ascending 布爾參數決定 |
join(otherDataset, [numTasks]) |
在類型爲(K,V)和(K,W) 類型的數據集上調用時,返回一個相同key對應的所有元素對在一起的 (K, (V, W)) 數據集 |
cogroup(otherDataset, [numTasks]) |
在類型爲(K,V)和(K,W) 的數據集上調用,返回一個 (K, Seq[V], Seq[W]) 元組的數據集。這個操作也可以稱之爲 groupwith |
cartesian(otherDataset) |
笛卡爾積,在類型爲 T 和 U 類型的數據集上調用時,返回一個 (T, U) 對數據集(兩兩的元素對) |
pipe(command, [envVars]) |
對 RDD 進行管道操作 |
coalesce(numPartitions) |
減少 RDD 的分區數到指定值。在過濾大量數據之後,可以執行此操作 |
repartition(numPartitions) |
重新給 RDD 分區 |
repartitionAndSortWithinPartitions(partitioner) |
重新給 RDD 分區,並且每個分區內以記錄的 key 排序 |
常用的動作
常用的動作列表
動作 | 含義 |
---|---|
reduce(func) |
通過函數 func 聚集數據集中的所有元素。這個功能必須可交換且可關聯的,從而可以正確的被並行執行。 |
collect() |
在驅動程序中,以數組的形式,返回數據集的所有元素。這通常會在使用 filter 或者其它操作並返回一個足夠小的數據子集後再使用會比較有用。 |
count() |
返回數據集的元素的個數。 |
first() |
返回數據集的第一個元素,類似於 take(1) |
take(n) |
返回一個由數據集的前 n 個元素組成的數組。注意,這個操作目前並非並行執行,而是由驅動程序計算所有的元素 |
takeSample(withReplacement,num, seed) |
返回一個數組,在數據集中隨機採樣 num 個元素組成,可以選擇是否用隨機數替換不足的部分,seed 用於指定的隨機數生成器種子 |
takeOrdered(n, [ordering]) |
返回自然順序或者自定義順序的前 n 個元素 |
saveAsTextFile(path) |
將數據集的元素,以 textfile 的形式,保存到本地文件系統,HDFS或者任何其它 hadoop 支持的文件系統。對於每個元素,Spark 將會調用 toString 方法,將它轉換爲文件中的文本行 |
saveAsSequenceFile(path) (Java and Scala) |
將數據集的元素,以 Hadoop sequencefile 的格式保存到指定的目錄下 |
saveAsObjectFile(path) (Java and Scala) |
將數據集的元素,以 Java 序列化的方式保存到指定的目錄下 |
countByKey() |
對(K,V)類型的 RDD 有效,返回一個 (K,Int) 對的 Map,表示每一個key對應的元素個數 |
foreach(func) |
在數據集的每一個元素上,運行函數 func 進行更新。這通常用於邊緣效果,例如更新一個累加器,或者和外部存儲系統進行交互,例如HBase |
3.4 RDD持久化
Spark 最重要的一個功能,就是在不同操作間,持久化(或緩存)一個數據集在內存中,這將使得後續的動作變得更加迅速。緩存是用 Spark 構建迭代算法的關鍵。 使用以下兩種方法可以標記要緩存的 RDD:
lineLengths.persist()
lineLengths.cache()
取消緩存則用:
lineLengths.unpersist()
每一個RDD都可以用不同的保存級別進行保存,通過將一個 org.apache.spark.storage.StorageLevel
對象傳遞給 persist(self, storageLevel)
可以控制 RDD 持久化到磁盤、內存或者是跨節點複製等等。 cache()
方法是使用默認存儲級別的快捷方法,也就是 StorageLevel.MEMORY_ONLY
。
完整的可選存儲級別如下:
存儲級別 | 意義 |
---|---|
MEMORY_ONLY |
默認的級別, 將 RDD 作爲反序列化的的對象存儲在 JVM 中。如果不能被內存裝下,一些分區將不會被緩存,並且在需要的時候被重新計算 |
MEMORY_AND_DISK |
將 RDD 作爲反序列化的的對象存儲在 JVM 中。如果不能被與內存裝下,超出的分區將被保存在硬盤上,並且在需要時被讀取 |
MEMORY_ONLY_SER |
將 RDD 作爲序列化的的對象進行存儲(每一分區佔用一個字節數組)。通常來說,這比將對象反序列化的空間利用率更高,尤其當使用fast serializer,但在讀取時會比較佔用CPU |
MEMORY_AND_DISK_SER |
與 MEMORY_ONLY_SER 相似,但是把超出內存的分區將存儲在硬盤上而不是在每次需要的時候重新計算 |
DISK_ONLY |
只將 RDD 分區存儲在硬盤上 |
MEMORY_ONLY_2 、MEMORY_AND_DISK_2 等 |
與上述的存儲級別一樣,但是將每一個分區都複製到兩個集羣結點上 |
OFF_HEAP |
開發中 |
Spark 的不同存儲級別,旨在滿足內存使用和 CPU 效率權衡上的不同需求。我們建議通過以下的步驟來進行選擇:
- 如果你的 RDD 可以很好的與默認的存儲級別契合,就不需要做任何修改了。這已經是 CPU 使用效率最高的選項,它使得 RDD的操作儘可能的快。
- 如果不行,試着使用
MEMORY_ONLY_SER
並且選擇一個快速序列化的庫使得對象在有比較高的空間使用率的情況下,依然可以較快被訪問。 - 儘可能不要存儲到硬盤上,除非計算數據集的函數,計算量特別大,或者它們過濾了大量的數據。否則,重新計算一個分區的速度,和與從硬盤中讀取基本差不多快。
- 如果你想有快速故障恢復能力,使用複製存儲級別。例如:用 Spark 來響應web應用的請求。所有的存儲級別都有通過重新計算丟失數據恢復錯誤的容錯機制,但是複製存儲級別可以讓你在 RDD 上持續的運行任務,而不需要等待丟失的分區被重新計算。
- 如果你想要定義你自己的存儲級別,比如複製因子爲3而不是2,可以使用
StorageLevel
單例對象的apply()
方法。