本文翻譯自O(shè)'Reilly出版Tom White所著《Hadoop: The Definitive Guide》第4版第19章,向作者致敬。該書英文第4版已于2015年4月出版,至今已近15個月,而市面上中文第3版還在大行其道。Spark一章是第4版新增的內(nèi)容,筆者在學(xué)習(xí)過程中順便翻譯記錄,由于筆者也在學(xué)習(xí),并無實(shí)戰(zhàn)經(jīng)驗,難免翻譯不妥或出錯,歡迎方家來信斧正。翻譯純屬興趣,不做商業(yè)用途。秦銘,Email地址qinm08@gmail.com。
Apache Spark 是一個大規(guī)模數(shù)據(jù)處理的集群計算框架。和本書中討論的大多數(shù)其他處理框架不同,Spark不使用MapReduce作為執(zhí)行引擎,作為替代,Spark使用自己的分布式運(yùn)行環(huán)境(distributed runtime)來執(zhí)行集群上的工作。然而,Spark與MapReduce在API和runtime方面有許多相似,本章中我們將會看到。Spark和Hadoop緊密集成:它可以運(yùn)行在YARN上,處理Hadoop的文件格式,后端存儲采用HDFS。
Spark最著名的是它擁有把大量的工作數(shù)據(jù)集保持在內(nèi)存中的能力。這種能力使得Spark勝過對應(yīng)的MapReduce工作流(某些情況下差別顯著),在MapReduce中數(shù)據(jù)集總是要從磁盤加載。兩種類型的應(yīng)用從Spark這種處理模型中受益巨大:1)迭代算法,一個函數(shù)在某數(shù)據(jù)集上反復(fù)執(zhí)行直到滿足退出條件。2)交互式分析,用戶在某數(shù)據(jù)集上執(zhí)行一系列的特定查詢。
即使你不需要內(nèi)存緩存,Spark依然有充滿魅力的理由:它的DAG引擎和用戶體驗。與MapReduce不同,Spark的DAG引擎能夠處理任意的多個操作組成的管道(pipelines of operators)并翻譯為單個Job。
Spark的用戶體驗也是首屈一指的(second to none),它有豐富的API用來執(zhí)行很多常見的數(shù)據(jù)處理任務(wù),比如join。行文之時,Spark提供三種語言的API:Scala,Java和Python。本章中的大多數(shù)例子將采用Scala API,但翻譯為別的語言也是容易的。Spark還帶有一個基于Scala或Python的REPL(read-eval-print loop)環(huán)境,可以快速簡便的查看數(shù)據(jù)集。
Spark是個構(gòu)建分析工具的好平臺,為達(dá)此目的,Apache Spark項目包含了眾多的模塊:機(jī)器學(xué)習(xí)(MLlib),圖形處理(GraphX),流式處理(Spark Streaming),還有SQL(Spark SQL)。本章內(nèi)容不涉及這些模塊,感興趣的讀者可以訪問 Apache Spark 網(wǎng)站 。
Installing Spark
從 下載頁面 下載Spark二進(jìn)制分發(fā)包的穩(wěn)定版本(選擇和你正在使用的Hadoop版本相匹配的)。在合適的地方解壓這個tar包。
% tar xzf spark-x.y.z-bin-distro.tgz
把Spark加入到PATH環(huán)境變量中
% export SPARK_HOME=~/sw/spark-x.y.z-bin-distro
% export PATH=$PATH:$SPARK_HOME/bin
我們現(xiàn)在可以運(yùn)行Spark的例子了。
An Example
為了介紹Spark,我們使用spark-shell來運(yùn)行一個交互式會話,這是帶有Spark附加組件的Scala REPL,用下面的命令啟動shell:
% spark-shell
Spark context available as sc.
scala>
從控制臺的輸出,我們可以看到shell創(chuàng)建了一個Scala變量,sc,用來存儲SparkContext實(shí)例。這是Spark的入口,我們可以這樣加載一個文本文件:
scala> val lines = sc.textFile("input/ncdc/micro-tab/sample.txt")
lines: org.apache.spark.rdd.RDD[String] = MappedRDD[1] at textFile at <console>:12
lines變量是對一個彈性數(shù)據(jù)集(RDD)的引用,RDD是Spark的核心抽象:分區(qū)在集群中多臺機(jī)器上的只讀的對象集合。在典型的Spark程序中,一個或多個RDD被加載進(jìn)來作為輸入,經(jīng)過一系列的轉(zhuǎn)變(transformation),成為一組目標(biāo)RDD,可以對其執(zhí)行action(比如計算結(jié)果或者寫入持久化存儲) 。“彈性數(shù)據(jù)集”中的“彈性”是指,Spark會通過從源RDD中重新計算的方式,來自動重建一個丟失的分區(qū)。
加載RDD和執(zhí)行transformation不會觸發(fā)數(shù)據(jù)處理,僅僅是創(chuàng)建一個執(zhí)行計算的計劃。當(dāng)action(比如 foreach())執(zhí)行的時候,才會觸發(fā)計算。
我們要做的第一個transformation,是把lines拆分為fields:
scala> val records = lines.map(_.split("\t"))
records: org.apache.spark.rdd.RDD[Array[String]] = MappedRDD[2] at map at <console>:14
這里使用了RDD的map()方法,對RDD中的每一個元素,執(zhí)行一個函數(shù)。本例中,我們把每一行(字符串String)拆分為 Scala 的字符串?dāng)?shù)組(Array of Strings)。
接下來,我們使用過濾器(filter)來去掉可能存在的壞記錄:
scala> val filtered = records.filter(rec => (rec(1) != "9999" && rec(2).matches("[01459]")))
filtered: org.apache.spark.rdd.RDD[Array[String]] = FilteredRDD[3] at filter at <console>:16
RDD的filter方法接收一個返回布爾值的函數(shù)作為參數(shù)。這個函數(shù)過濾掉那些溫度缺失(由9999表示)或者質(zhì)量不好的記錄。
為了找到每一年的最高氣溫,我們需要在year字段上執(zhí)行分組操作,這樣才能處理每一年的所有溫度值。Spark提供reduceByKey()方法來做這件事情,但它需要一個鍵值對RDD,因此我們需要通過另一個map來把現(xiàn)有的RDD轉(zhuǎn)變?yōu)檎_的形式:
scala> val tuples = filtered.map(rec => (rec(0).toInt, rec(1).toInt))
tuples: org.apache.spark.rdd.RDD[(Int, Int)] = MappedRDD[4] at map at <console>:18
現(xiàn)在可以執(zhí)行聚合了。reduceByKey()方法的參數(shù)是一個函數(shù),這個函數(shù)接受兩個數(shù)值并聯(lián)合為一個單獨(dú)的數(shù)值。這里我們使用Java的Math.max函數(shù):
scala> val maxTemps = tuples.reduceByKey((a, b) => Math.max(a, b))
maxTemps: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[7] at reduceByKey at <console>:21
現(xiàn)在可以展示maxTemps的內(nèi)容了,調(diào)用foreach()方法并傳入println(),把每個元素打印到控制臺:
scala> maxTemps.foreach(println(_))
(1950,22)
(1949,111)
這個foreach()方法,與標(biāo)準(zhǔn)Scala集合(比如List)中的等價物相同,對RDD中的每個元素應(yīng)用一個函數(shù)(此函數(shù)具有副作用)。正是這個操作,促使Spark運(yùn)行一個job來計算RDD中的數(shù)據(jù),使之能夠跑步通過println()方法:-)
或者,也可以把RDD保存到文件系統(tǒng):
scala> maxTemps.saveAsTextFile("output")
這樣會創(chuàng)建一個output目錄,包含分區(qū)文件:
% cat output/part-*
(1950,22)
(1949,111)
這個saveAsTextFile()方法也會觸發(fā)一個Spark job。主要的區(qū)別是沒有返回值,而是把RDD的計算結(jié)果及其分區(qū)文件寫入output目錄中。
Spark Applications, Jobs, Stages, Tasks
示例中我們看到,和MapReduce一樣,Spark也有job的概念。然而,Spark的job比MapReduce的job更通用,因為它是由任意的stage的有向無環(huán)圖(DAG)組成。每個stage大致等同于MapReduce中的map或者reduce階段。
Stages被Spark 運(yùn)行時拆分為tasks,并行地運(yùn)行在RDD的分區(qū)之上,就像MapReduce的task一樣。
一個Job總是運(yùn)行于一個application的上下文中,由SparkContext實(shí)例表示,application的作用是分組RDD和共享變量。一個application可以運(yùn)行多個job,串行或者并行,并且提供一種機(jī)制,使得一個job可以訪問同一application中的前一個job緩存的RDD。一個交互式的Spark會話,比如spark-shell會話,就是一個application的實(shí)例。