Hadoop是對大資料集進行分散式計算的標準工具,這也是為什麼當你穿過機場時能看到”大資料(Big Data)”廣告的原因。它已經成為大資料的作業系統,提供了包括工具和技巧在內的豐富生態系統,允許使用相對便宜的商業硬體叢集進行超級計算機級別的計算。2003和2004年,兩個來自Google的觀點使Hadoop成為可能:一個分散式儲存框架(Google檔案系統),在Hadoop中被實現為HDFS;一個分散式計算框架(MapReduce)。
這兩個觀點成為過去十年規模分析(scaling analytics)、大規模機器學習(machine learning),以及其他大資料應用出現的主要推動力!但是,從技術角度上講,十年是一段非常長的時間,而且Hadoop還存在很多已知限制,尤其是MapReduce。對MapReduce程式設計明顯是困難的。對大多數分析,你都必須用很多步驟將Map和Reduce任務串接起來。這造成類SQL的計算或機器學習需要專門的系統來進行。更糟的是,MapReduce要求每個步驟間的資料要序列化到磁碟,這意味著MapReduce作業的I/O成本很高,導致互動分析和迭代演算法(iterative algorithms)開銷很大;而事實是,幾乎所有的最優化和機器學習都是迭代的。
為了解決這些問題,Hadoop一直在向一種更為通用的資源管理框架轉變,即YARN(Yet Another Resource Negotiator, 又一個資源協調者)。YARN實現了下一代的MapReduce,但同時也允許應用利用分散式資源而不必採用MapReduce進行計算。通過將叢集管理一般化,研究轉到分散式計算的一般化上,來擴充套件了MapReduce的初衷。
Spark是第一個脫胎於該轉變的快速、通用分散式計算正規化,並且很快流行起來。Spark使用函數語言程式設計正規化擴充套件了MapReduce模型以支援更多計算型別,可以涵蓋廣泛的工作流,這些工作流之前被實現為Hadoop之上的特殊系統。Spark使用記憶體快取來提升效能,因此進行互動式分析也足夠快速(就如同使用Python直譯器,與叢集進行互動一樣)。快取同時提升了迭代演算法的效能,這使得Spark非常適合資料理論任務,特別是機器學習。
本文中,我們將首先討論如何在本地機器上或者EC2的叢集上設定Spark進行簡單分析。然後,我們在入門級水平探索Spark,瞭解Spark是什麼以及它如何工作(希望可以激發更多探索)。最後兩節我們開始通過命令列與Spark進行互動,然後演示如何用Python寫Spark應用,並作為Spark作業提交到叢集上。
設定Spark
在本機設定和執行Spark非常簡單。你只需要下載一個預構建的包,只要你安裝了Java 6+和Python 2.6+,就可以在Windows、Mac OS X和Linux上執行Spark。確保java程式在PATH環境變數中,或者設定了JAVA_HOME環境變數。類似的,python也要在PATH中。
假設你已經安裝了Java和Python:
- 訪問Spark下載頁
- 選擇Spark最新發布版(本文寫作時是1.2.0),一個預構建的Hadoop 2.4包,直接下載。
現在,如何繼續依賴於你的作業系統,靠你自己去探索了。Windows使用者可以在評論區對如何設定的提示進行評論。
一般,我的建議是按照下面的步驟(在POSIX作業系統上):
1.解壓Spark
1 |
~$ tar -xzf spark-1.2.0-bin-hadoop2.4.tgz |
2.將解壓目錄移動到有效應用程式目錄中(如Windows上的
1 |
~$ mv spark-1.2.0-bin-hadoop2.4 /srv/spark-1.2.0 |
3.建立指向該Spark版本的符號連結到<spark目錄。這樣你可以簡單地下載新/舊版本的Spark,然後修改連結來管理Spark版本,而不用更改路徑或環境變數。
1 |
~$ ln -s /srv/spark-1.2.0 /srv/spark |
4.修改BASH配置,將Spark新增到PATH中,設定SPARK_HOME環境變數。這些小技巧在命令列上會幫到你。在Ubuntu上,只要編輯~/.bash_profile或~/.profile檔案,將以下語句新增到檔案中:
1 2 |
export SPARK_HOME=/srv/spark export PATH=$SPARK_HOME/bin:$PATH |
5.source這些配置(或者重啟終端)之後,你就可以在本地執行一個pyspark直譯器。執行pyspark命令,你會看到以下結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
~$ pyspark Python 2.7.8 (default, Dec 2 2014, 12:45:58) [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.54)] on darwin Type "help", "copyright", "credits" or "license" for more information. Spark assembly has been built with Hive, including Datanucleus jars on classpath Using Sparks default log4j profile: org/apache/spark/log4j-defaults.properties [… snip …] Welcome to ____ __ / __/__ ___ _____/ /__ _\ \/ _ \/ _ `/ __/ `_/ /__ / .__/\_,_/_/ /_/\_\ version 1.2.0 /_/ Using Python version 2.7.8 (default, Dec 2 2014 12:45:58) SparkContext available as sc. >>> |
現在Spark已經安裝完畢,可以在本機以”單機模式“(standalone mode)使用。你可以在本機開發應用並提交Spark作業,這些作業將以多程式/多執行緒模式執行的,或者,配置該機器作為一個叢集的客戶端(不推薦這樣做,因為在Spark作業中,驅動程式(driver)是個很重要的角色,並且應該與叢集的其他部分處於相同網路)。可能除了開發,你在本機使用Spark做得最多的就是利用spark-ec2指令碼來配置Amazon雲上的一個EC2 Spark叢集了。
簡略Spark輸出
Spark(和PySpark)的執行可以特別詳細,很多INFO日誌訊息都會列印到螢幕。開發過程中,這些非常惱人,因為可能丟失Python棧跟蹤或者print的輸出。為了減少Spark輸出 – 你可以設定$SPARK_HOME/conf下的log4j。首先,拷貝一份$SPARK_HOME/conf/log4j.properties.template檔案,去掉“.template”副檔名。
1 |
~$ cp $SPARK_HOME/conf/log4j.properties.template $SPARK_HOME/conf/log4j.properties |
編輯新檔案,用WARN替換程式碼中出現的INFO。你的log4j.properties檔案類似:
1 2 3 4 5 6 7 8 9 10 11 |
# Set everything to be logged to the console log4j.rootCategory=WARN, console log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.target=System.err log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n # Settings to quiet third party logs that are too verbose log4j.logger.org.eclipse.jetty=WARN log4j.logger.org.eclipse.jetty.util.component.AbstractLifeCycle=ERROR log4j.logger.org.apache.spark.repl.SparkIMain$exprTyper=WARN log4j.logger.org.apache.spark.repl.SparkILoop$SparkILoopInterpreter=WARN |
現在執行PySpark,輸出訊息將會更簡略!感謝@genomegeek在一次District Data Labs的研討會中指出這一點。
在Spark中使用IPython Notebook
當搜尋有用的Spark小技巧時,我發現了一些文章提到在PySpark中配置IPython notebook。IPython notebook對資料科學家來說是個互動地呈現科學和理論工作的必備工具,它整合了文字和Python程式碼。對很多資料科學家,IPython notebook是他們的Python入門,並且使用非常廣泛,所以我想值得在本文中提及。
這裡的大部分說明都來改編自IPython notebook: 在PySpark中設定IPython。但是,我們將聚焦在本機以單機模式將IPtyon shell連線到PySpark,而不是在EC2叢集。如果你想在一個叢集上使用PySpark/IPython,檢視並評論下文的說明吧!
- 1.為Spark建立一個iPython notebook配置
1 2 3 4 |
~$ ipython profile create spark [ProfileCreate] Generating default config file: u'$HOME/.ipython/profile_spark/ipython_config.py' [ProfileCreate] Generating default config file: u'$HOME/.ipython/profile_spark/ipython_notebook_config.py' [ProfileCreate] Generating default config file: u'$HOME/.ipython/profile_spark/ipython_nbconvert_config.py' |
記住配置檔案的位置,替換下文各步驟相應的路徑:
2.建立檔案$HOME/.ipython/profile_spark/startup/00-pyspark-setup.py,並新增如下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import os import sys # Configure the environment if 'SPARK_HOME' not in os.environ: os.environ['SPARK_HOME'] = '/srv/spark' # Create a variable for our root path SPARK_HOME = os.environ['SPARK_HOME'] # Add the PySpark/py4j to the Python Path sys.path.insert(0, os.path.join(SPARK_HOME, "python", "build")) sys.path.insert(0, os.path.join(SPARK_HOME, "python")) |
3.使用我們剛剛建立的配置來啟動IPython notebook。
1 |
~$ ipython notebook --profile spark |
4.在notebook中,你應該能看到我們剛剛建立的變數。
1 |
print SPARK_HOME |
5.在IPython notebook最上面,確保你新增了Spark context。
1 2 |
from pyspark import SparkContext sc = SparkContext( 'local', 'pyspark') |
6.使用IPython做個簡單的計算來測試Spark context。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
def isprime(n): """ check if integer n is a prime """ # make sure n is a positive integer n = abs(int(n)) # 0 and 1 are not primes if n < 2: return False # 2 is the only even prime number if n == 2: return True # all other even numbers are not primes if not n & 1: return False # range starts with 3 and only needs to go up the square root of n # for all odd numbers for x in range(3, int(n**0.5)+1, 2): if n % x == 0: return False return True # Create an RDD of numbers from 0 to 1,000,000 nums = sc.parallelize(xrange(1000000)) # Compute the number of primes in the RDD print nums.filter(isprime).count() |
如果你能得到一個數字而且沒有錯誤發生,那麼你的context正確工作了!
編輯提示:上面配置了一個使用PySpark直接呼叫IPython notebook的IPython context。但是,你也可以使用PySpark按以下方式直接啟動一個notebook: $ IPYTHON_OPTS=”notebook –pylab inline” pyspark
哪個方法好用取決於你使用PySpark和IPython的具體情景。前一個允許你更容易地使用IPython notebook連線到一個叢集,因此是我喜歡的方法。
在EC2上使用Spark
在講授使用Hadoop進行分散式計算時,我發現很多可以通過在本地偽分散式節點(pseudo-distributed node)或以單節點模式(single-node mode)講授。但是為了瞭解真正發生了什麼,就需要一個叢集。當資料變得龐大,這些書面講授的技能和真實計算需求間經常出現隔膜。如果你肯在學習詳細使用Spark上花錢,我建議你設定一個快速Spark叢集做做實驗。 包含5個slave(和1個master)每週大概使用10小時的叢集每月大概需要$45.18。
完整的討論可以在Spark文件中找到:在EC2上執行Spark在你決定購買EC2叢集前一定要通讀這篇文件!我列出了一些關鍵點:
- 通過AWS Console獲取AWS EC2 key對(訪問key和金鑰key)。
- 將key對匯出到你的環境中。在shell中敲出以下命令,或者將它們新增到配置中。
1 2 |
export AWS_ACCESS_KEY_ID=myaccesskeyid export AWS_SECRET_ACCESS_KEY=mysecretaccesskey |
注意不同的工具使用不同的環境名稱,確保你用的是Spark指令碼所使用的名稱。
3.啟動叢集:
1 2 |
~$ cd $SPARK_HOME/ec2 ec2$ ./spark-ec2 -k <keypair> -i <key-file> -s <num-slaves> launch <cluster-name> |
4.SSH到叢集來執行Spark作業。
1 |
ec2$ ./spark-ec2 -k <keypair> -i <key-file> login <cluster-name> |
5.銷燬叢集
1 |
ec2$ ./spark-ec2 destroy <cluster-name>. |
這些指令碼會自動建立一個本地的HDFS叢集來新增資料,copy-dir命令可以同步程式碼和資料到該叢集。但是你最好使用S3來儲存資料,建立使用s3://URI來載入資料的RDDs。
Spark是什麼?
既然設定好了Spark,現在我們討論下Spark是什麼。Spark是個通用的叢集計算框架,通過將大量資料集計算任務分配到多臺計算機上,提供高效記憶體計算。如果你熟悉Hadoop,那麼你知道分散式計算框架要解決兩個問題:如何分發資料和如何分發計算。Hadoop使用HDFS來解決分散式資料問題,MapReduce計算正規化提供有效的分散式計算。類似的,Spark擁有多種語言的函數語言程式設計API,提供了除map和reduce之外更多的運算子,這些操作是通過一個稱作彈性分散式資料集(resilient distributed datasets, RDDs)的分散式資料框架進行的。
本質上,RDD是種程式設計抽象,代表可以跨機器進行分割的只讀物件集合。RDD可以從一個繼承結構(lineage)重建(因此可以容錯),通過並行操作訪問,可以讀寫HDFS或S3這樣的分散式儲存,更重要的是,可以快取到worker節點的記憶體中進行立即重用。由於RDD可以被快取在記憶體中,Spark對迭代應用特別有效,因為這些應用中,資料是在整個演算法運算過程中都可以被重用。大多數機器學習和最優化演算法都是迭代的,使得Spark對資料科學來說是個非常有效的工具。另外,由於Spark非常快,可以通過類似Python REPL的命令列提示符互動式訪問。
Spark庫本身包含很多應用元素,這些元素可以用到大部分大資料應用中,其中包括對大資料進行類似SQL查詢的支援,機器學習和圖演算法,甚至對實時流資料的支援。
核心元件如下:
- Spark Core:包含Spark的基本功能;尤其是定義RDD的API、操作以及這兩者上的動作。其他Spark的庫都是構建在RDD和Spark Core之上的。
- Spark SQL:提供通過Apache Hive的SQL變體Hive查詢語言(HiveQL)與Spark進行互動的API。每個資料庫表被當做一個RDD,Spark SQL查詢被轉換為Spark操作。對熟悉Hive和HiveQL的人,Spark可以拿來就用。
- Spark Streaming:允許對實時資料流進行處理和控制。很多實時資料庫(如Apache Store)可以處理實時資料。Spark Streaming允許程式能夠像普通RDD一樣處理實時資料。
- MLlib:一個常用機器學習演算法庫,演算法被實現為對RDD的Spark操作。這個庫包含可擴充套件的學習演算法,比如分類、迴歸等需要對大量資料集進行迭代的操作。之前可選的大資料機器學習庫Mahout,將會轉到Spark,並在未來實現。
- GraphX:控制圖、並行圖操作和計算的一組演算法和工具的集合。GraphX擴充套件了RDD API,包含控制圖、建立子圖、訪問路徑上所有頂點的操作。
由於這些元件滿足了很多大資料需求,也滿足了很多資料科學任務的演算法和計算上的需要,Spark快速流行起來。不僅如此,Spark也提供了使用Scala、Java和Python編寫的API;滿足了不同團體的需求,允許更多資料科學家簡便地採用Spark作為他們的大資料解決方案。
對Spark程式設計
編寫Spark應用與之前實現在Hadoop上的其他資料流語言類似。程式碼寫入一個惰性求值的驅動程式(driver program)中,通過一個動作(action),驅動程式碼被分發到叢集上,由各個RDD分割槽上的worker來執行。然後結果會被髮送回驅動程式進行聚合或編譯。本質上,驅動程式建立一個或多個RDD,呼叫操作來轉換RDD,然後呼叫動作處理被轉換後的RDD。
這些步驟大體如下:
- 定義一個或多個RDD,可以通過獲取儲存在磁碟上的資料(HDFS,Cassandra,HBase,Local Disk),並行化記憶體中的某些集合,轉換(transform)一個已存在的RDD,或者,快取或儲存。
- 通過傳遞一個閉包(函式)給RDD上的每個元素來呼叫RDD上的操作。Spark提供了除了Map和Reduce的80多種高階操作。
- 使用結果RDD的動作(action)(如count、collect、save等)。動作將會啟動叢集上的計算。
當Spark在一個worker上執行閉包時,閉包中用到的所有變數都會被拷貝到節點上,但是由閉包的區域性作用域來維護。Spark提供了兩種型別的共享變數,這些變數可以按照限定的方式被所有worker訪問。廣播變數會被分發給所有worker,但是是隻讀的。累加器這種變數,worker可以使用關聯操作來“加”,通常用作計數器。
Spark應用本質上通過轉換和動作來控制RDD。後續文章將會深入討論,但是理解了這個就足以執行下面的例子了。
Spark的執行
簡略描述下Spark的執行。本質上,Spark應用作為獨立的程式執行,由驅動程式中的SparkContext協調。這個context將會連線到一些叢集管理者(如YARN),這些管理者分配系統資源。叢集上的每個worker由執行者(executor)管理,執行者反過來由SparkContext管理。執行者管理計算、儲存,還有每臺機器上的快取。
重點要記住的是應用程式碼由驅動程式傳送給執行者,執行者指定context和要執行的任務。執行者與驅動程式通訊進行資料分享或者互動。驅動程式是Spark作業的主要參與者,因此需要與叢集處於相同的網路。這與Hadoop程式碼不同,Hadoop中你可以在任意位置提交作業給JobTracker,JobTracker處理叢集上的執行。
與Spark互動
使用Spark最簡單的方式就是使用互動式命令列提示符。開啟PySpark終端,在命令列中打出pyspark。
1 2 3 |
~$ pyspark [… snip …] >>> |
PySpark將會自動使用本地Spark配置建立一個SparkContext。你可以通過sc變數來訪問它。我們來建立第一個RDD。
1 2 3 |
>>> text = sc.textFile("shakespeare.txt") >>> print text shakespeare.txt MappedRDD[1] at textFile at NativeMethodAccessorImpl.java:-2 |
textFile方法將莎士比亞全部作品載入到一個RDD命名文字。如果檢視了RDD,你就可以看出它是個MappedRDD,檔案路徑是相對於當前工作目錄的一個相對路徑(記得傳遞磁碟上正確的shakespear.txt檔案路徑)。我們轉換下這個RDD,來進行分散式計算的“hello world”:“字數統計”。
1 2 3 4 5 6 7 |
>>> from operator import add >>> def tokenize(text): ... return text.split() ... >>> words = text.flatMap(tokenize) >>> print words PythonRDD[2] at RDD at PythonRDD.scala:43 |
我們首先匯入了add操作符,它是個命名函式,可以作為加法的閉包來使用。我們稍後再使用這個函式。首先我們要做的是把文字拆分為單詞。我們建立了一個tokenize函式,引數是文字片段,返回根據空格拆分的單詞列表。然後我們通過給flatMap操作符傳遞tokenize閉包對textRDD進行變換建立了一個wordsRDD。你會發現,words是個PythonRDD,但是執行本應該立即進行。顯然,我們還沒有把整個莎士比亞資料集拆分為單詞列表。
如果你曾使用MapReduce做過Hadoop版的“字數統計”,你應該知道下一步是將每個單詞對映到一個鍵值對,其中鍵是單詞,值是1,然後使用reducer計算每個鍵的1總數。
首先,我們map一下。
1 2 3 4 5 |
>>> wc = words.map(lambda x: (x,1)) >>> print wc.toDebugString() (2) PythonRDD[3] at RDD at PythonRDD.scala:43 | shakespeare.txt MappedRDD[1] at textFile at NativeMethodAccessorImpl.java:-2 | shakespeare.txt HadoopRDD[0] at textFile at NativeMethodAccessorImpl.java:-2 |
我使用了一個匿名函式(用了Python中的lambda關鍵字)而不是命名函式。這行程式碼將會把lambda對映到每個單詞。因此,每個x都是一個單詞,每個單詞都會被匿名閉包轉換為元組(word, 1)。為了檢視轉換關係,我們使用toDebugString方法來檢視PipelinedRDD是怎麼被轉換的。可以使用reduceByKey動作進行字數統計,然後把統計結果寫到磁碟。
1 2 |
>>> counts = wc.reduceByKey(add) >>> counts.saveAsTextFile("wc") |
一旦我們最終呼叫了saveAsTextFile動作,這個分散式作業就開始執行了,在作業“跨叢集地”(或者你本機的很多程式)執行時,你應該可以看到很多INFO語句。如果退出直譯器,你可以看到當前工作目錄下有個“wc”目錄。
1 2 |
$ ls wc/ _SUCCESS part-00000 part-00001 |
每個part檔案都代表你本機上的程式計算得到的被保持到磁碟上的最終RDD。如果對一個part檔案進行head命令,你應該能看到字數統計元組。
1 2 3 4 5 6 7 8 9 10 11 |
$ head wc/part-00000 (u'fawn', 14) (u'Fame.', 1) (u'Fame,', 2) (u'kinghenryviii@7731', 1) (u'othello@36737', 1) (u'loveslabourslost@51678', 1) (u'1kinghenryiv@54228', 1) (u'troilusandcressida@83747', 1) (u'fleeces', 1) (u'midsummersnightsdream@71681', 1) |
注意這些鍵沒有像Hadoop一樣被排序(因為Hadoop中Map和Reduce任務中有個必要的打亂和排序階段)。但是,能保證每個單詞在所有檔案中只出現一次,因為你使用了reduceByKey操作符。你還可以使用sort操作符確保在寫入到磁碟之前所有的鍵都被排過序。
編寫一個Spark應用
編寫Spark應用與通過互動式控制檯使用Spark類似。API是相同的。首先,你需要訪問<SparkContext,它已經由<pyspark自動載入好了。
使用Spark編寫Spark應用的一個基本模板如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
## Spark Application - execute with spark-submit ## Imports from pyspark import SparkConf, SparkContext ## Module Constants APP_NAME = "My Spark Application" ## Closure Functions ## Main functionality def main(sc): pass if __name__ == "__main__": # Configure Spark conf = SparkConf().setAppName(APP_NAME) conf = conf.setMaster("local[*]") sc = SparkContext(conf=conf) # Execute Main functionality main(sc) |
這個模板列出了一個Spark應用所需的東西:匯入Python庫,模組常量,用於除錯和Spark UI的可識別的應用名稱,還有作為驅動程式執行的一些主要分析方法學。在ifmain中,我們建立了SparkContext,使用了配置好的context執行main。我們可以簡單地匯入驅動程式碼到pyspark而不用執行。注意這裡Spark配置通過setMaster方法被硬編碼到SparkConf,一般你應該允許這個值通過命令列來設定,所以你能看到這行做了佔位符註釋。
使用<sc.stop()或<sys.exit(0)來關閉或退出程式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
## Spark Application - execute with spark-submit ## Imports import csv import matplotlib.pyplot as plt from StringIO import StringIO from datetime import datetime from collections import namedtuple from operator import add, itemgetter from pyspark import SparkConf, SparkContext ## Module Constants APP_NAME = "Flight Delay Analysis" DATE_FMT = "%Y-%m-%d" TIME_FMT = "%H%M" fields = ('date', 'airline', 'flightnum', 'origin', 'dest', 'dep', 'dep_delay', 'arv', 'arv_delay', 'airtime', 'distance') Flight = namedtuple('Flight', fields) ## Closure Functions def parse(row): """ Parses a row and returns a named tuple. """ row[0] = datetime.strptime(row[0], DATE_FMT).date() row[5] = datetime.strptime(row[5], TIME_FMT).time() row[6] = float(row[6]) row[7] = datetime.strptime(row[7], TIME_FMT).time() row[8] = float(row[8]) row[9] = float(row[9]) row[10] = float(row[10]) return Flight(*row[:11]) def split(line): """ Operator function for splitting a line with csv module """ reader = csv.reader(StringIO(line)) return reader.next() def plot(delays): """ Show a bar chart of the total delay per airline """ airlines = [d[0] for d in delays] minutes = [d[1] for d in delays] index = list(xrange(len(airlines))) fig, axe = plt.subplots() bars = axe.barh(index, minutes) # Add the total minutes to the right for idx, air, min in zip(index, airlines, minutes): if min > 0: bars[idx].set_color('#d9230f') axe.annotate(" %0.0f min" % min, xy=(min+1, idx+0.5), va='center') else: bars[idx].set_color('#469408') axe.annotate(" %0.0f min" % min, xy=(10, idx+0.5), va='center') # Set the ticks ticks = plt.yticks([idx+ 0.5 for idx in index], airlines) xt = plt.xticks()[0] plt.xticks(xt, [' '] * len(xt)) # minimize chart junk plt.grid(axis = 'x', color ='white', linestyle='-') plt.title('Total Minutes Delayed per Airline') plt.show() ## Main functionality def main(sc): # Load the airlines lookup dictionary airlines = dict(sc.textFile("ontime/airlines.csv").map(split).collect()) # Broadcast the lookup dictionary to the cluster airline_lookup = sc.broadcast(airlines) # Read the CSV Data into an RDD flights = sc.textFile("ontime/flights.csv").map(split).map(parse) # Map the total delay to the airline (joined using the broadcast value) delays = flights.map(lambda f: (airline_lookup.value[f.airline], add(f.dep_delay, f.arv_delay))) # Reduce the total delay for the month to the airline delays = delays.reduceByKey(add).collect() delays = sorted(delays, key=itemgetter(1)) # Provide output from the driver for d in delays: print "%0.0f minutes delayed\t%s" % (d[1], d[0]) # Show a bar chart of the delays plot(delays) if __name__ == "__main__": # Configure Spark conf = SparkConf().setMaster("local[*]") conf = conf.setAppName(APP_NAME) sc = SparkContext(conf=conf) # Execute Main functionality main(sc) |
使用<spark-submit命令來執行這段程式碼(假設你已有ontime目錄,目錄中有兩個CSV檔案):
1 |
~$ spark-submit app.py |
這個Spark作業使用本機作為master,並搜尋app.py同目錄下的ontime目錄下的2個CSV檔案。最終結果顯示,4月的總延誤時間(單位分鐘),既有早點的(如果你從美國大陸飛往夏威夷或者阿拉斯加),但對大部分大型航空公司都是延誤的。注意,我們在app.py中使用matplotlib直接將結果視覺化出來了:
這段程式碼做了什麼呢?我們特別注意下與Spark最直接相關的main函式。首先,我們載入CSV檔案到RDD,然後把split函式對映給它。split函式使用csv模組解析文字的每一行,並返回代表每行的元組。最後,我們將collect動作傳給RDD,這個動作把資料以Python列表的形式從RDD傳回驅動程式。本例中,airlines.csv是個小型的跳轉表(jump table),可以將航空公司程式碼與全名對應起來。我們將轉移表儲存為Python字典,然後使用sc.broadcast廣播給叢集上的每個節點。
接著,main函式載入了資料量更大的flights.csv([譯者注]作者筆誤寫成fights.csv,此處更正)。拆分CSV行完成之後,我們將parse函式對映給CSV行,此函式會把日期和時間轉成Python的日期和時間,並對浮點數進行合適的型別轉換。每行作為一個NamedTuple儲存,名為Flight,以便高效簡便地使用。
有了Flight物件的RDD,我們對映一個匿名函式,這個函式將RDD轉換為一些列的鍵值對,其中鍵是航空公司的名字,值是到達和出發的延誤時間總和。使用reduceByKey動作和add操作符可以得到每個航空公司的延誤時間總和,然後RDD被傳遞給驅動程式(資料中航空公司的數目相對較少)。最終延誤時間按照升序排列,輸出列印到了控制檯,並且使用matplotlib進行了視覺化。
這個例子稍長,但是希望能演示出叢集和驅動程式之間的相互作用(傳送資料進行分析,結果取回給驅動程式),以及Python程式碼在Spark應用中的角色。
結論
儘管算不上一個完整的Spark入門,我們希望你能更好地瞭解Spark是什麼,如何使用進行快速、記憶體分散式計算。至少,你應該能將Spark執行起來,並開始在本機或Amazon EC2上探索資料。你應該可以配置好iPython notebook來執行Spark。
Spark不能解決分散式儲存問題(通常Spark從HDFS中獲取資料),但是它為分散式計算提供了豐富的函數語言程式設計API。這個框架建立在伸縮分散式資料集(RDD)之上。RDD是種程式設計抽象,代表被分割槽的物件集合,允許進行分散式操作。RDD有容錯能力(可伸縮的部分),更重要的時,可以儲存到節點上的worker記憶體裡進行立即重用。記憶體儲存提供了快速和簡單表示的迭代演算法,以及實時互動分析。
由於Spark庫提供了Python、Scale、Java編寫的API,以及內建的機器學習、流資料、圖演算法、類SQL查詢等模組;Spark迅速成為當今最重要的分散式計算框架之一。與YARN結合,Spark提供了增量,而不是替代已存在的Hadoop叢集,它將成為未來大資料重要的一部分,為資料科學探索鋪設了一條康莊大道。
有用的連結
希望你喜歡這篇博文!寫作並不是憑空而來的,以下是一些曾幫助我寫作的有用連結;檢視這些連結,可能對進一步探索Spark有幫助。注意,有些圖書連結是推廣連結,意味著如果你點選併購買了這些圖書,你將會支援District Data Labs!
這篇更多是篇入門文章,而不是District Data Labs的典型文章,有些與此入門相關的資料和程式碼你可以在這裡找到:
Spark論文
Spark與Hadoop一樣,有一些基礎論文,我認為那些需要對大資料集進行分散式計算的嚴謹資料科學家一定要讀。首先是HotOS(“作業系統熱門話題”的簡寫)的一篇研討會論文,簡單易懂地描述了Spark。第二個是偏理論的論文,具體描述了RDD。
- M. Zaharia, M. Chowdhury, M. J. Franklin, S. Shenker, and I. Stoica, “Spark: cluster computing with working sets,” in Proceedings of the 2nd USENIX conference on Hot topics in cloud computing, 2010, pp. 10–10.
- M. Zaharia, M. Chowdhury, T. Das, A. Dave, J. Ma, M. McCauley, M. J. Franklin, S. Shenker, and I. Stoica, “Resilient distributed datasets: A fault-tolerant abstraction for in-memory cluster computing,” in Proceedings of the 9th USENIX conference on Networked Systems Design and Implementation, 2012, pp. 2–2.