Linux環境Spark安裝配置及使用

YBCarry發表於2019-05-07

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。

      Linux環境Spark安裝配置及使用

    • 中間結果輸出:基於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體系架構

Linux環境Spark安裝配置及使用

Linux環境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
        複製程式碼
  • <2>. 配置slaves:
    • 進入spark-2.1.0/conf路徑,重新命名配置檔案:
      • mv slaves.template slaves
    • 修改slaves資訊:
      • vi slaves
      • bigdata02
        bigdata03
        複製程式碼

(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
  • 通過網頁端檢視:

(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設定

    Linux環境Spark安裝配置及使用

    • 建立存放資料夾: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的叢集整體架構如下圖所示:

    Linux環境Spark安裝配置及使用
    Linux環境Spark安裝配置及使用

  • 修改配置資訊:

    • 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_HOSTexport 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值。
      Linux環境Spark安裝配置及使用
    • 命令:
      • 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地址)
  • 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")

(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程式處理過程

Linux環境Spark安裝配置及使用

(6) Spark提交任務的流程

Linux環境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
  • <4>. RDD的基本原理
    Linux環境Spark安裝配置及使用

(2) Transformation

  • RDD中的所有轉換都是延遲載入的,也就是說,它們並不會直接計算結果。相反的,它們只是記住這些應用到基礎資料集(例如一個檔案)上的轉換動作。只有當發生一個要求返回結果給Driver的動作時,這些轉換才會真正執行。這種設計讓Spark更加有效率地執行。
    Linux環境Spark安裝配置及使用
    Linux環境Spark安裝配置及使用

(3) Action

Linux環境Spark安裝配置及使用

(4) RDD的快取機制

  • RDD通過persist方法或cache方法可以將前面的計算結果快取,但是並不是這兩個方法被呼叫時立即快取,而是觸發後面的action時,該RDD將會被快取在計算節點的記憶體中,並供後面重用。
    Linux環境Spark安裝配置及使用
  • 通過檢視原始碼發現cache最終也是呼叫了persist方法,預設的儲存級別都是僅在記憶體儲存一份,Spark的儲存級別還有好多種,儲存級別在object StorageLevel中定義的。
    Linux環境Spark安裝配置及使用
  • 快取有可能丟失,或者儲存儲存於記憶體的資料由於記憶體不足而被刪除,RDD的快取容錯機制保證了即使快取丟失也能保證計算的正確執行。通過基於RDD的一系列轉換,丟失的資料會被重算,由於RDD的各個Partition是相對獨立的,因此只需要計算丟失的部分即可,並不需要重算全部Partition。
    • Demo示例:
      Linux環境Spark安裝配置及使用
    • 通過UI進行監控:
      Linux環境Spark安裝配置及使用

(5) RDD的Checkpoint(檢查點)機制:容錯機制

  • 檢查點(本質是通過將RDD寫入Disk做檢查點)是為了通過lineage(血統)做容錯的輔助,lineage過長會造成容錯成本過高,這樣就不如在中間階段做檢查點容錯,如果之後有節點出現問題而丟失分割槽,從做檢查點的RDD開始重做Lineage,就會減少開銷。
  • 設定checkpoint的目錄,可以是本地的資料夾、也可以是HDFS。一般是在具有容錯能力,高可靠的檔案系統上(比如HDFS, S3等)設定一個檢查點路徑,用於儲存檢查點資料。
  • 分別舉例說明:
    • <1>. 本地目錄
    • 注意:這種模式,需要將spark-shell執行在本地模式上
      Linux環境Spark安裝配置及使用
    • <2>. HDFS的目錄
    • 注意:這種模式,需要將spark-shell執行在叢集模式上
      Linux環境Spark安裝配置及使用

(6) RDD的依賴關係和Spark任務中的Stage

  • RDD的依賴關係

    • RDD和它依賴的父RDD(s)的關係有兩種不同的型別,即窄依賴(narrow dependency)和寬依賴(wide dependency)。

      Linux環境Spark安裝配置及使用

    • 窄依賴指的是每一個父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的依據
      Linux環境Spark安裝配置及使用

(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

  • 先對區域性聚合,再對全域性聚合
    Linux環境Spark安裝配置及使用
  • 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
      }
      複製程式碼
  • 兩個分割槽中的元素:

    Linux環境Spark安裝配置及使用

  • 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的訪問日誌如下:

    Linux環境Spark安裝配置及使用

  • 需求:找到訪問量最高的兩個網頁,要求顯示網頁名稱和訪問量

  • 步驟分析:

    • <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檔案的名字,將各自的訪問日誌放入到不同的分割槽檔案中,如下:
    • 生成的分割槽檔案

      Linux環境Spark安裝配置及使用

    • 如:part-00000檔案中的內容:只包含了web.jsp的訪問日誌

      Linux環境Spark安裝配置及使用

  • 程式碼:
    • 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引數說明:

    Linux環境Spark安裝配置及使用

  • 從上面的引數說明可以看出,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 的資料來源。
      • 對接多種資料來源,且使用方式類似。
  • <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
    • 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)
          複製程式碼
    • 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")
        複製程式碼
  • <2>. DataFrame 操作

    • DataFrame操作也稱為無型別的Dataset操作

    • a. DSL語句

      • 查詢所有的員工姓名
        Linux環境Spark安裝配置及使用
      • 查詢所有的員工姓名和薪水,並給薪水加100塊錢
        Linux環境Spark安裝配置及使用
      • 查詢工資大於2000的員工
        Linux環境Spark安裝配置及使用
      • 求每個部門的員工人數
        Linux環境Spark安裝配置及使用
      • 參考: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的型別擴充套件)。
    Linux環境Spark安裝配置及使用
  • 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
    • <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
    • <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
          複製程式碼

(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會不一樣
        複製程式碼
  • <5>. 檢視執行計劃:
    • result.explain

相關文章