Linux環境Spark安裝配置及使用
1. 認識Spark
(1) Spark介紹
- 大資料計算引擎
- 官網:spark.apache.org/
- 官方介紹:Apache Spark™ is a unified analytics engine for large-scale data processing.(Apache Spark™是一個用於大規模資料處理的統一分析引擎。)
- Spark是一種快速、通用、可擴充套件的大資料分析引擎,2009年誕生於加州大學伯克利分校AMPLab,2010年開源,2013年6月成為Apache孵化專案,2014年2月成為Apache頂級專案。目前,Spark生態系統已經發展成為一個包含多個子專案的集合,其中包含SparkSQL、Spark Streaming、GraphX、MLlib等子專案,Spark是基於記憶體計算的大資料平行計算框架。Spark基於記憶體計算,提高了在大資料環境下資料處理的實時性,同時保證了高容錯性和高可伸縮性,允許使用者將Spark部署在大量廉價硬體之上,形成叢集。
- Spark生態圈:
- Spark Core:RDD(彈性分散式資料集)
- Spark SQL
- Spark Streaming
- Spark MLLib:協同過濾,ALS,邏輯迴歸等等 --> 機器學習
- Spark Graphx:圖計算
(2) 為什麼要學習Spark
- Hadoop的MapReduce計算模型存在的問題:
-
MapReduce的核心是Shuffle(洗牌)。在整個Shuffle的過程中,至少會產生6次的I/O。
-
中間結果輸出:基於MapReduce的計算引擎通常會將中間結果輸出到磁碟上,進行儲存和容錯。另外,當一些查詢(如:Hive)翻譯到MapReduce任務時,往往會產生多個Stage(階段),而這些串聯的Stage又依賴於底層檔案系統(如HDFS)來儲存每一個Stage的輸出結果,而I/O的效率往往較低,從而影響了MapReduce的執行速度。
-
- Spark的最大特點:基於記憶體
- Spark是MapReduce的替代方案,而且相容HDFS、Hive,可融入Hadoop的生態系統,彌補MapReduce的不足。
(3) Spark的特點:快、易用、通用、相容
- 快——與Hadoop的MapReduce相比,Spark基於記憶體的運算速度要快100倍以上,即使,Spark基於硬碟的運算也要快10倍。Spark實現了高效的DAG執行引擎,從而可以通過記憶體來高效處理資料流。
- 易用——Spark支援Java、Python和Scala的API,還支援超過80種高階演算法,使使用者可以快速構建不同的應用。而且Spark支援互動式的Python和Scala的shell,可以非常方便地在這些shell中使用Spark叢集來驗證解決問題的方法。
- 通用——Spark提供了統一的解決方案。Spark可以用於批處理、互動式查詢(Spark SQL)、實時流處理(Spark Streaming)、機器學習(Spark MLlib)和圖計算(GraphX)。這些不同型別的處理都可以在同一個應用中無縫使用。Spark統一的解決方案非常具有吸引力,畢竟任何公司都想用統一的平臺去處理遇到的問題,減少開發和維護的人力成本和部署平臺的物力成本。另外Spark還可以很好的融入Hadoop的體系結構中可以直接操作HDFS,並提供Hive on Spark、Pig on Spark的框架整合Hadoop。
- 相容——Spark可以非常方便地與其他的開源產品進行融合。比如,Spark可以使用Hadoop的YARN和ApacheMesos作為它的資源管理和排程器,並且可以處理所有Hadoop支援的資料,包括HDFS、HBase和Cassandra等。這對於已經部署Hadoop叢集的使用者特別重要,因為不需要做任何資料遷移就可以使用Spark的強大處理能力。Spark也可以不依賴於第三方的資源管理和排程器,它實現了Standalone作為其內建的資源管理和排程框架,這樣進一步降低了Spark的使用門檻,使得所有人都可以非常容易地部署和使用Spark。此外,Spark還提供了在EC2上部署Standalone的Spark叢集的工具。
2. Spark體系架構
- Spark的執行方式
- Yarn
- Standalone:本機除錯(demo)
- Worker(從節點):每個伺服器上,資源和任務的管理者,只負責管理一個節點。
- 執行過程:
- 一個Worker 有多個 Executor。 Executor是任務的執行者,按階段(stage)劃分任務。—> RDD
- 客戶端:Driver Program 提交任務到叢集中。
- spark-submit
- spark-shell
3. Spark-2.1.0安裝流程
(1) 準備工作
- 具備java環境
- 配置主機名
- 配置免密碼登入
- 防火牆關閉
(2) 解壓spark-2.1.0-bin-hadoop2.7.tgz安裝包到目標目錄下:
tar -zxvf .tar.gz -C 目標目錄
(3) 為後續方便,重新命名Spark資料夾:
mv spark-2.1.0-bin-hadoop2.7/ spark-2.1.0
(4) Spark目錄介紹
- bin —— Spark操作命令
- conf —— 配置檔案
- data —— Spark測試檔案
- examples —— Spark示例程式
- jars
- LICENSE
- licenses
- NOTICE
- python
- R
- README.md
- RELEASE
- sbin —— Spark叢集命令
- yarn —— Spark-yarn配置
(5) 修改配置檔案:
- <1>. 配置spark-env.sh:
- 進入spark-2.1.0/conf路徑,重新命名配置檔案:
mv spark-env.sh.template spark-env.sh
- 修改spark-env.sh資訊:
vi spark-env.sh
-
export JAVA_HOME=/opt/module/jdk1.8.0_144 export SPARK_MASTER_HOST=bigdata01 export SPARK_MASTER_PORT=7077 複製程式碼
- 進入spark-2.1.0/conf路徑,重新命名配置檔案:
- <2>. 配置slaves:
- 進入spark-2.1.0/conf路徑,重新命名配置檔案:
mv slaves.template slaves
- 修改slaves資訊:
vi slaves
-
bigdata02 bigdata03 複製程式碼
- 進入spark-2.1.0/conf路徑,重新命名配置檔案:
(6) 配置環境變數:
- 修改配置檔案:
vi /etc/profile
- 增加以下內容:
export SPARK_HOME=spark安裝路徑
export PATH=$PATH:$SPARK_HOME/bin
export PATH=$PATH:$SPARK_HOME/sbin
- 宣告環境變數:
source /etc/profile
(6) 叢集配置:
- 拷貝配置好的spark到其他機器上
scp -r spark-2.1.0/ bigdata02:$PWD
scp -r spark-2.1.0/ bigdata03:$PWD
(7) 啟動:
- 啟動主節點:
start-master.sh
- 啟動從節點:
start-slaves.sh
- 啟動shell:
spark-shell
- 通過網頁端檢視:
- http://bigdata01:8080/
- Spark中內建有Tomcat,故埠號預設為8080
(8) 關閉:
- 關閉主節點:
stop-master.sh
- 關閉從節點:
stop-slaves.sh
4. Spark HA的實現
(1) 基於檔案系統的單點恢復
-
主要用於開發或測試環境。
-
當spark提供目錄儲存spark Application和worker的註冊資訊,並將他們的恢復狀態寫入該目錄中,一旦Master發生故障,就可以通過重新啟動Master程式(sbin/start-master.sh),恢復已執行的spark Application和worker的註冊資訊。
-
基於檔案系統的單點恢復,主要是在spark-env.sh裡對SPARK_DAEMON_JAVA_OPTS設定
- 建立存放資料夾:
mkdir /opt/module/spark-2.1.0/recovery
- 修改配置資訊:
vi spark-env.sh
- 增加內容:
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=FILESYSTEM -Dspark.deploy.recoveryDirectory=/opt/module/spark-2.1.0/recovery"
- 建立存放資料夾:
(2) 基於Zookeeper的Standby Masters
-
適用於現實生產。
-
ZooKeeper提供了一個Leader Election機制,利用這個機制可以保證雖然叢集存在多個Master,但是隻有一個是Active的,其他的都是Standby。當Active的Master出現故障時,另外的一個Standby Master會被選舉出來。由於叢集的資訊,包括Worker,Driver和Application的資訊都已經持久化到ZooKeeper,因此在切換的過程中只會影響新Job的提交,對於正在進行的Job沒有任何的影響。加入ZooKeeper的叢集整體架構如下圖所示:
-
修改配置資訊:
vi spark-env.sh
- 增加內容:
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=bigdata01:2181,bigdata02:2181,bigdata03:2181 -Dspark.deploy.zookeeper.dir=/spark"
- 註釋掉:
export SPARK_MASTER_HOST
和export SPARK_MASTER_PORT
-
傳送新的配置檔案到叢集其餘節點:
scp spark-env.sh bigdata02:$PWD
scp spark-env.sh bigdata03:$PWD
5. 執行Spark的任務
(1) spark-submit
- 用於提交Spark的任務(任務即相關jar包)
- e.g.: 蒙特卡洛求PI(圓周率)
- 原理:如下圖所示,隨機向正方形內落點,通過統計正方形內所有點數和落入圓內的點數來計算佔比,得出正方形與圓的面積近似比值,進而近似出PI值。
- 命令:
spark-submit --master spark://XXXX:7077
(指明master地址)--class org.apache.spark.examples.SparkPi
(指明主程式的名字)/XXXX/spark/examples/jars/spark-examples_2.11-2.1.0.jar
(指明jar包地址)100
(指明執行次數)
(2) spark-shell
- 相當於REPL,作為一個獨立的Application執行
- spark-shell是Spark自帶的互動式Shell程式,方便使用者進行互動式程式設計,使用者可以在該命令列下用scala編寫spark程式。
- 引數說明:
--master spark://XXXX:7077
指定Master的地址--executor-memory 2g
指定每個worker可用記憶體為2G--total-executor-cores 2
指定整個叢集使用的cup核數為2個
- Spark Session 是 2.0 以後提供的,利用 SparkSession 可以訪問spark所有元件
- 兩種執行模式:
- <1>. 本地模式
- 啟動:
spark-shell
(後面不接任何引數)
- 啟動:
- <2>. 叢集模式
- 啟動:
spark-shell --master spark://XXXX:7077
(指明master地址)
- 啟動:
- <1>. 本地模式
- e.g.: 編寫WordCount程式
- <1>. 處理本地檔案,把結果列印到螢幕上
- 啟動:
spark-shell
- 傳入檔案:
sc.textFile("/XXXX/WordCount.txt")
(本地檔案路徑).flatMap(_.split(" "))
(按照空格分割).map((_,1))
(單詞遍歷).reduceByKey(_+_)
(單詞計數).collect
- 啟動:
- <2>. 處理HDFS檔案,結果儲存在hdfs上
- 啟動:
spark-shell --master spark://XXXX:7077
(指 - sc.textFile("hdfs://XXXX:9000/sp_wc.txt").flatMap(.split(" ")).map((,1)).reduceByKey(+).saveAsTextFile("hdfs://XXXX:9000/output/spark/WordCount")
- 啟動:
- <1>. 處理本地檔案,把結果列印到螢幕上
(3) 單步執行WordCount -> RDD
- 啟動shell:
spark-shell
-
scala> val rdd1 = sc.textFile("/root/sp_wc.txt") rdd1: org.apache.spark.rdd.RDD[String] = /root/sp_wc.txt MapPartitionsRDD[1] at textFile at <console>:24 scala> rdd1.collect res0: Array[String] = Array(I love Scala, I love Skark, 2019/5/8) scala> val rdd2 = rdd1.flatMap(_.split(" ")) rdd2: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[2] at flatMap at <console>:26 scala> rdd2.collect res1: Array[String] = Array(I, love, Scala, I, love, Skark, 2019/5/8) scala> val rdd3 = rdd2.map((_,1)) rdd3: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[3] at map at <console>:28 scala> rdd3.collect res2: Array[(String, Int)] = Array((I,1), (love,1), (Scala,1), (I,1), (love,1), (Skark,1), (2019/5/8,1)) scala> val rdd4 = rdd3.reduceByKey(_+_) rdd4: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[4] at reduceByKey at <console>:30 scala> rdd4.collect res3: Array[(String, Int)] = Array((2019/5/8,1), (love,2), (I,2), (Skark,1), (Scala,1)) 複製程式碼
(4) 在IDE中執行WorkCount
- <1>. scala版本
-
import org.apache.spark.SparkConf import org.apache.spark.SparkContext object WordCount { def main(args: Array[String]): Unit = { //建立一個Spark配置檔案 val conf = new SparkConf().setAppName("Scala WordCount").setMaster("local") //建立Spark物件 val sc = new SparkContext(conf) val result = sc.textFile(args(0)) .flatMap(_.split(" ")) .map((_, 1)) .reduceByKey(_ + _) .saveAsTextFile(args(1)) sc.stop() } } 複製程式碼
-
- <2>. Java版本
-
import java.util.Arrays; import java.util.Iterator; import java.util.List; 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.Function2; import org.apache.spark.api.java.function.PairFunction; import parquet.format.PageHeader; import scala.Tuple2; public class WordCount { public static void main(String[] args) { // TODO Auto-generated method stub SparkConf conf = new SparkConf() .setAppName("JavaWordCount") .setMaster("local") ; //新建SparkContext物件 JavaSparkContext sc = new JavaSparkContext(conf) ; //讀入資料 JavaRDD<String> lines = sc.textFile("hdfs://XXXX:9000/WordCount.txt") ; //分詞 第一個參數列示讀進來的話 第二個參數列示 返回值 JavaRDD<String> words = lines.flatMap(new FlatMapFunction<String, String>() { @Override public Iterator<String> call(String input) throws Exception { return Arrays.asList(input.split(" ")).iterator() ; } }) ; //每個單詞記一次數 /* * String, String, Integer * input <key value> */ JavaPairRDD<String, Integer> ones = words.mapToPair(new PairFunction<String, String, Integer>() { @Override public Tuple2<String, Integer> call(String input) throws Exception { return new Tuple2<String, Integer>(input, 1) ; } }) ; //執行reduce操作 /* * Integer, Integer, Integer * nteger arg0, Integer arg1 返回值 */ JavaPairRDD<String,Integer> counts = ones.reduceByKey(new Function2<Integer, Integer, Integer>() { @Override public Integer call(Integer arg0, Integer arg1) throws Exception { // TODO Auto-generated method stub return arg0 + arg1 ; } }) ; //列印結果 List<Tuple2<String, Integer>> output = counts.collect() ; for (Tuple2<String, Integer> tuple :output) { System.out.println(tuple._1 + " : " + tuple._2) ; } sc.stop() ; } } 複製程式碼
-
(5) WordCount程式處理過程
(6) Spark提交任務的流程
6. Spark的運算元
(1) RDD基礎
- <1>. 什麼是RDD
- RDD(Resilient Distributed Dataset)叫做彈性分散式資料集,是Spark中最基本的資料抽象,它代表一個不可變、可分割槽、裡面的元素可平行計算的集合。RDD具有資料流模型的特點:自動容錯、位置感知性排程和可伸縮性。RDD允許使用者在執行多個查詢時顯式地將工作集快取在記憶體中,後續的查詢能夠重用工作集,這極大地提升了查詢速度。
- <2>. RDD的屬性(原始碼中的一段話)
- **一組分片(Partition)。**即資料集的基本組成單位。對於RDD來說,每個分片都會被一個計算任務處理,並決定平行計算的粒度。使用者可以在建立RDD時指定RDD的分片個數,如果沒有指定,那麼就會採用預設值。預設值就是程式所分配到的CPU Core的數目。
- **一個計算每個分割槽的函式。**Spark中RDD的計算是以分片為單位的,每個RDD都會實現compute函式以達到這個目的。compute函式會對迭代器進行復合,不需要儲存每次計算的結果。
- **RDD之間的依賴關係。**RDD的每次轉換都會生成一個新的RDD,所以RDD之間就會形成類似於流水線一樣的前後依賴關係。在部分分割槽資料丟失時,Spark可以通過這個依賴關係重新計算丟失的分割槽資料,而不是對RDD的所有分割槽進行重新計算。
- **一個Partitioner,即RDD的分片函式。**當前Spark中實現了兩種型別的分片函式,一個是基於雜湊的HashPartitioner,另外一個是基於範圍的RangePartitioner。只有對於於key-value的RDD,才會有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函式不但決定了RDD本身的分片數量,也決定了parent RDD Shuffle輸出時的分片數量。
- **一個列表。**儲存存取每個Partition的優先位置(preferred location)。對於一個HDFS檔案來說,這個列表儲存的就是每個Partition所在的塊的位置。按照“移動資料不如移動計算”的理念,Spark在進行任務排程的時候,會盡可能地將計算任務分配到其所要處理資料塊的儲存位置。
- <3>. RDD的建立方式
- 通過外部的資料檔案建立,如HDFS:
val rdd1 = sc.textFile(“hdfs://XXXX:9000/data.txt”)
- 通過sc.parallelize進行建立:
val rdd1 = sc.parallelize(Array(1,2,3,4,5,6,7,8))
- DD的型別:Transformation和Action
- 通過外部的資料檔案建立,如HDFS:
- <4>. RDD的基本原理
(2) Transformation
- RDD中的所有轉換都是延遲載入的,也就是說,它們並不會直接計算結果。相反的,它們只是記住這些應用到基礎資料集(例如一個檔案)上的轉換動作。只有當發生一個要求返回結果給Driver的動作時,這些轉換才會真正執行。這種設計讓Spark更加有效率地執行。
(3) Action
(4) RDD的快取機制
- RDD通過persist方法或cache方法可以將前面的計算結果快取,但是並不是這兩個方法被呼叫時立即快取,而是觸發後面的action時,該RDD將會被快取在計算節點的記憶體中,並供後面重用。
- 通過檢視原始碼發現cache最終也是呼叫了persist方法,預設的儲存級別都是僅在記憶體儲存一份,Spark的儲存級別還有好多種,儲存級別在object StorageLevel中定義的。
- 快取有可能丟失,或者儲存儲存於記憶體的資料由於記憶體不足而被刪除,RDD的快取容錯機制保證了即使快取丟失也能保證計算的正確執行。通過基於RDD的一系列轉換,丟失的資料會被重算,由於RDD的各個Partition是相對獨立的,因此只需要計算丟失的部分即可,並不需要重算全部Partition。
- Demo示例:
- 通過UI進行監控:
(5) RDD的Checkpoint(檢查點)機制:容錯機制
- 檢查點(本質是通過將RDD寫入Disk做檢查點)是為了通過lineage(血統)做容錯的輔助,lineage過長會造成容錯成本過高,這樣就不如在中間階段做檢查點容錯,如果之後有節點出現問題而丟失分割槽,從做檢查點的RDD開始重做Lineage,就會減少開銷。
- 設定checkpoint的目錄,可以是本地的資料夾、也可以是HDFS。一般是在具有容錯能力,高可靠的檔案系統上(比如HDFS, S3等)設定一個檢查點路徑,用於儲存檢查點資料。
- 分別舉例說明:
- <1>. 本地目錄
- 注意:這種模式,需要將spark-shell執行在本地模式上
- <2>. HDFS的目錄
- 注意:這種模式,需要將spark-shell執行在叢集模式上
(6) RDD的依賴關係和Spark任務中的Stage
-
RDD的依賴關係
-
RDD和它依賴的父RDD(s)的關係有兩種不同的型別,即窄依賴(narrow dependency)和寬依賴(wide dependency)。
-
窄依賴指的是每一個父RDD的Partition最多被子RDD的一個Partition使用
- 總結:窄依賴我們形象的比喻為獨生子女
-
寬依賴指的是多個子RDD的Partition會依賴同一個父RDD的Partition
- 總結:窄依賴我們形象的比喻為超生
-
-
Spark任務中的Stage
- DAG(Directed Acyclic Graph)叫做有向無環圖,原始的RDD通過一系列的轉換就就形成了DAG,根據RDD之間的依賴關係的不同將DAG劃分成不同的Stage,對於窄依賴,partition的轉換處理在Stage中完成計算。對於寬依賴,由於有Shuffle的存在,只能在parent RDD處理完成後,才能開始接下來的計算,因此寬依賴是劃分Stage的依據。
(7) RDD基礎練習
-
練習1:
-
//通過並行化生成rdd val rdd1 = sc.parallelize(List(5, 6, 4, 7, 3, 8, 2, 9, 1, 10)) //對rdd1裡的每一個元素乘2然後排序 val rdd2 = rdd1.map(_ * 2).sortBy(x => x, true) //過濾出大於等於十的元素 val rdd3 = rdd2.filter(_ >= 10) //將元素以陣列的方式在客戶端顯示 rdd3.collect 複製程式碼
-
練習2:
-
val rdd1 = sc.parallelize(Array("a b c", "d e f", "h i j")) //將rdd1裡面的每一個元素先切分在壓平 val rdd2 = rdd1.flatMap(_.split(' ')) rdd2.collect 複製程式碼
-
練習3:
-
val rdd1 = sc.parallelize(List(5, 6, 4, 3)) val rdd2 = sc.parallelize(List(1, 2, 3, 4)) //求並集 val rdd3 = rdd1.union(rdd2) //求交集 val rdd4 = rdd1.intersection(rdd2) //去重 rdd3.distinct.collect rdd4.collect 複製程式碼
-
練習4:
-
val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 3), ("kitty", 2))) val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2))) //求jion val rdd3 = rdd1.join(rdd2) rdd3.collect //求並集 val rdd4 = rdd1 union rdd2 //按key進行分組 rdd4.groupByKey rdd4.collect 複製程式碼
-
練習5:
-
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2))) val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2))) //cogroup val rdd3 = rdd1.cogroup(rdd2) //注意cogroup與groupByKey的區別 rdd3.collect 複製程式碼
-
練習6:
-
val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5)) //reduce聚合 val rdd2 = rdd1.reduce(_ + _) rdd2.collect 複製程式碼
-
練習7:
-
val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 3), ("kitty", 2), ("shuke", 1))) val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 3), ("shuke", 2), ("kitty", 5))) val rdd3 = rdd1.union(rdd2) //按key進行聚合 val rdd4 = rdd3.reduceByKey(_ + _) rdd4.collect //按value的降序排序 val rdd5 = rdd4.map(t => (t._2, t._1)).sortByKey(false).map(t => (t._2, t._1)) rdd5.collect 複製程式碼
7. Spark RDD的高階運算元
(1) mapPartitionsWithIndex
- 把每個partition中的分割槽號和對應的值拿出來
def mapPartitionsWithIndex[U](f: (Int, Iterator[T]) ⇒ Iterator[U], preservesPartitioning: Boolean = false)(implicit arg0: ClassTag[U]): RDD[U]
- f中函式引數:
- 第一個引數是Int,代表分割槽號
- 第二個Iterator[T]代表分割槽中的元素
- e.g.: 將每個分割槽中的元素和分割槽號列印出來
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9), 2)
- 建立一個函式返回RDD中的每個分割槽號和元素:
-
def func1(index:Int, iter:Iterator[Int]):Iterator[String] ={ iter.toList.map( x => "[PartID:" + index + ", value=" + x + "]" ).iterator } 複製程式碼
-
- 呼叫:
rdd1.mapPartitionsWithIndex(func1).collect
(2) aggregate
- 先對區域性聚合,再對全域性聚合
- e.g.:
val rdd1 = sc.parallelize(List(1,2,3,4,5), 2)
-
檢視每個分割槽中的元素:
-
scala> rdd1.mapPartitionsWithIndex(fun1).collect res4: Array[String] = Array( [partId : 0 , value = 1 ], [partId : 0 , value = 2 ], [partId : 1 , value = 3 ], [partId : 1 , value = 4 ], [partId : 1 , value = 5 ]) 複製程式碼
-
-
將每個分割槽中的最大值求和,注意初始值是0:
-
scala> rdd2.aggregate(0)(max(_,_),_+_) res6: Int = 7 複製程式碼
- 如果初始值時候100,則結果為300:
-
scala> rdd2.aggregate(100)(max(_,_),_+_) res8: Int = 300 ``` 複製程式碼
-
-
如果是求和,注意初始值是0:
-
scala> rdd2.aggregate(0)(_+_,_+_) res9: Int = 15 複製程式碼
- 如果初始值是10,則結果是45
-
scala> rdd2.aggregate(10)(_+_,_+_) res10: Int = 45 複製程式碼
-
-
e.g. —— 字串:
val rdd2 = sc.parallelize(List("a","b","c","d","e","f"),2)
- 修改一下剛才的檢視分割槽元素的函式
-
def func2(index: Int, iter: Iterator[(String)]) : Iterator[String] = { iter.toList.map(x => "[partID:" + index + ", val: " + x + "]").iterator } 複製程式碼
- 兩個分割槽中的元素:
-
[partID:0, val: a], [partID:0, val: b], [partID:0, val: c], [partID:1, val: d], [partID:1, val: e], [partID:1, val: f] 複製程式碼
-
- 執行結果:
-
-
e.g.:
-
val rdd3 = sc.parallelize(List("12","23","345","4567"),2) rdd3.aggregate("")((x,y) => math.max(x.length, y.length).toString, (x,y) => x + y) 複製程式碼
-
結果可能是
24
,也可能是42
-
val rdd4 = sc.parallelize(List("12","23","345",""),2) rdd4.aggregate("")((x,y) => math.min(x.length, y.length).toString, (x,y) => x + y) 複製程式碼
-
結果是
10
,也可能是01
-
原因:注意有個初始值
""
,其長度0,然後0.toString變成字串 -
val rdd5 = sc.parallelize(List("12","23","","345"),2) rdd5.aggregate("")((x,y) => math.min(x.length, y.length).toString, (x,y) => x + y) 複製程式碼
-
結果是
11
,原因同上。
-
-
(3) aggregateByKey
-
準備資料:
-
val pairRDD = sc.parallelize(List( ("cat",2), ("cat", 5), ("mouse", 4),("cat", 12), ("dog", 12), ("mouse", 2)), 2) def func3(index: Int, iter: Iterator[(String, Int)]) : Iterator[String] = { iter.toList.map(x => "[partID:" + index + ", val: " + x + "]").iterator } 複製程式碼
-
-
兩個分割槽中的元素:
-
e.g.:
- 將每個分割槽中的動物最多的個數求和
-
scala> pairRDD.aggregateByKey(0)(math.max(_, _), _ + _).collect res69: Array[(String, Int)] = Array((dog,12), (cat,17), (mouse,6)) 複製程式碼
- 將每種動物個數求和
-
scala> pairRDD.aggregateByKey(0)(_+_, _ + _).collect res71: Array[(String, Int)] = Array((dog,12), (cat,19), (mouse,6)) 複製程式碼
- 這個例子也可以使用:reduceByKey
-
scala> pairRDD.reduceByKey(_+_).collect res73: Array[(String, Int)] = Array((dog,12), (cat,19), (mouse,6)) 複製程式碼
(4) coalesce與repartition
- 都是將RDD中的分割槽進行重分割槽。
- 區別:
- coalesce預設不會進行shuffle(false);
- repartition會進行shuffle(true),會將資料真正通過網路進行重分割槽。
- e.g.:
-
def func4(index: Int, iter: Iterator[(Int)]) : Iterator[String] = { iter.toList.map(x => "[partID:" + index + ", val: " + x + "]").iterator } val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9), 2) 下面兩句話是等價的: val rdd2 = rdd1.repartition(3) val rdd3 = rdd1.coalesce(3,true) -> 如果是false,檢視RDD的length依然是2 複製程式碼
-
(5) 其他高階運算元
8. Spark 基礎程式設計案例
(1) 求網站的訪問量
-
Tomcat的訪問日誌如下:
-
需求:找到訪問量最高的兩個網頁,要求顯示網頁名稱和訪問量
-
步驟分析:
- <1>. 對網頁的訪問量求和
- <2>. 降序排序
-
程式碼:
-
import org.apache.spark.SparkConf import org.apache.spark.SparkContext object TomcatLogCount { def main(args: Array[String]): Unit = { val conf = new SparkConf().setMaster("local").setAppName("TomcatLogCount") val sc = new SparkContext(conf) /* * 讀入日誌並解析 * * 192.168.88.1 - - [30/Jul/2017:12:54:37 +0800] "GET /MyDemoWeb/oracle.jsp HTTP/1.1" 200 242 * */ val rdd1 = sc.textFile(" ").map( line => { //解析字串,得到jsp的名字 //1. 解析兩個引號間的字串 val index1 = line.indexOf("\"") val index2 = line.lastIndexOf("\"") //line1 = GET /MyDemoWeb/oracle.jsp HTTP/1.1 val line1 = line.substring(index1 + 1, index2) val index3 = line1.indexOf(" ") val index4 = line1.lastIndexOf(" ") //line2 = /MyDemoWeb/oracle.jsp val line2 = line1.substring(index3 + 1, index4) //得到jsp的名字 oracle.jsp val jspName = line2.substring(line2.lastIndexOf("/")) (jspName, 1) } ) //統計每個jsp的次數 val rdd2 = rdd1.reduceByKey(_+_) //使用Value排序 val rdd3 = rdd2.sortBy(_._2, false) //得到次數最多的兩個jsp rdd3.take(2).foreach(println) sc.stop() } } 複製程式碼
-
(2) 建立自定義分割槽
- 根據jsp檔案的名字,將各自的訪問日誌放入到不同的分割槽檔案中,如下:
-
生成的分割槽檔案
-
如:part-00000檔案中的內容:只包含了web.jsp的訪問日誌
-
- 程式碼:
-
import org.apache.spark.SparkConf import org.apache.spark.SparkContext import scala.collection.mutable.HashMap object TomcatLogPartitioner { def main(args: Array[String]): Unit = { val conf = new SparkConf().setMaster("local").setAppName("TomcatLogPartitioner") val sc = new SparkContext(conf) /* * 讀入日誌並解析 * * 192.168.88.1 - - [30/Jul/2017:12:54:37 +0800] "GET /MyDemoWeb/oracle.jsp HTTP/1.1" 200 242 * */ val rdd1 = sc.textFile(" ").map( line => { //解析字串,得到jsp的名字 //1. 解析兩個引號間的字串 val index1 = line.indexOf("\"") val index2 = line.lastIndexOf("\"") //line1 = GET /MyDemoWeb/oracle.jsp HTTP/1.1 val line1 = line.substring(index1 + 1, index2) val index3 = line1.indexOf(" ") val index4 = line1.lastIndexOf(" ") //line2 = /MyDemoWeb/oracle.jsp val line2 = line1.substring(index3 + 1, index4) //得到jsp的名字 oracle.jsp val jspName = line2.substring(line2.lastIndexOf("/")) (jspName, line) } ) //得到不重複的jsp名字 val rdd2 = rdd1.map(_._1).distinct().collect() //建立分割槽規則 val wepPartitioner = new WepPartitioner(rdd2) val rdd3 = rdd1.partitionBy(wepPartitioner) //輸出rdd3 rdd3.saveAsTextFile(" ") } //定義分割槽規則 class WepPartitioner(jspList : Array[String]) extends Partitioner { /* * 定義集合來儲存分割槽條件: * String 代表jsp的名字 * Int 代表序號 * */ val partitionMap = new HashMap[String, Int]() //初始分割槽號 val partID = 0 //填值 for (jsp <- jspList) { patitionMap.put(jsp, partID) partID += 1 } //返回分割槽個數 def numPartitioners : Int = partitionMap.size //根據jsp,返回對應的分割槽 def getPartition(key : Any) : Int = partitionMap.getOrElse(key.toString(), 0) } } 複製程式碼
-
(3) 使用JDBCRDD 訪問資料庫
-
JdbcRDD引數說明:
-
從上面的引數說明可以看出,JdbcRDD有以下兩個缺點:
- <1>. 執行的SQL必須有兩個引數,並型別都是Long
- <2>. 得到的結果是ResultSet,即:只支援select操作
-
程式碼:
-
import org.apache.spark.SparkConf import org.apache.spark.SparkContext import java.sql.Connection import java.sql.DriverManager import java.sql.PreparedStatement /* * 把Spark結果存放到mysql資料庫中 * */ object TomcatLogCountToMysql { def main(args: Array[String]): Unit = { //建立SparkContext val conf = new SparkConf().setMaster("local").setAppName("MyTomcatLogCountToMysql") val sc = new SparkContext(conf) /* * * 讀入日誌 解析: * * 192.168.88.1 - - [30/Jul/2017:12:54:37 +0800] "GET /MyDemoWeb/oracle.jsp HTTP/1.1" 200 242 */ val rdd1 = sc.textFile("H:\\tmp_files\\localhost_access_log.txt") .map( line => { //解析字串,得到jsp的名字 //1、解析兩個引號之間的字串 val index1 = line.indexOf("\"") val index2 = line.lastIndexOf("\"") val line1 = line.substring(index1 + 1, index2) // GET /MyDemoWeb/oracle.jsp HTTP/1.1 //得到兩個空格的位置 val index3 = line1.indexOf(" ") val index4 = line1.lastIndexOf(" ") val line2 = line1.substring(index3 + 1, index4) // /MyDemoWeb/oracle.jsp //得到jsp的名字 val jspName = line2.substring(line2.lastIndexOf("/")) // oracle.jsp (jspName, 1) }) // // try { // /* // * create table mydata(jsname varchar(50),countNumber Int) // * // * foreach 沒有返回值,在本需求中,只需要寫資料庫,不需要返回新的RDD,所以用foreach即可 // * // * // * 執行 Task not serializable // */ // conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/company?serverTimezone=UTC&characterEncoding=utf-8", "root", "123456") // pst = conn.prepareStatement("insert into mydata values(?,?)") // // rdd1.foreach(f => { // pst.setString(1, f._1) // pst.setInt(2, f._2) // // pst.executeUpdate() // }) // } catch { // case t: Throwable => t.printStackTrace() // } finally { // if (pst != null) pst.close() // if (conn != null) conn.close() // } // // sc.stop() // //存入資料庫 // var conn: Connection = null // var pst: PreparedStatement = null // //第一種修改方法 // /* // * 修改思路: // * conn pst 讓每一個節點都是用到,需要在不同的節點上傳輸,實現sericalizable介面 // */ // try { // rdd1.foreach(f => { // conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/company?serverTimezone=UTC&characterEncoding=utf-8", "root", "123456") // pst = conn.prepareStatement("insert into mydata values(?,?)") // // pst.setString(1, f._1) // pst.setInt(2, f._2) // // pst.executeUpdate() // }) // } catch { // case t: Throwable => t.printStackTrace() // } finally { // if (pst != null) pst.close() // if (conn != null) conn.close() // } // // sc.stop() /* * 第一種修改方式,功能上可以實現,但每條資料都會建立連線,對資料庫造成很大壓力 * * 針對分割槽來操作:一個分割槽,建立一個連線即可 */ rdd1.foreachPartition(saveToMysql) sc.stop() } def saveToMysql(it: Iterator[(String, Int)]) = { var conn: Connection = null var pst: PreparedStatement = null try { conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/company?serverTimezone=UTC&characterEncoding=utf-8", "root", "123456") pst = conn.prepareStatement("insert into mydata values(?,?)") it.foreach(f => { pst.setString(1, f._1) pst.setInt(2, f._2) pst.executeUpdate() }) } catch { case t: Throwable => t.printStackTrace() } finally { if (pst != null) pst.close() if (conn != null) conn.close() } } } 複製程式碼
9. 認識 Spark SQL
-
(1) 什麼是Spark SQL
- Spark SQL is Apache Spark's module for working with structured data.(Spark SQL 是spark 的一個模組,用來處理 結構化的資料。<不能處理非結構化的資料>)
- Spark SQL是Spark用來處理結構化資料的一個模組,它提供了一個程式設計抽象叫做DataFrame並且作為分散式SQL查詢引擎的作用。
(2) 為什麼要學習Spark SQL
- Hive是將HQL轉換成MapReduce然後提交到叢集上執行,大大簡化了編寫MapReduce的程式的複雜性,但是MapReduce這種計算模型執行效率比較慢。所以Spark SQL的應運而生,它是將Spark SQL轉換成RDD,然後提交到叢集執行,執行效率非常快,同時Spark SQL也支援從Hive中讀取資料,Hive 2.x 執行引擎可以使用Spark。
(3) Spark SQL的特點:
- <1>. 容易整合
- 不需要單獨安裝。
- <2>. 統一的資料訪問方式
- 結構化資料(JDBC、JSon、Hive、parquer檔案)都可以作為Spark SQL 的資料來源。
- 對接多種資料來源,且使用方式類似。
- 結構化資料(JDBC、JSon、Hive、parquer檔案)都可以作為Spark SQL 的資料來源。
- <3>. 相容Hive
- 把Hive中的資料,讀取到Spark SQL中執行。
- <4>. 支援標準的資料連線(JDBC)
10. Spark SQL 基礎
(1) 基本概念:Datasets和DataFrames
-
<1>. DataFrame
-
DataFrame是組織成命名列的資料集。它在概念上等同於關聯式資料庫中的表,但在底層具有更豐富的優化。DataFrames可以從各種來源構建,
-
例如:
- 結構化資料檔案
- Hive中的表
- 外部資料庫或現有RDDs
-
DataFrame API支援的語言有Scala,Java,Python和R。
-
從上圖可以看出,DataFrame多了資料的結構資訊,即schema。RDD是分散式的 Java物件的集合。DataFrame是分散式的Row物件的集合。DataFrame除了提供了比RDD更豐富的運算元以外,更重要的特點是提升執行效率、減少資料讀取以及執行計劃的優化。
-
-
<2>. Datasets
- Dataset是資料的分散式集合。Dataset是在Spark 1.6中新增的一個新介面,是DataFrame之上更高一級的抽象。它提供了RDD的優點(強型別化,使用強大的lambda函式的能力)以及Spark SQL優化後的執行引擎的優點。一個Dataset 可以從JVM物件構造,然後使用函式轉換(map, flatMap,filter等)去操作。Dataset API 支援Scala和Java,Python不支援Dataset API。
(2) DataFrames
-
<1>. 建立 DataFrames
- a. 通過Case Class建立DataFrames
- ① 定義case class(相當於表的結構:Schema)
case class Emp(empno:Int,ename:String,job:String,mgr:Int,hiredate:String,sal:Int,comm:Int,deptno:Int)
- 注意:由於mgr和comm列中包含null值,簡單起見,將對應的case class型別定義為String
- ② 將HDFS上的資料讀入RDD,並將RDD與case Class關聯
val lines = sc.textFile("/XXXX/emp.csv").map(_.split(","))
- ③ 將RDD轉換成DataFrames
val allEmp = lines.map(x => Emp(x(0).toInt,x(1),x(2),x(3).toInt,x(4),x(5).toInt,x(6).toInt,x(7).toInt))
- ④ 通過DataFrames查詢資料
val df1 = allEmp.toDF
df1.show
- ① 定義case class(相當於表的結構:Schema)
- b. 使用SparkSession
- 什麼是SparkSession
- Apache Spark 2.0引入了SparkSession,其為使用者提供了一個統一的切入點來使用Spark的各項功能,並且允許使用者通過它呼叫DataFrame和Dataset相關API來編寫Spark程式。最重要的是,它減少了使用者需要了解的一些概念,使得我們可以很容易地與Spark互動。
- 在2.0版本之前,與Spark互動之前必須先建立SparkConf和SparkContext。然而在Spark 2.0中,我們可以通過SparkSession來實現同樣的功能,而不需要顯式地建立SparkConf, SparkContext 以及 SQLContext,因為這些物件已經封裝在SparkSession中。 - 建立StructType,來定義Schema結構資訊
- 注意:需要
import org.apache.spark.sql.types._
,import org.apache.spark.sql.Row
-
import org.apache.spark.sql.types._ val myschema = StructType( List( StructField("empno",DataTypes.IntegerType), StructField("ename",DataTypes.StringType), StructField("job",DataTypes.StringType), StructField("mgr",DataTypes.IntegerType), StructField("hiredate",DataTypes.StringType), StructField("sal",DataTypes.IntegerType), StructField("comm",DataTypes.IntegerType), StructField("deptno",DataTypes.IntegerType), )) val allEmp = lines.map(x => Row(x(0).toInt,x(1),x(2),x(3).toInt,x(4),x(5).toInt,x(6).toInt,x(7).toInt)) import org.apache.spark.sql.Row val df2 = spark.createDataFrame(allEmp,myschema) 複製程式碼
- 什麼是SparkSession
- c. 使用JSon檔案來建立DataFame
-
val df3 = spark.read 讀檔案,預設是Parquet檔案 val df3 = spark.read.json("/XXXX/people.json") 讀json檔案 df3.show val df4 = spark.read.format("json").load("/XXXX/people.json") 複製程式碼
-
- a. 通過Case Class建立DataFrames
-
<2>. DataFrame 操作
-
DataFrame操作也稱為無型別的Dataset操作
-
a. DSL語句
- 查詢所有的員工姓名
- 查詢所有的員工姓名和薪水,並給薪水加100塊錢
- 查詢工資大於2000的員工
- 求每個部門的員工人數
- 參考:spark.apache.org/docs/2.1.0/…
-
b. SQL語句
- **注意:**不能直接執行SQL,需要生成一個檢視,再執行sql。
- ① 將DataFrame註冊成表(檢視):
df.createOrReplaceTempView("emp")
- ② 執行查詢:
spark.sql("select * from emp").show
spark.sql("select * from emp where deptno=10").show
spark.sql("select deptno,sum(sal) from emp group by deptno").show
-
(3) Spark SQL 中的檢視
- 檢視是一個虛表,不儲存資料。
- 兩種型別:
-
<1>. 普通檢視(本地檢視)——createOrReplaceTempView
- 只在當前Session中有效。
-
<2>. 全域性檢視: ——createGlobalTempView
- 在Spark SQL中,如果想擁有一個臨時的view,並想在不同的Session中共享,而且在application的執行週期內可用,那麼就需要建立一個全域性的臨時view。並記得使用的時候加上global_temp作為字首來引用它,因為全域性的臨時view是繫結到系統保留的資料庫global_temp上。
-
e.g.: ``` 建立一個新session,讀取不到emp檢視 spark.newSession.sql("select * from emp")
以下兩種方式均可讀到 全域性檢視 中的資料: df1.createGlobalTempView("emp1") spark.newSession.sql("select * from global_temp.emp1").show spark.sql("select * from global_temp.emp1").show 複製程式碼
複製程式碼
-
(4) 建立Datasets
- DataFrame的引入,可以讓Spark更好的處理結構資料的計算,但其中一個主要的問題是:缺乏編譯時型別安全。為了解決這個問題,Spark採用新的Dataset API (DataFrame API的型別擴充套件)。
- Dataset是一個分散式的資料收集器。這是在Spark1.6之後新加的一個介面,兼顧了RDD的優點(強型別,可以使用功能強大的lambda)以及Spark SQL的執行器高效性的優點。所以可以把DataFrames看成是一種特殊的Datasets,即:Dataset(Row)
- 建立DataSet:
- <1>. 使用序列
- ① 定義case class:
case class MyData(a:Int,b:String)
- ② 生成序列並建立DataSet:
val ds = Seq(MyData(1,"Tom"),MyData(2,"Mary")).toDS
- ③ 檢視結果
ds.show
- ① 定義case class:
- <2>. 使用JSON資料
- ① 定義case class:
case class Person(name: String, gender: String)
- ② 通過JSON資料生成DataFrame:
val df = spark.read.json(sc.parallelize("""{"gender": "Male", "name": "Tom"}""" :: Nil))
- ③ 將DataFrame轉成DataSet:
df.as[Person].show
df.as[Person].collect
- ① 定義case class:
- <3>. 使用HDFS資料
- ① 讀取HDFS資料,並建立DataSet:
val linesDS = spark.read.text("hdfs://XXXX:9000/XXXX/data.txt").as[String]
- ② 對DataSet進行操作:分詞後,查詢長度大於3的單詞
-
val words = linesDS.flatMap(_.split(" ")).filter(_.length > 3) words.show words.collect 複製程式碼
-
- ③ 執行WordCount程式
-
val result = linesDS.flatMap(_.split(" ")).map((_,1)).groupByKey(x => x._1).count result.show 排序:result.orderBy($"value").show 複製程式碼
-
- ① 讀取HDFS資料,並建立DataSet:
- <1>. 使用序列
(5) Datasets 的操作案例
- <1>. 使用emp.json 生成DataFrame:
-
val empDF = spark.read.json("/XXXX/emp.json") 查詢工資大於3000的員工 empDF.where($"sal" >= 3000).show 複製程式碼
-
- <2>. 建立case class:
case class Emp(empno:Long,ename:String,job:String,hiredate:String,mgr:String,sal:Long,comm:String,deptno:Long)
- <3>. 生成DataSets並查詢資料:
-
val empDS = empDF.as[Emp] 查詢工資大於3000的員工 empDS.filter(_.sal > 3000).show 檢視10號部門的員工 empDS.filter(_.deptno == 10).show 複製程式碼
-
- <4>. 多表查詢:
- a. 建立部門表:
-
val deptRDD=sc.textFile("/XXXX/dept.csv").map(_.split(",")) case class Dept(deptno:Int,dname:String,loc:String) val deptDS = deptRDD.map(x=>Dept(x(0).toInt,x(1),x(2))).toDS 複製程式碼
複製程式碼
-
- b. 建立員工表:
-
case class Emp(empno:Int,ename:String,job:String,mgr:String,hiredate:String,sal:Int,comm:String,deptno:Int) val empRDD = sc.textFile("/XXXX/emp.csv").map(_.split(",")) val empDS = empRDD.map(x => Emp(x(0).toInt,x(1),x(2),x(3),x(4),x(5).toInt,x(6),x(7).toInt)).toDS 複製程式碼
-
- c. 執行多表查詢:等值連結
-
val result = deptDS.join(empDS,"deptno") 另一種寫法:注意有三個等號 val result = deptDS.joinWith(empDS,deptDS("deptno")=== empDS("deptno")) joinWith和join的區別是連線後的新Dataset的schema會不一樣 複製程式碼
-
- a. 建立部門表:
- <5>. 檢視執行計劃:
result.explain