Spark學習筆記--超全,所有知識點全覆蓋總結

Spark架構,運行原理,任務調度和資源調度分析,內存管理分析,SparkSQL,SparkSreaming與kafaka,數據傾斜的解決,調優。

Spark簡介

Spark是美國加州大學伯克利分校的AMP實驗室(主要創始人lester和Matei)開發的通用的大數據處理框架。

Apache Spark™ is a fast and general engine for large-scale data processing.
Apache Spark is an open source cluster computing system that aims to make data analytics fast,both fast to run and fast to wrtie

Spark應用程序可以使用R語言、Java、Scala和Python進行編寫,極少使用R語言編寫Spark程序,Java和Scala語言編寫的Spark程序的執行效率是相同的,但Java語言寫的代碼量多,Scala簡潔優雅,但可讀性不如Java,Python語言編寫的Spark程序的執行效率不如Java和Scala。

Spark有4中運行模式:

  1. local模式,適用於測試
  2. standalone,並非是單節點,而是使用spark自帶的資源調度框架
  3. yarn,最流行的方式,使用yarn集羣調度資源
  4. mesos,國外使用的多

Spark比MapReduce快的原因

  1. Spark基於內存迭代,而MapReduce基於磁盤迭代
    MapReduce的設計:中間結果保存到文件,可以提高可靠性,減少內存佔用,但是犧牲了性能。
    Spark的設計:數據在內存中進行交換,要快一些,但是內存這個東西,可靠性比不過MapReduce。
  2. DAG計算模型在迭代計算上還是比MR的更有效率。
    在圖論中,如果一個有向圖無法從某個頂點出發經過若干條邊回到該點,則這個圖是一個有向無環圖(DAG)

    DAG計算模型在Spark任務調度中詳解!
    Spark計算比MapReduce快的根本原因在於DAG計算模型。一般而言,DAG相比MapReduce在大多數情況下可以減少shuffle次數。Spark的DAGScheduler相當於一個改進版的MapReduce,如果計算不涉及與其他節點進行數據交換,Spark可以在內存中一次性完成這些操作,也就是中間結果無須落盤,減少了磁盤IO的操作。但是,如果計算過程中涉及數據交換,Spark也是會把shuffle的數據寫磁盤的!有一個誤區,Spark是基於內存的計算,所以快,這不是主要原因,要對數據做計算,必然得加載到內存,Hadoop也是如此,只不過Spark支持將需要反覆用到的數據給Cache到內存中,減少數據加載耗時,所以Spark跑機器學習算法比較在行(需要對數據進行反覆迭代)。Spark基於磁盤的計算也是比Hadoop快。剛剛提到了Spark的DAGScheduler是個改進版的MapReduce,所以Spark天生適合做批處理的任務。Hadoop的MapReduce雖然不如spark性能好,但是HDFS仍然是業界的大數據存儲標準。

  3. Spark是粗粒度的資源調度,而MR是細粒度的資源調度。
    粗細粒度的資源調度,在Spark資源調度中詳解!

RDD(Resilient Distributed Dataset )-彈性分佈式數據集

A list of partitions
A function for computing each partition
A list of dependencies on other RDDs
Optionally, a Partitioner for key-value RDDs
Optionally, a list of preferred locations to compute each split on


RDD之間的依賴關係稱作爲Lineage——血統

Spark任務執行流程

寫一個Spark應用程序的流程

1.加載數據集(獲得RDD)

可以從HDFS,NoSQL數據庫中加載數據集

2.使用transformations算子對RDD進行操作

transformations算子是一系列懶執行的函數

3.使用actions算子觸發執行

transformations算子對RDD的操作會被先記錄,當actions算子觸發後纔會真正執行

僞代碼示例:


 

1

2

3

4

5


 

lines = sc.textFile(“hdfs://...”) //加載數據集

errors = lines.filter(_.startsWith(“ERROR”)) //transformations算子

lines.filter(x=>{x.startsWith(“ERROR”)}) //transformations算子

Mysql_errors = errors.filter(_.contain(“MySQL”)).count //count是actions算子

http_errors = errors.filter(_.contain(“Http”)).count

 

算子

Actions

count:統計RDD中元素的個數


 

1

2

3

4

5


 

val rdd = sc.makeRDD(Array("hello","hello","hello","world"))

val num = rdd.count()

println(num)

結果:

4

foreach:遍歷RDD中的元素


 

1

2

3

4

5

6

7


 

val rdd = sc.makeRDD(Array("hello","hello","hello","world"))

rdd.foreach(println)

結果:

hello

hello

hello

world

foreachPartition

foreach以一條記錄爲單位來遍歷RDD
foreachPartition以分區爲單位遍歷RDD
foreach和foreachPartition都是actions算子
map和mapPartition可以與它們做類比,但map和mapPartitions是transformations算子


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17


 

//設置rdd的分區數爲2

val rdd = sc.parallelize(1 to 6, 2)

rdd.foreachPartition(x => {

  println("data from a partition:")

  while(x.hasNext) {

    println(x.next())

  }

})

結果:

data from a partition:

1

2

3

data from a partition:

4

5

6

collect:把運行結果拉回到Driver端


 

1

2

3

4

5

6

7

8

9

10

11


 

val rdd = sc.makeRDD(Array(

  (5,"Tom"),(10,"Jed"),(3,"Tony"),(2,"Jack")

))

val resultRDD = rdd.sortByKey()

val list = resultRDD.collect()

list.foreach(println)

結果:

(2,Jack)

(3,Tony)

(5,Tom)

(10,Jed)

take(n):取RDD中的前n個元素


 

1

2

3

4

5


 

val rdd = sc.makeRDD(Array("hello","hello","hello","world"))

rdd.take(2).foreach(println)

結果:

hello

hello

first :相當於take(1)


 

1

2

3

4


 

val rdd = sc.makeRDD(Array("hello","hello","hello","world"))

println(rdd.first)

結果:

Hello

reduce:按照指定規則聚合RDD中的元素


 

1

2

3

4

5

6


 

val numArr = Array(1,2,3,4,5)

val rdd = sc.parallelize(numArr)

val sum = rdd.reduce(_+_)

println(sum)

結果:

15

countByKey:統計出KV格式的RDD中相同的K的個數


 

1

2

3

4

5

6

7

8

9

10

11

12

13


 

val rdd = sc.parallelize(Array(

  ("銷售部","Tom"), ("銷售部","Jack"),("銷售部","Bob"),("銷售部","Terry"),

  ("後勤部","Jack"),("後勤部","Selina"),("後勤部","Hebe"),

  ("人力部","Ella"),("人力部","Harry"),

  ("開發部","Allen")

))

val result = rdd.countByKey();

result.foreach(println)

結果:

(後勤部,3)

(開發部,1)

(銷售部,4)

(人力部,2)

countByValue:統計出RDD中每個元素的個數


 

1

2

3

4

5

6

7

8

9

10

11


 

val rdd = sc.parallelize(Array(

  "Tom","Jed","Tom",

  "Tom","Jed","Jed",

  "Tom","Tony","Jed"

))

val result = rdd.countByValue();

result.foreach(println)

結果:

(Tom,4)

(Tony,1)

(Jed,4)

Transformations

filter:過濾


 

1

2

3

4


 

val rdd = sc.makeRDD(Array("hello","hello","hello","world"))

rdd.filter(!_.contains("hello")).foreach(println)

結果:

world

map 和flatMap

sample :隨機抽樣

sample(withReplacement: Boolean, fraction: Double, seed: Long)
withReplacement : 是否是放回式抽樣
true代表如果抽中A元素,之後還可以抽取A元素
false代表如果抽住了A元素,之後都不在抽取A元素
fraction : 抽樣的比例
seed : 抽樣算法的初始值


 

1

2

3

4

5

6

7

8

9


 

val rdd = sc.makeRDD(Array(

  "hello1","hello2","hello3","hello4","hello5","hello6",

  "world1","world2","world3","world4"

))

rdd.sample(false, 0.3).foreach(println)

結果:

hello4

world1

在數據量不大的時候,不會很準確

groupByKey和reduceByKey

sortByKey:按key進行排序


 

1

2

3

4

5

6

7

8

9

10

11


 

val rdd = sc.makeRDD(Array(

  (5,"Tom"),(10,"Jed"),(3,"Tony"),(2,"Jack")

))

rdd.sortByKey().foreach(println)

結果:

(2,Jack)

(3,Tony)

(5,Tom)

(10,Jed)

說明:

sortByKey(fasle):倒序

sortBy:自定義排序規則


 

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


 

object SortByOperator {

  def main(args: Array[String]): Unit = {

   val conf = new SparkConf().setAppName("TestSortBy").setMaster("local")

    val sc = new SparkContext(conf)

    val arr = Array(

        Tuple3(190,100,"Jed"),

        Tuple3(100,202,"Tom"),

        Tuple3(90,111,"Tony")

    )

    val rdd = sc.parallelize(arr)

    rdd.sortBy(_._1).foreach(println)

    /* (90,111,Tony)

       (100,202,Tom)

       (190,100,Jed)

     */

    rdd.sortBy(_._2).foreach(println)

    /*(190,100,Jed)

       (90,111,Tony)

       (100,202,Tom)

     */

    rdd.sortBy(_._3).foreach(println)

    /*

       (190,100,Jed)

       (100,202,Tom)

       (90,111,Tony)

     */

    sc.stop();

  }

}

distinct:去掉重複數據

distinct算子實際上經過了以下步驟:


 

1

2

3

4

5

6

7

8

9

10

11

12

13


 

val rdd = sc.makeRDD(Array(

      "hello",

      "hello",

      "hello",

      "world"

))

val distinctRDD = rdd

      .map {(_,1)}

      .reduceByKey(_+_)

      .map(_._1)

distinctRDD.foreach {println}

等價於:

rdd.distinct().foreach {println}

 

join

先看看SQL中的join
假設有如下兩張表:table A是左表,table B是右表

不同join方式會有不同的結果

1.Inner join

產生的結果集是A和B的交集

執行SQL:


 

1

2

3


 

SELECT * FROM TableA

INNER JOIN TableB

ON TableA.name = TableB.name

結果:

2.Left outer join

產生表A的完全集,而B表中匹配的則有值,沒有匹配的則以null值取代

執行SQL:


 

1

2

3


 

SELECT * FROM TableA

LEFT OUTER JOIN TableB

ON TableA.name = TableB.name

 

結果:

3.Right outer join

產生表B的完全集,而A表中匹配的則有值,沒有匹配的則以null值取代

執行SQL:


 

1

2

3


 

SELECT * FROM TableA

RIGHT OUTER JOIN TableB

ON TableA.name = TableB.name

 

結果:

4.Full outer join(MySQL不支持)

產生A和B的並集,但是需要注意的是,對於沒有匹配的記錄,則會以null做爲值

執行SQL:


 

1

2

3


 

SELECT * FROM TableA

FULL OUTER JOIN TableB

ON TableA.name = TableB.name

 

結果:

在Spark的算子中,對兩個RDD進行join有着類似的作用
假設有兩個RDD:


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22


 

val nameList = List(

      (1,"Jed"),

      (2,"Tom"),

      (3,"Bob"),

      (4,"Tony")

)

   

val salaryArr = Array(

      (1,8000),

      (2,6000),

      (3,5000)

)

/*

 * parallelize(Seq[T],Int num)

 * 使用指定的集合(可以是List、Array等)來創建RDD

 * num 指定RDD的分區數,默認爲1

 * 這個方法經常用於測試環境

 * join產生的RDD的分區數由分區數最多的父RDD決定

 */

val nameRDD = sc.parallelize(nameList,2)

val salaryRDD = sc.parallelize(salaryArr,3)

 

分別對4種join做測試:


 

1

2

3

4

5

6

7


 

val joinRDD = nameRDD.join(salaryRDD)

joinRDD.foreach( x => {

    val id = x._1

    val name = x._2._1

    val salary = x._2._2

    println(id + "\t" + name + "\t" + salary)

})

 

結果:
1 Jed 8000
2 Tom 6000
3 Bob 5000


 

1

2

3

4

5

6

7


 

val leftOuterJoinRDD = nameRDD.leftOuterJoin(salaryRDD)

leftOuterJoinRDD.foreach( x => {

      val id = x._1

      val name = x._2._1

      val salary = x._2._2

      println(id + "\t" + name + "\t" + salary)

})

 

結果:
1 Jed Some(8000)
2 Tom Some(6000)
3 Bob Some(5000)
4 Tony None


 

1

2

3

4

5

6

7


 

val rightOuterJoinRDD = nameRDD.rightOuterJoin(salaryRDD)

rightOuterJoinRDD.foreach( x => {

      val id = x._1

      val name = x._2._1

      val salary = x._2._2

      println(id + "\t" + name + "\t" + salary)

})

 

結果:
1 Some(Jed) 8000
2 Some(Tom) 6000
3 Some(Bob) 5000


 

1

2

3

4

5

6

7


 

val fullOuterJoinRDD = nameRDD.fullOuterJoin(salaryRDD)

fullOuterJoinRDD.foreach( x => {

      val id = x._1

      val name = x._2._1

      val salary = x._2._2

      println(id + "\t" + name + "\t" + salary)

})

 

結果:
1 Some(Jed) Some(8000)
2 Some(Tom) Some(6000)
3 Some(Bob) Some(5000)
4 Some(Tony) None

union:把兩個RDD進行邏輯上的合併

union這個算子關聯的兩個RDD必須類型一致


 

1

2

3


 

val rdd1 =sc.makeRDD(1 to 10)

val rdd2 = sc.parallelize(11 until 20)

rdd1.union(rdd2).foreach {println}

 

map和mapPartitions

map()會一條記錄爲單位進行操作


 

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


 

val arr = Array("Tom","Bob","Tony","Jerry")

//把4條數據分到兩個分區中

val rdd = sc.parallelize(arr,2)

   

/*

 * 模擬把RDD中的元素寫入數據庫的過程

 */

rdd.map(x => {

  println("創建數據庫連接...")

  println("寫入數據庫...")

  println("關閉數據庫連接...")

  println()

}).count()

結果:

創建數據庫連接...

寫入數據庫...

關閉數據庫連接...

創建數據庫連接...

寫入數據庫...

關閉數據庫連接...

創建數據庫連接...

寫入數據庫...

關閉數據庫連接...

創建數據庫連接...

寫入數據庫...

關閉數據庫連接...

 

mapPartitions以分區爲單位進行操作


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21


 

/*

 * 將RDD中的數據寫入到數據庫中,絕大部分使用mapPartitions算子來實現

 */

rdd.mapPartitions(x => {

  println("創建數據庫")

  val list = new ListBuffer[String]()

  while(x.hasNext){

    //寫入數據庫

    list += x.next()+":寫入數據庫"

  }

  //執行SQL語句  批量插入

  list.iterator

})foreach(println)

結果:

創建數據庫

Tom:寫入數據庫

Bob:寫入數據庫

創建數據庫

Tony:寫入數據庫

Jerry:寫入數據庫

 

mapPartitionsWithIndex


 

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


 

val dataArr = Array("Tom01","Tom02","Tom03"

                  ,"Tom04","Tom05","Tom06"

                  ,"Tom07","Tom08","Tom09"

                  ,"Tom10","Tom11","Tom12")

val rdd = sc.parallelize(dataArr, 3);

val result = rdd.mapPartitionsWithIndex((index,x) => {

    val list = ListBuffer[String]()

    while (x.hasNext) {

      list += "partition:"+ index + " content:" + x.next

    }

    list.iterator

})

println("分區數量:" + result.partitions.size)

val resultArr = result.collect()

for(x <- resultArr){

  println(x)

}

結果:

分區數量:3

partition:0 content:Tom01

partition:0 content:Tom02

partition:0 content:Tom03

partition:0 content:Tom04

partition:1 content:Tom05

partition:1 content:Tom06

partition:1 content:Tom07

partition:1 content:Tom08

partition:2 content:Tom09

partition:2 content:Tom10

partition:2 content:Tom11

partition:2 content:Tom12

coalesce:改變RDD的分區數


 

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


 

/*

 * false:不產生shuffle

 * true:產生shuffle

 * 如果重分區的數量大於原來的分區數量,必須設置爲true,否則分區數不變

 * 增加分區會把原來的分區中的數據隨機分配給設置的分區個數

 */

val coalesceRdd = result.coalesce(6,true)

   

val results = coalesceRdd.mapPartitionsWithIndex((index,x) => {

  val list = ListBuffer[String]()

  while (x.hasNext) {

      list += "partition:"+ index + " content:[" + x.next + "]"

  }

  list.iterator

})

   

println("分區數量:" + results.partitions.size)

val resultArr = results.collect()

for(x <- resultArr){

  println(x)

}

結果:

分區數量:6

partition:0 content:[partition:1 content:Tom07]

partition:0 content:[partition:2 content:Tom10]

partition:1 content:[partition:0 content:Tom01]

partition:1 content:[partition:1 content:Tom08]

partition:1 content:[partition:2 content:Tom11]

partition:2 content:[partition:0 content:Tom02]

partition:2 content:[partition:2 content:Tom12]

partition:3 content:[partition:0 content:Tom03]

partition:4 content:[partition:0 content:Tom04]

partition:4 content:[partition:1 content:Tom05]

partition:5 content:[partition:1 content:Tom06]

partition:5 content:[partition:2 content:Tom09]

val coalesceRdd = result.coalesce(6,fasle)的結果是:

分區數量:3

partition:0 content:[partition:0 content:Tom01]

partition:0 content:[partition:0 content:Tom02]

partition:0 content:[partition:0 content:Tom03]

partition:0 content:[partition:0 content:Tom04]

partition:1 content:[partition:1 content:Tom05]

partition:1 content:[partition:1 content:Tom06]

partition:1 content:[partition:1 content:Tom07]

partition:1 content:[partition:1 content:Tom08]

partition:2 content:[partition:2 content:Tom09]

partition:2 content:[partition:2 content:Tom10]

partition:2 content:[partition:2 content:Tom11]

partition:2 content:[partition:2 content:Tom12]

val coalesceRdd = result.coalesce(2,fasle)的結果是:

分區數量:2

partition:0 content:[partition:0 content:Tom01]

partition:0 content:[partition:0 content:Tom02]

partition:0 content:[partition:0 content:Tom03]

partition:0 content:[partition:0 content:Tom04]

partition:1 content:[partition:1 content:Tom05]

partition:1 content:[partition:1 content:Tom06]

partition:1 content:[partition:1 content:Tom07]

partition:1 content:[partition:1 content:Tom08]

partition:1 content:[partition:2 content:Tom09]

partition:1 content:[partition:2 content:Tom10]

partition:1 content:[partition:2 content:Tom11]

partition:1 content:[partition:2 content:Tom12]

val coalesceRdd = result.coalesce(2,true)的結果是:

分區數量:2

partition:0 content:[partition:0 content:Tom01]

partition:0 content:[partition:0 content:Tom03]

partition:0 content:[partition:1 content:Tom05]

partition:0 content:[partition:1 content:Tom07]

partition:0 content:[partition:2 content:Tom09]

partition:0 content:[partition:2 content:Tom11]

partition:1 content:[partition:0 content:Tom02]

partition:1 content:[partition:0 content:Tom04]

partition:1 content:[partition:1 content:Tom06]

partition:1 content:[partition:1 content:Tom08]

partition:1 content:[partition:2 content:Tom10]

partition:1 content:[partition:2 content:Tom12]

下圖說明了三種coalesce的情況:

repartition:改變RDD分區數

repartition(int n) = coalesce(int n, true)

partitionBy:通過自定義分區器改變RDD分區數


 

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


 

JavaPairRDD<Integer, String> partitionByRDD = nameRDD.partitionBy(new Partitioner() {

              

    private static final long serialVersionUID = 1L;

    

    //分區數2

    @Override

    public int numPartitions() {

        return 2;

    }

    //分區邏輯

    @Override

    public int getPartition(Object obj) {

        int i = (int)obj;

        if(i % 2 == 0){

            return 0;

        }else{

            return 1;

        }

    }

});

``` 

 

##### glom:把分區中的元素封裝到數組中

```scala

val rdd = sc.parallelize(1 to 10,2) 

/**

 *  rdd有兩個分區

 *   partition0分區裏面的所有元素封裝到一個數組

 *   partition1分區裏面的所有元素封裝到一個數組

 */

val glomRDD = rdd.glom()

glomRDD.foreach(x => {

  println("============")

  x.foreach(println)

println("============")

})

println(glomRDD.count())

結果:

============

1

2

3

4

5

============

============

6

7

8

9

10

============

2

randomSplit:拆分RDD


 

1

2

3

4

5

6

7

8

9

10

11

12

13


 

/**

 * randomSplit:

 *   根據傳入的 Array中每個元素的權重將rdd拆分成Array.size個RDD

 *  拆分後的RDD中元素數量由權重來決定,數據量不大時不一定準確

 */

val rdd = sc.parallelize(1 to 10)

rdd.randomSplit(Array(0.1,0.2,0.3,0.4)).foreach(x => {println(x.count)})

理論結果:

1

2

3

4

實際結果不一定準確

zip

與zip有關的3個算子如下圖所示:

算子案例

WordCount-Java版


 

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


 

/**

 * 文件中的數據 :

 * Spark Core

 * Spark Streaming

 * Spark SQL

 * @author root

 */

public class WordCount {

     

     public static void main(String[] args) {

         /*

          * SparkConf對象主要用於設置Spark運行時的環境參數 :

          * 1.運行模式

          * 2.Application Name

          * 3.運行時的資源需求

          */

         SparkConf conf = new SparkConf();

          conf.setMaster("local").setAppName("WordCount");

         /*

          * SparkContext是Spark運行的上下文,是通往Spark集羣的唯一通道

          */

         JavaSparkContext jsc = new JavaSparkContext(conf);

         

         String path = "cs";

         JavaRDD<String> rdd = jsc.textFile(path);

         

         //================== wordcount start =================

         flatMapRDD.mapToPair(new PairFunction<String, String, Integer>() {

              /**

               *

               */

              private static final long serialVersionUID = 1L;

              @Override

              public Tuple2<String, Integer> call(String word)

throws Exception {

                  return new Tuple2<String, Integer>(word, 1);

              }

         }).reduceByKey(new Function2<Integer, Integer, Integer>() {

              

              /**

               *

               */

              private static final long serialVersionUID = 1L;

              @Override

              public Integer call(Integer v1, Integer v2)

throws Exception {

                  return v1 + v2;

              }

         }).mapToPair(new PairFunction<Tuple2<String, Integer>,

Integer, String>() {

              /**

               *

               */

              private static final long serialVersionUID = 1L;

              @Override

              public Tuple2<Integer, String> call(

Tuple2<String, Integer> tuple) throws Exception {

                  return new Tuple2<Integer, String>(tuple._2, tuple._1);

              }

         }).sortByKey(false) //fasle : 降序

         .mapToPair(new PairFunction<Tuple2<Integer,String>,

String, Integer>() {

              /**

               *

               */

              private static final long serialVersionUID = 1L;

              @Override

              public Tuple2<String, Integer> call(

Tuple2<Integer, String> tuple) throws Exception {

                  return new Tuple2<String, Integer>(tuple._2, tuple._1);

              }

         }).foreach(new VoidFunction<Tuple2<String,Integer>>() {

              

              /**

               *

               */

              private static final long serialVersionUID = 1L;

              @Override

              public void call(Tuple2<String, Integer> tuple)

throws Exception {

                  System.out.println(tuple);

              }

         });;

         //================= wordcount end ==================

         jsc.stop();

     }

}

結果:

(Spark,3)

(SQL,1)

(Streaming,1)

(Core,1)

過濾掉出現次數最多的數據-Scala版


 

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


 

/**

 * 文件中的數據 :

 * hello java

 * hello java

 * hello java

 * hello java

 * hello java

 * hello hadoop

 * hello hadoop

 * hello hadoop

 * hello hive

 * hello hive

 * hello world

 * hello spark

 * @author root

 */

object FilterMost {

  def main(args: Array[String]): Unit = {

    val conf = new SparkConf()

        .setMaster("local")

        .setAppName("FilterMost")

    val sc = new SparkContext(conf)

   

    val rdd : RDD[String] = sc.textFile("test")

    val sampleRDD : RDD[String] = rdd.sample(false, 0.9)

    val result = sampleRDD

      .map { x => (x.split(" ")(1),1) }

      .reduceByKey(_+_)

      .map { x => {(x._2,x._1)}}

      .sortByKey(false)

      .first()

      ._2

    rdd

      .filter {(!_.contains(result))}

      .foreach(println)

     

    sc.stop();

  }

}

結果:

hello hadoop

hello hadoop

hello hadoop

hello hive

hello hive

hello world

hello spark

統計每個頁面的UV

部分數據如下:
日期 時間戳 用戶ID pageID 模塊 用戶事件
2017-05-13 1494643577030 null 54 Kafka View
2017-05-13 1494643577031 8 70 Kafka Register
2017-05-13 1494643577031 9 12 Storm View
2017-05-13 1494643577031 9 1 Scala View
2017-05-13 1494643577032 7 73 Scala Register
2017-05-13 1494643577032 16 23 Storm Register

scala代碼:


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22


 

object CountUV {

  def main(args: Array[String]): Unit = {

    val conf = new SparkConf()

    conf.setMaster("local")

    conf.setAppName("CountUV")

   

    val sc = new SparkContext(conf)

   

    val rdd = sc.textFile("userLog")

    val result = rdd.filter(!_.split("\t")(2).contains("null"))

    .map(x => {

      (x.split("\t")(3), x.split("\t")(2))

    })

    .distinct().countByKey()

   

    result.foreach(x => {

      println("PageId: " + x._1 + "\tUV: " + x._2)

    })

   

    sc.stop();

  }

}

Java代碼:


 

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


 

public class CountUV {

     

     public static void main(String[] args) {

         SparkConf sparkConf = new SparkConf()

.setMaster("local")

.setAppName("CountUV");

         final JavaSparkContext jsc = new JavaSparkContext(sparkConf);

         

         JavaRDD<String> rdd = jsc.textFile("userLog");

         

         JavaRDD<String> filteredRDD =

rdd.filter(new Function<String, Boolean>() {

              /**

               *

               */

              private static final long serialVersionUID = 1L;

              @Override

              public Boolean call(String v1) throws Exception {

                  return !"null".equals(v1.split("\t")[2]);

              }

         });

         

         JavaPairRDD<String, String> pairRDD =

filteredRDD.mapToPair(new PairFunction<String, String, String>() {

              /**

               *

               */

              private static final long serialVersionUID = 1L;

              @Override

              public Tuple2<String, String> call(String t)

throws Exception {

                  String[] splits = t.split("\t");

                 return new Tuple2<String, String>(splits[3], splits[2]);

              }

         });

         

         JavaPairRDD<String, String> distinctRDD = pairRDD.distinct();

         

         Map<String, Object> resultMap = distinctRDD.countByKey();

         

         for(Entry<String, Object> entry : resultMap.entrySet()) {

              System.out.println("pageId:"+ entry.getKey() +

" UV:" + entry.getValue());

         }

         

         jsc.stop();

     }

}

 

部分結果:
pageId:45 UV:20
pageId:98 UV:20
pageId:34 UV:18
pageId:67 UV:20
pageId:93 UV:20

二次排序-scala版


 

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


 

object SecondSort {

 

  def main(args: Array[String]): Unit = {

    val sparkConf = new SparkConf().setMaster("local").setAppName("SecondSort")

    val sc = new SparkContext(sparkConf)

   

    val rdd = sc.textFile("secondSort.txt")

   

    val mapRDD = rdd.map(x => {

  (new SecondSortKey(x.split(" ")(0).toInt, x.split(" ")(1).toInt), null)

    })

   

    val sortedRDD = mapRDD.sortByKey(false)

    //val sortedRDD = mapRDD.sortBy(_._1, false)

   

    sortedRDD.map(_._1).foreach(println)

   

    sc.stop()

  }

}

class SecondSortKey(val first:Int, val second:Int) extends Ordered[SecondSortKey] with Serializable{

  def compare(ssk:SecondSortKey): Int = {

    if(this.first - ssk.first == 0) {

      this.second - ssk.second

    }else{

      this.first - ssk.first

    }

  }

  override

  def toString(): String = {

    this.first + " " + this.second

  }

}

分組取TopN問題:找出每個班級中排名前三的分數-Java版

部分數據:
class1 100
class2 85
class3 70
class1 102
class2 65
class1 45


 

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


 

/**

思路:

     java:mapToPair/scala:map

         (class1,100),(class2,80)...

     groupByKey 

         class1 [100,101,88,99...]

     如果把[100,101,88,99....]封裝成List,然後進行Collections.sort(list)

     會有問題:

         大數據級別的value放到list裏排序可能會造成OOM

     解決辦法:

         定義一個定長的數組,通過一個簡單的算法解決

 * @author root

 *

 */

public class GroupTopN {

     

     private final static Integer N = 3;

     

     public static void main(String[] args) {

         SparkConf sparkConf = new SparkConf()

                  .setMaster("local")

                  .setAppName("GroupTopN");

         JavaSparkContext jsc = new JavaSparkContext(sparkConf);

         JavaRDD<String> rdd = jsc.textFile("scores.txt");

         

         JavaPairRDD<String, Integer> pairRDD =

            rdd.mapToPair(new PairFunction<String, String, Integer>() {

                       /**

                        *

                        */

                       private static final long serialVersionUID = 1L;

                       @Override

                       public Tuple2<String, Integer> call(String t)

throws Exception {

                            String className = t.split("\t")[0];

                       Integer score = Integer.valueOf(t.split("\t")[1]);

                    return new Tuple2<String, Integer>(className, score);

                       }

         });

         

         pairRDD.groupByKey().foreach(

new VoidFunction<Tuple2<String,Iterable<Integer>>>() {

              

              /**

               *

               */

              private static final long serialVersionUID = 1L;

              @Override

              public void call(Tuple2<String, Iterable<Integer>> t)

throws Exception {

                  String className = t._1;

                  Iterator<Integer> iter = t._2.iterator();

                  

                  Integer[] nums = new Integer[N];

                  

                  while(iter.hasNext()) {

                       Integer score = iter.next();

                       for(int i=0; i<nums.length; i++) {

                            if(nums[i] == null) {

                                nums[i] = score;//給數組的前三個元素賦值

                                break;

                            }else if(score > nums[i]) {

                                for (int j = 2; j > i; j--) {

                                     nums[j] = nums[j-1];

                                }

                                nums[i] = score;

                                break;

                            }

                       }

                  }

                  

                  System.out.println(className);

                  for(Integer i : nums) {

                       System.out.println(i);

                  }

              }

         });

    

         jsc.stop();

     }

}

結果:

class1

102

100

99

class2

88

85

85

class3

98

70

70

 

廣播變量

有如下僞代碼:


 

1

2

3

4


 

var rdd = sc.textFile(path)

val blackName = “Tom”

val fliterRDD = rdd.fliter(_.equals(blackName))

filterRDD.count()

 

blackName是RDD外部的變量,當把task發送到其他節點執行,需要使用這個變量時,必須給每個task發送這個變量,假如這個變量佔用的內存很大,而且task數量也有很多,那麼導致集羣資源緊張。

廣播變量可以解決這個問題:
把這個變量定義爲廣播變量,發送到每個executor中,每個在executor中執行的task都可以使用這個廣播變量,而一個executor可以包含多個task,task數一般是executor數的好幾倍,這樣就減少了集羣的資源負荷。

注意:

  1. 廣播變量只能在Driver端定義
  2. 廣播變量在Executor端無法修改
  3. 只能在Driver端改變廣播變量的值

累加器

有如下僞代碼:


 

1

2

3

4

5

6

7


 

//統計RDD中元素的個數

var rdd = sc.textFile(path)

var count = 0 //這是定義在Driver端的變量

rdd.map(x => {

count += 1 //這個計算在Executor端執行

})

println(count)

 

這個代碼執行完畢是得不到rdd中元素的個數的,原因:

在spark應用程序中,我們經常會有這樣的需求,如異常監控,調試,記錄符合某特性的數據的數目,這種需求都需要用到計數器,如果一個變量不被聲明爲一個累加器,那麼它將在被改變時不會再driver端進行全局彙總,即在分佈式運行時每個task運行的知識原始變量的一個副本,並不能改變原始變量的值,但是當這個變量被聲明爲累加器後,該變量就會有分佈式計數的功能。

注意:

  1. 累加器定義在Driver端
  2. Executor端只能對累加器進行操作,也就是隻能累加
  3. Driver端可以讀取累加器的值,Executor端不能讀取累加器的值

累加器(Accumulator)陷阱及解決辦法

計數器的測試代碼


 

1

2

3

4

5

6

7

8

9


 

//在driver中定義

val accum = sc.accumulator(0, "Example Accumulator")

//在task中進行累加

sc.parallelize(1 to 10).foreach(x=> accum += 1)

//在driver中輸出

accum.value

//結果將返回10

res: 10

累加器的錯誤用法


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19


 

val accum= sc.accumulator(0, "Error Accumulator")

val data = sc.parallelize(1 to 10)

//用accumulator統計偶數出現的次數,同時偶數返回0,奇數返回1

val newData = data.map{x => {

if(x%2 == 0){

accum += 1

0

}else 1

}}

//使用action操作觸發執行

newData.count

//此時accum的值爲5,是我們要的結果

accum.value

//繼續操作,查看剛纔變動的數據,foreach也是action操作

newData.foreach(println)

//上個步驟沒有進行累計器操作,可是累加器此時的結果已經是10了

//這並不是我們想要的結果

accum.value

原因分析

我們都知道,spark中的一系列transform操作會構成一串長的任務鏈,此時需要通過一個action操作來觸發,accumulator也是一樣。因此在一個action操作之前,你調用value方法查看其數值,肯定是沒有任何變化的。

所以在第一次count(action操作)之後,我們發現累加器的數值變成了5,是我們要的答案。

之後又對新產生的的newData進行了一次foreach(action操作),其實這個時候又執行了一次map(transform)操作,所以累加器又增加了5。最終獲得的結果變成了10。

解決辦法

For accumulator updates performed inside actions only, Spark guarantees that each task’s update to the accumulator will only be applied once, i.e. restarted tasks will not update the value. In transformations, users should be aware of that each task’s update may be applied more than once if tasks or job stages are re-executed.

看了上面的分析,大家都有這種印象了,那就是使用累加器的過程中只能使用一次action的操作才能保證結果的準確性。

事實上,還是有解決方案的,只要將任務之間的依賴關係切斷就可以了。什麼方法有這種功能呢?你們肯定都想到了,cache,persist。調用這個方法的時候會將之前的依賴切除,後續的累加器就不會再被之前的transfrom操作影響到了。


 

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


 

object AccumulatorTest {

def main(args: Array[String]): Unit = {

val sc = new SparkContext(new SparkConf().setAppName("MapPartitionsOperator").setMaster("local"))

val accum= sc.accumulator(0, "Error Accumulator")

val data = sc.parallelize(1 to 10)

//用accumulator統計偶數出現的次數,同時偶數返回0,奇數返回1

val newData = data.map{x => {

if(x%2 == 0){

accum += 1

0

}else 1

}}

newData.cache.count

//使用action操作觸發執行

// newData.count

//此時accum的值爲5,是我們要的結果

println(accum.value)

println("test")

//繼續操作,查看剛纔變動的數據,foreach也是action操作

newData.foreach(println)

//上個步驟沒有進行累計器操作,可是累加器此時的結果已經是10了

//這並不是我們想要的結果

println("test")

println(accum.value)

}

}

自定義累加器

所以在第一次count(action操作)之後,我們發現累加器的數值變成了5,是我們要的答案。
之後又對新產生的的newData進行了一次foreach(action操作),其實這個時候又執行了一次map(transform)操作,所以累加器又增加了5。最終獲得的結果變成了10。

總結

使用Accumulator時,爲了保證準確性,只使用一次action操作。如果需要使用多次則使用cache或persist操作切斷依賴。
RDD持久化
這段僞代碼的瑕疵:


 

1

2

3

4


 

lines = sc.textFile(“hdfs://...”)

errors = lines.filter(_.startsWith(“ERROR”))

mysql_errors = errors.filter(_.contain(“MySQL”)).count

http_errors = errors.filter(_.contain(“Http”)).count

 

errors是一個RDD,mysql_errors這個RDD執行時,會先讀文件,然後獲取數據,通過計算errors,把數據傳給mysql_errors,再進行計算,因爲RDD中是不存儲數據的,所以http_errors計算的時候會重新讀數據,計算errors後把數據傳給http_errors進行計算,重複使用errors這個RDD很有必須,這就需要把errors這個RDD持久化,以便其他RDD使用。
RDD持久化有三個算子:cache、persist、checkpoint

cache:把RDD持久化到內存

使用方法:


 

1

2

3


 

var rdd = sc.textFile("test")

rdd = rdd.cache()

val count = rdd.count() //或者其他操作

 

查看源碼,可以發現其實cahce就是persist(StorageLevel.MEMORY_ONLY)


 

1

2

3

4

5


 

/** Persist this RDD with the default storage level (`MEMORY_ONLY`). */

def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)

/** Persist this RDD with the default storage level (`MEMORY_ONLY`). */

def cache(): this.type = persist()

persist:可以選擇多種持久化方式

使用方法:


 

1

2

3


 

var rdd = sc.textFile("test")

rdd = rdd.persist(StorageLevel.MEMORY_ONLY)

val count = rdd.count() //或者其他操作

 

Persist StorageLevel說明:


 

1

2

3

4

5

6


 

class StorageLevel private(

    private var _useDisk: Boolean,

    private var _useMemory: Boolean,

    private var _useOffHeap: Boolean,

    private var _deserialized: Boolean,

    private var _replication: Int = 1)

 

初始化StorageLevel可以傳入5個參數,分別對應是否存入磁盤、是否存入內存、是否使用堆外內存、是否不進行序列化,副本數(默認爲1)

使用不同參數的組合構造的實例被預先定義爲一些值,比如MEMORY_ONLY代表着不存入磁盤,存入內存,不使用堆外內存,不進行序列化,副本數爲1,使用persisit()方法時把這些持久化的級別作爲參數傳入即可,cache()與persist(StorageLevel.MEMORY_ONLY)是等價的。
cache和persist的注意事項

  1. cache 和persist是懶執行算子,需要有一個action類的算子觸發執行
  2. cache 和 persist算子的返回執行必須賦值給一個變量,在接下來的job中直接使用這個變量,那麼就是使用了持久化的數據了,如果application中只有一個job,沒有必要使用RDD持久化
  3. cache 和 persist算子後不能立即緊跟action類算子,比如count算子,但是在下一行可以有action類算子

    `error : rdd = cache().count()`
    `right : rdd = rdd.cache()`
    `rdd.count()****`
    
  4. cache() = persist(StorageLevel.MEMORY_ONLY)

checkpoint : 可以把RDD持久化到HDFS,同時切斷RDD之間的依賴

使用方法:


 

1

2

3

4


 

sc.setCheckpointDir("hdfs://...")

var rdd = sc.textFile("test")

rdd.checkpoint()

val count = rdd.count() //或者其他操作

 

對於切斷RDD之間的依賴的說明:
當業務邏輯很複雜時,RDD之間頻繁轉換,RDD的血統很長,如果中間某個RDD的數據丟失,還需要重新從頭計算,如果對中間某個RDD調用了checkpoint()方法,把這個RDD上傳到HDFS,同時讓後面的RDD不再依賴於這個RDD,而是依賴於HDFS上的數據,那麼下次計算會方便很多。
checkpoint()執行原理:

  1. 當RDD的job執行完畢後,會從finalRDD從後往前回溯
  2. 當回溯到調用了checkpoint()方法的RDD後,會給這個RDD做一個標記
  3. Spark框架自動啓動一個新的job,計算這個RDD的數據,然後把數據持久化到HDFS上
  4. 優化:對某個RDD執行checkpoint()之前,對該RDD執行cache(),這樣的話,新啓動的job只需要把內存中的數據上傳到HDFS中即可,不需要重新計算。

Spark任務調度和資源調度

一些術語

Master(standalone):資源管理的主節點(進程)
Cluster Manager:在集羣上獲取資源的外部服務(例如standalone,Mesos,Yarn)
Worker Node(standalone):資源管理的從節點(進程)或者說管理本機資源的進程
Application:基於Spark的用戶程序,包含了driver程序和運行在集羣上的executor程序
Driver Program:用來連接工作進程(Worker)的程序
Executor:是在一個worker進程所管理的節點上爲某Application啓動的一個進程,該進程負責運行任務,並且負責將數據存在內存或者磁盤上,每個應用都有各自獨立的executors
Task:被送到某個executor上的工作單元
Job:包含很多任務(Task)的並行計算,和action是對應的
Stage:一個Job會被拆分很多組任務,每組任務被稱爲Stage(就像Mapreduce分map task和reduce task一樣)

寬窄依賴



join既可能是窄依賴,又是寬依賴。

寬窄依賴的作用

在Spark裏每一個操作生成一個RDD,RDD之間連一條邊,最後這些RDD和他們之間的邊組成一個有向無環圖,這個就是DAG,Spark內核會在需要計算髮生的時刻繪製一張關於計算路徑的有向無環圖,也就是DAG。

有了計算的DAG圖,Spark內核下一步的任務就是根據DAG圖將計算劃分成Stage,如上圖,G與F之間是寬依賴,所以把G和F分爲兩個Stage,而CD到F,E到F都是窄依賴,所以CDEF最終劃分爲一個Stage2,A與B之間是寬依賴,B與G之間是窄依賴,所以最終,A被劃分爲一個Stage1,因爲BG的stage依賴於stage1和stage2,所以最終把整個DAG劃分爲一個stage3,所以說,寬窄依賴的作用就是切割job,劃分stage。

Stage:由一組可以並行計算的task組成。
Stage的並行度:就是其中的task的數量

與互聯網業界的概念有些差異:在互聯網的概念中,並行度是指可同時開闢的線程數,併發數是指每個線程中可處理的最大數據量,比如4個線程,每個線程可處理的數據爲100萬條,那麼並行度就是4,併發量是100萬,而對於stage而言,即使其中的task是分批進行執行的,也都算在並行度中,比如,stage中有100個task,而這100個task分4次才能執行完,那麼該stage的並行度也爲100。

Stage的並行度是由最後一個RDD的分區決定的。

RDD中爲什麼不存儲數據以及stage的計算模式

有僞代碼如下:


 

1

2

3

4

5

6

7

8

9

10


 

var lineRDD = sc.textFile(“hdfs://…”) 從HDFS中讀數據

var fliterRDD = rdd.fliter(x => {

println(“fliter” + x)

true

})

var mapRDD = fliterRDD.map(x => {

println(“map” + x)

x

})

mapRDD.count()

 

執行流程:

在每一個task執行之前,它會把所有的RDD的處理邏輯進行整合,以遞歸函數的展開式整合,即map(fliter(readFromB())),而spark沒有讀取文件的方法,用的是MR的讀文件的方法,所以readFromB()實際上是一行一行的讀數據,所以以上task執行時會輸出:
fliter
map
fliter
map
……
stage的計算模式就是:pipeline模式,即計算過程中數據不會落地,也就是不會存到磁盤,而是放在內存中直接給下一個函數使用,stage的計算模式類似於 1+1+1 = 3,而MapReduce的計算模式類似於 1+1=2、2+1=3,就是說MR的中間結果都會寫到磁盤上

管道中的數據在以下情況會落地:

  1. 對某一個RDD執行控制算子(比如對mapRDD執行了foreach()操作)
  2. 在每一個task執行完畢後,數據會寫入到磁盤上,這就是shuffle write階段

任務調度

  1. N(N>=1)個RDD Object組成了一個DAG,它用代碼實現後就是一個application
  2. DAGScheduler是任務調度的高層調度器的對象,它依據RDD之間的寬窄依賴把DAG切割成一個個Stage,然後把這些stage以TaskSet的形式提交給TaskScheduler(調用了TaskScheduler的某個方法,然後把TaskSet作爲參數傳進去)
  3. TaskScheduler是任務調度的底層調度器的對象
  4. Stage是一組task的組合,TaskSet是task的集合,所以兩者並沒有本質的區別,只是在不同層次的兩個概念而已
  5. TaskScheduler遍歷TaskSet,把每個task發送到Executor中的線程池中進行計算
  6. 當某個task執行失敗後,會由TaskScheduler進行重新提交給Executor,默認重試3次,如果重試3次仍然失敗,那麼該task所在的stage就執行失敗,由DAGScheduler進行重新發送,默認重試4次,如果重試4次後,stage仍然執行失敗,那麼該stage所在的job宣佈執行失敗,且不會再重試
  7. TaskScheduler還可以重試straggling tasks,就是那些運行緩慢的task,當TaskScheduler認爲某task0是straggling task,那麼TaskScheduler會發送一條相同的task1,task0與task1中選擇先執行完的task的計算結果爲最終結果,這種機制被稱爲推測執行
  8. 推測執行建議關閉而且默認就是關閉的,原因如下:
    推測執行可能導致數據重複
    比如做數據清洗時,某個task正在往關係型數據庫中寫數據,而當它執行的一定階段但還沒有執行完的時候,此時如果TaskScheduler認爲它是straggling task,那麼TaskScheduler會新開啓一個一模一樣的task進行數據寫入,會造成數據重複。
    推測執行可能會大量佔用資源導致集羣崩潰
    比如某條task執行時發生了數據傾斜,該task需要計算大量的數據而造成它執行緩慢,那麼當它被認爲是straggling task後,TaskScheduler會新開啓一個一模一樣的task進行計算,新的task計算的還是大量的數據,且分配得到與之前的task相同的資源,兩條task執行比之前一條task執行還慢,TaskScheduler有可能再分配一條task來計算這些數據,這樣下去,資源越來越少,task越加越多,形成死循環後,程序可能永遠都跑不完。

資源調度

1)

2)

3)

4)

5&6)

7&8)

注意:
application執行之前申請的這批executor可以被這個application中的所有job共享。

粗粒度和細粒度的資源申請

粗粒度的資源申請:Spark

在Application執行之前,將所有的資源申請完畢,然後再進行任務調度,直到最後一個task執行完畢,纔會釋放資源
優點:每一個task執行之前不需要自己去申請資源,直接使用資源就可以,每一個task的啓動時間就變短了,task執行時間縮短,使得整個Application執行的速度較快
缺點:無法充分利用集羣的資源,比如總共有10萬的task,就要申請10萬個task的資源,即使只剩下一個task要執行,也得等它執行完才釋放資源,在這期間99999個task的資源沒有執行任何task,但也不能被其他需要的進程或線程使用

細粒度的資源申請:MapReduce

在Application執行之前,不需要申請好資源,直接進行任務的調度,在每一個task執行之前,自己去申請資源,申請到就執行,申請不到就等待,每一個task執行完畢後就立馬釋放資源。
優點:可以充分的利用集羣的資源
缺點:每一個task的執行時間變長了,導致整個Application的執行的速度較慢

yarn如何同時調度粗細兩種方式

Spark和MapReduce都可以跑在yarn上,那怎麼做到一個組粒度一個細粒度呢?原因是他們各自實現ApplicationMaster的方式不同。

資源調度源碼分析

分析以集羣方式提交命令後的資源調度源碼

資源調度的源碼(Master.scala)位置:

1.Worker啓動後向Master註冊
2.client向Master發送一條消息,爲當前的Application啓動一個Driver進程


schedule()方法是對Driver和Executor進行調度的方法,看看啓動Driver進程的過程:

spark_資源調度源碼分析_Driver進程的創建

schedule()方法有一些問題:


 

1

2

3

4

5

6

7

8

9

10

11

12

13


 

private def schedule(): Unit = {

if (state != RecoveryState.ALIVE) { return }

// Drivers take strict precedence over executors

val shuffledWorkers = Random.shuffle(workers) // Randomization helps balance drivers for (worker <- shuffledWorkers if worker.state == WorkerState.ALIVE) {

for (driver <- waitingDrivers) {

if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) {

launchDriver(worker, driver)

waitingDrivers -= driver

}

}

}

startExecutorsOnWorkers()

}

 

1) 如果是以客戶端方式命令執行程序,那麼不需要Master來調度Worker創建Driver進程,那麼waitingDrivers這個集合中就沒有元素,所以也就不需要遍歷shuffledWorkers,源碼並沒有考慮這種情況。應該是有if語句進行非空判定。
2) 如果waitingDrivers中只有一個元素,那麼也會一直遍歷shuffledWorkers這個集合,實際上是不需要的。

3.Driver進程向Master發送消息:爲當前的Application申請一批Executor。

下面看看Executor的創建過程:
spark源碼分析_資源調度_Executor的啓動

通過以上過程,Executor進程就被啓動了

資源調度的三個結論

  1. 在默認情況下(沒有使用--executor --cores這個選項)時,每一個Worker節點爲當前的Application只啓動一個Executor,這個Executor會使用這個Worker管理的所有的core(原因:assignedCores(pos) += minCoresPerExecutor)
  2. 默認情況下,每個Executor使用1G內存
  3. 如果想要在一個Worker節點啓動多個Executor,需要使--executor --cores這個選項
  4. spreadOutApps這個參數可以決定Executor的啓動方式,默認輪詢方式啓動,這樣有利於數據的本地化。

驗證資源調度的三個結論

集羣中總共有6個core和4G內存可用,每個Worker管理3個core和2G內存

SPARK_HOME/bin下有一個spark-shell腳本文件,執行這個腳本文件就是提交一個application


 

1

2

3

4

5


 

function main() {

……

"${SPARK_HOME}"/bin/spark-submit --class org.apache.spark.repl.Main --name "Spark shell" "$@"

……

}

 

默認情況(不指定任何參數)下啓動spark-shell:
[root@node04 bin]# ./spark-shell --master spark://node01:7077

這個application啓動了2個Executor進程,每個Worker節點上啓動一個,總共使用了6個core和2G內存,每個Work提供3個core和1G內存。

設置每個executor使用1個core
[root@node04 bin]# ./spark-shell --master spark://node01:7077 --executor-cores 1

那麼每個worker爲application啓動兩個executor,每個executor使用1個core,這是因爲啓動兩個executor後,內存已經用完了,所以即使還有剩餘的core可用,也無法再啓動executor了

設置每個executor使用2個core
[root@node04 bin]# ./spark-shell --master spark://node01:7077 --executor-cores 2

那麼每個worker爲application啓動1個executor,每個executor使用2個core,這是因爲啓動兩個executor後,每個executor剩餘的core爲1,已經不夠再啓動一個exexutor了

設置每個executor使用3G內存
[root@node04 bin]# ./spark-shell --master spark://node01:7077 --executor-memory 3G

提交任務顯示爲waiting狀態,而不是running狀態,也不會啓動executor

設置每個executor使用1個core,500M內存
[root@node04 bin]# ./spark-shell --master spark://node01:7077 --executor-cores 1 --executor-memory 500M

設置每個設置每個executor使用1個core,500M內存,集羣總共可以使用3個core,集羣總共啓動3個executor,其中有一個Worker啓動了兩個executor
[root@node04 bin]# ./spark-shell --master spark://node01:7077 --executor-cores 1 --executor-memory 500M --total-executor-cores 3

設置每個設置每個executor使用1個core,1.2內存,集羣總共可以使用3個core,集羣總共啓動2個executor,每個Worker啓動了1個executor,表面上看起來,兩個worker加起來的內存(1.6G)和剩餘的core數(1),還夠啓動一個exexutor,但是這裏需要注意的是,兩個Worker的內存並不能共用,每個Worker剩餘的內存(800M)並不足以啓動一個executor
[root@node04 bin]# ./spark-shell --master spark://node01:7077 --executor-cores 1 --executor-memory 1200M --total-executor-cores 3

任務調度源碼分析

源碼位置:core/src/main/scala/rdd/RDD.scala
spark任務調度

Spark Standalone集羣搭建

角色劃分

1.解壓安裝包

[root@node01 chant]# tar zxf spark-1.6.0-bin-hadoop2.6.tgz

2.編輯spark-env.sh文件


 

1

2

3

4

5

6

7

8

9

10

11

12


 

[root@node01 chant]# mv spark-1.6.0-bin-hadoop2.6 spark-1.6.0

[root@node01 chant]# cd spark-1.6.0/conf/

[root@node01 conf]# cp spark-env.sh.template spark-env.sh

[root@node01 conf]# vi spark-env.sh

# 綁定Master的IP

export SPARK_MASTER_IP=node01

# 提交Application的端口

export SPARK_MASTER_PORT=7077

# 每一個Worker最多可以支配core的個數,注意core是否支持超線程

export SPARK_WORKER_CORES=3

# 每一個Worker最多可以支配的內存

export SPARK_WORKER_MEMORY=2g

3.編輯slaves文件


 

1

2

3

4


 

[root@node01 conf]# cp slaves.template slaves

[root@node01 conf]# vi slaves

node02

node03

4.Spark的web端口默認爲8080,與Tomcat衝突,進行修改


 

1

2

3

4

5


 

[root@node01 spark-1.6.0]# cd sbin/

[root@node01 sbin]# vi start-master.sh

if [ "$SPARK_MASTER_WEBUI_PORT" = "" ]; then

SPARK_MASTER_WEBUI_PORT=8081

fi

5.同步配置


 

1

2

3

4


 

[root@node01 conf]# cd /opt/chant/

[root@node01 chant]# scp -r spark-1.6.0 node02:`pwd`

[root@node01 chant]# scp -r spark-1.6.0 node03:`pwd`

[root@node01 chant]# scp -r spark-1.6.0 node04:`pwd`

6.進入spark安裝目錄的sbin目錄下,啓動集羣


 

1

2


 

[root@node01 chant]# cd spark-1.6.0/sbin/

[root@node01 sbin]# ./start-all.sh

7.訪問web界面

8.提交Application驗證集羣是否工作正常

以下scala代碼是spark源碼包自帶的例子程序,用於計算圓周率,可傳入參數:


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23


 

package org.apache.spark.examples

import scala.math.random

import org.apache.spark._

/** Computes an approximation to pi */

object SparkPi {

def main(args: Array[String]) {

val conf = new SparkConf().setAppName("Spark Pi")

val spark = new SparkContext(conf)

val slices = if (args.length > 0) args(0).toInt else 2

// avoid overflow

val n = math.min(100000L * slices, Int.MaxValue).toInt

val count = spark.parallelize(1 to n, slices).map { i =>

val x = random * 2 - 1

val y = random * 2 - 1

if (x*x + y*y < 1) 1 else 0

}.reduce((v1,v2) => {v1+v2})

println("Pi is roughly " + 4.0 * count / n)

spark.stop()

}

}

此程序的jar包路徑爲:SPARK_HOME/lib/spark-examples-1.6.0-hadoop2.6.0.jar

Standalone模式下提交任務

Standalone模式:提交的任務在spark集羣中管理,包括資源調度,計算

客戶端方式提交任務

進入客戶端所在節點的spark安裝目錄的bin目錄下,提交這個程序:


 

1

2

3

4

5


 

[root@node04 bin]# ./spark-submit --master spark://node01:7077 #指定 master的地址

> --deploy-mode client #指定在客戶端提交任務,這個選項可以不寫,默認

> --class org.apache.spark.examples.SparkPi #指定程序的全名

> ../lib/spark-examples-1.6.0-hadoop2.6.0.jar #指定jar包路徑

> 1000 #程序運行時傳入的參數

說明:
1.客戶端提交,Driver進程就在客戶端啓動,進程名爲SparkSubmit


 

1

2

3

4


 

# 注意:任務結束後,該進程就關閉了

[root@node04 ~]# jps

1646 Jps

1592 SparkSubmit

2.在客戶端可以看到task執行情況和執行結果


 

1

2

3

4

5


 

……

17/08/04 02:55:47 INFO DAGScheduler: Job 0 finished: reduce at SparkPi.scala:36, took 6.602662 s

Pi is roughly 3.1409092

17/08/04 02:55:47 INFO SparkUI: Stopped Spark web UI at http://192.168.9.14:4040

……

3.適合場景:測試
原因:當提交的任務數量很多時,客戶端資源不夠用

集羣方式提交任務

還是在客戶端所在節點的spark安裝目錄的bin目錄下提交程序,只是命令需要修改:


 

1

2

3

4

5


 

[root@node04 bin]# ./spark-submit --master spark://node01:7077

> --deploy-mode cluster #指定在集羣中提交任務,這個選項必須寫

> --class org.apache.spark.examples.SparkPi

> ../lib/spark-examples-1.6.0-hadoop2.6.0.jar

> 1000

說明:
1.集羣方式提交任務,Driver進程隨機找一個Worker所在的節點啓動,進程名爲DriverWrapper


 

1

2

3

4


 

[root@node02 ~]# jps

1108 Worker

1529 Jps

1514 DriverWrapper

2.客戶端看不到task執行情況和執行結果,可以在web界面查看

3.適合場景:生產環境
原因:當task數量很多時,集羣方式可以做到負載均衡,解決多次網卡流量激增問題(分攤到集羣的Worker節點上),但無法解決單次網卡流量激增問題。

Yarn模式下提交任務

yarn模式:把spark任務提交給yarn集羣,由yarn集羣進行管理,包括資源分配和計算
編輯客戶端節點中spark配置文件,加入:
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop,啓動hadoop集羣,不需要啓動spark集羣

客戶端方式提交任務

1.命令
./spark-submit --master yarn --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100
2.流程

① 在客戶端執行提交命令
② 上傳應用程序(jar包)及其依賴的jar包到HDFS上,開啓Driver進程執行應用程序
③ 客戶端會向RS發送請求,爲當前的Application啓動一個ApplicationMaster進程
④ RS會找一臺NM啓動ApplicationMaster,ApplicationMaster進程啓動成功後,會向RS申請資源(圖畫的有誤,ApplicationMaster應該在NM上)
⑤ RS接受請求後,會向資源充足的NM發送消息:在當前的節點上啓動一個Executor進程,去HDFS下載spark-assembly-1.6.0-hadoop2.6.0.jar包,這個jar包中有啓動Executor進程的相關類,調用其中的方法就可以啓動Executor進程
⑥ Executor啓動成功後,Driver開始分發task,在集羣中執行任務

3.總結
Driver負責任務的調度
ApplicationMaster負責資源的申請

集羣方式提交任務

1.命令


 

1


 

./spark-submit --master yarn-cluster --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100

或者


 

1


 

./spark-submit --master yarn --deploy-mode cluster --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100

2.流程

① 在客戶端執行提交命令
② 上傳應用程序(jar包)及其依賴的jar包到HDFS上
③ 客戶端會向RS發送請求,爲當前的Application啓動一個ApplicationMaster(Driver)進程,這個ApplicationMaster就是driver。
④ RS會找一臺NM啓動ApplicationMaster,ApplicationMaster(Driver)進程啓動成功後,會向RS申請資源(圖畫的有誤,ApplicationMaster應該在NM上),ApplicationMaster(Driver)進程啓動成功後,會向RS申請資源
⑤ RS接受請求後,會向資源充足的NM發送消息:在當前的節點上啓動一個Executor進程,去HDFS下載spark-assembly-1.6.0-hadoop2.6.0.jar包,這個jar包中有啓動Executor進程的相關類,調用其中的方法就可以啓動Executor進程
⑥ Executor啓動成功後,ApplicationMaster(Driver)開始分發task,在集羣中執行任務
3.總結
在cluster提交方式中,ApplicationMaster進程就是Driver進程,任務調度和資源申請都是由一個進程來做的

Spark HA集羣搭建

Spark高可用的原理

說明:
主備切換的過程中,不能提交新的Application。
已經提交的Application在執行過程中,集羣進行主備切換,是沒有影響的,因爲spark是粗粒度的資源調度。

角色劃分

1.修改spark-env.sh配置文件


 

1

2

3

4

5

6

7


 

[root@node01 ~]# cd /opt/chant/spark-1.6.0/conf/

[root@node01 conf]# vi spark-env.sh

加入以下配置

export SPARK_DAEMON_JAVA_OPTS="

-Dspark.deploy.recoveryMode=ZOOKEEPER

-Dspark.deploy.zookeeper.url=node01:2181,node02:2181,node03:2181

-Dspark.deploy.zookeeper.dir=/spark/ha"

2. 同步配置文件


 

1

2

3


 

[root@node01 conf]# scp spark-env.sh node02:`pwd`

[root@node01 conf]# scp spark-env.sh node03:`pwd`

[root@node01 conf]# scp spark-env.sh node04:`pwd`

3. 修改node02的spark配置文件

把master的IP改爲node02,把node02的masterUI port改爲8082
因爲node01的masterUI的port設置爲8081,同步後,node02的masterUI的port也爲8081,那麼在node02啓動master進程時,日誌中會有警告:
WARN Utils: Service ‘MasterUI’ could not bind on port 8081
導致我們不能通過該port訪問node02的masterUI,所以修改的和node01不一樣就可以


 

1

2

3

4

5

6

7

8


 

[root@node02 ~]# cd /opt/chant/spark-1.6.0/conf

[root@node02 conf]# vi spark-env.sh

export SPARK_MASTER_IP=node02

[root@node02 conf]# cd ../sbin

[root@node02 sbin]# vi start-master.sh

if [ "$SPARK_MASTER_WEBUI_PORT" = "" ]; then

SPARK_MASTER_WEBUI_PORT=8082

fi

4. 啓動Zookeeper集羣


 

1

2

3


 

[root@node02 ~]# zkServer.sh start

[root@node03 ~]# zkServer.sh start

[root@node04 ~]# zkServer.sh start

5.在node01上啓動Spark集羣


 

1

2

3

4


 

[root@node01 conf]# cd ../sbin

[root@node01 sbin]# pwd

/opt/chant/spark-1.6.0/sbin

[root@node01 sbin]# ./start-all.sh

6.在node02上啓動Master進程


 

1

2

3

4


 

[root@node02 bin]# cd ../sbin

[root@node02 sbin]# pwd

/opt/chant/spark-1.6.0/sbin

[root@node02 sbin]# ./start-master.sh

7.驗證集羣高可用



 

1

2

3

4


 

[root@node01 sbin]# jps

1131 Master

1205 Jps

[root@node01 sbin]# kill -9 1131


再次啓動node01的master進程,node01成爲standby
[root@node01 sbin]# ./start-master.sh

Spark History Server配置

提交一個Application:


 

1

2


 

[root@node04 ~]# cd /opt/chant/spark-1.6.0/bin

[root@node04 bin]# ./spark-shell --name "testSparkShell" --master spark://node02:7077


點擊ApplicationID

點擊appName查看job信息

提交一個job


 

1

2


 

scala> sc.textFile("/tmp/wordcount_data")

.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).saveAsTextFile("/tmp/wordcount_result")


點擊job查看stage信息

點擊stage查看task信息

退出Spark-shell後,這個Application的信息是不被保存的,需要做一些配置纔會保存歷史記錄,有兩種方法設置保存歷史記錄
1.提交命令時指定
./spark-shell --master spark://node02:7077 --conf spark.eventLog.enabled=true --conf spark.eventLog.dir="/tmp/spark/historyLog"
注意:保存歷史數據的目錄需要先創建好
2.啓動history-server
修改conf/spark-defaults.conf文件


 

1

2

3


 

spark.eventLog.enabled true

spark.eventLog.dir hdfs://node01:9000/spark/historyLog

spark.history.fs.logDirectory hdfs://node01:9000/spark/historyLog

spark.eventLog.compress true 可以設置保存歷史日誌時進行壓縮
注意:保存歷史數據的目錄需要先創建好
然後啓動history server:sbin/start-history-server.sh
之後提交的所有的Application的執行記錄都會被保存,訪問18080端口就可以查看

Spark Shuffle

reduceByKey會將上一個RDD中的每一個key對應的所有value聚合成一個value,然後生成一個新的RDD,元素類型是對的形式,這樣每一個key對應一個聚合起來的value

存在的問題:
每一個key對應的value不一定都是在一個partition中,也不太可能在同一個節點上,因爲RDD是分佈式的彈性的數據集,他的partition極有可能分佈在各個節點上。

那麼如何進行聚合?
Shuffle Write:上一個stage的每個map task就必須保證將自己處理的當前分區中的數據相同的key寫入一個分區文件中,可能會寫入多個不同的分區文件中
Shuffle Read:reduce task就會從上一個stage的所有task所在的機器上尋找屬於自己的那些分區文件,這樣就可以保證每一個key所對應的value都會匯聚到同一個節點上去處理和聚合

普通的HashShuffle


上圖中,每個節點啓動一個Executor來運行Application,每個Executor使用1個core,其中有2條task,所以2條task不是並行執行的。Map task每計算一條數據之後,就寫到對應的buffer(默認32K)中(比如key爲hello的寫入到藍色buffer,key爲world的寫入到紫色buffer中),當buffer到達閾值後,把其中的數據溢寫到磁盤,當task0執行完後,task2開始執行,在這個過程中,每一個map task產生reduce的個數個小文件,假如總共有m個map task,r個reduce,最終會產生m*r個小文件,磁盤小文件和緩存過多,造成耗時且低效的IO操作,可能造成OOM

內存估算

假設:一臺服務器24核,支持超線程,1000個maptask,1000個reduceTask。
超線程相當於48核,所以並行48個mapTask,每個mapTask產生1000個(reduceTask數)磁盤小文件,也就是對應產生48*1000個buffer,而每個buffer默認值爲32k。
所以使用內存爲:maptask數*reduceTask數*buffer大小 = 48*1000*32k=1536M1.5G

優化的HashShuffle

這裏說的優化,是指我們可以設置一個參數,spark.shuffle.consolidateFiles。該參數默認值爲false,將其設置爲true即可開啓優化機制。通常來說,如果我們使用HashShuffleManager,那麼都建議開啓這個選項。

每個map task 之間可以共享buffer,task0執行完成後,task1開始執行,繼續使用task0使用的buffer,假如總共有c個core, r個reduce,最終會產生c*r個小文件,因爲複用buffer後,每個core執行的所有map task產生r個小文件

普通的SortShuffle

下圖說明了普通的SortShuffleManager的原理。在該模式下,數據會先寫入一個內存數據結構中,此時根據不同的shuffle算子,可能選用不同的數據結構。如果是reduceByKey這種聚合類的shuffle算子,那麼會選用Map數據結構,一邊通過Map進行聚合,一邊寫入內存;如果是join這種普通的shuffle算子,那麼會選用Array數據結構,直接寫入內存。接着,每寫一條數據進入內存數據結構之後,就會判斷一下,是否達到了某個臨界閾值。如果達到臨界閾值的話,那麼就會嘗試將內存數據結構中的數據溢寫到磁盤,然後清空內存數據結構。

  1. 每個maptask將計算結果寫入內存數據結構中,這個內存默認大小爲5M
  2. 會有一個“監控器”來不定時的檢查這個內存的大小,如果寫滿了5M,比如達到了5.01M,那麼再給這個內存申請5.02M(5.01M * 2 – 5M = 5.02)的內存,此時這個內存空間的總大小爲10.02M
  3. 當“定時器”再次發現數據已經寫滿了,大小10.05M,會再次給它申請內存,大小爲 10.05M * 2 – 10.02M = 10.08M
  4. 假如此時總的內存只剩下5M,不足以再給這個內存分配10.08M,那麼這個內存會被鎖起來,把裏面的數據按照相同的key爲一組,進行排序後,分別寫到不同的緩存中,然後溢寫到不同的小文件中,而map task產生的新的計算結果會寫入總內存剩餘的5M中
  5. buffer中的數據(已經排好序)溢寫的時候,會分批溢寫,默認一次溢寫10000條數據,假如最後一部分數據不足10000條,那麼剩下多少條就一次性溢寫多少條
  6. 每個map task產生的小文件,最終合併成一個大文件來讓reduce拉取數據,合成大文件的同時也會生成這個大文件的索引文件,裏面記錄着分區信息和偏移量(比如:key爲hello的數據在第5個字節到第8097個字節)
  7. 最終產生的小文件數爲2*m(map task的數量)

SortShuffle的bypass機制

下圖說明了bypass SortShuffleManager的原理。bypass運行機制的觸發條件如下:

  • shuffle map task數量小於spark.shuffle.sort.bypassMergeThreshold參數的值(默認200)。
  • 不是聚合類的shuffle算子(比如reduceByKey)。因爲如果是聚合類的shuffle算子,那麼會選用Map數據結構,一邊通過Map進行聚合,一邊寫入內存,而bypass機制下是無法聚合的。

有條件的sort,當shuffle reduce task數量小於spark.shuffle.sort.bypassMergeThreshold參數的值(默認200)時,會觸發bypass機制,不進行sort,假如目前有300個reduce task,如果要觸發bypass機制,就就設置spark.shuffle.sort.bypassMergeThreshold的值大於300,bypass機制最終產生2*m(map task的數量)的小文件。

SparkShuffle詳解

先了解一些角色:

MapOutputTracker:管理磁盤小文件的地址

  • 主:MapOutputTrackerMaster
  • 從:MapOutputTrackerWorker

BlockManager:

主:BlockManagerMaster,存在於Driver端

管理範圍:RDD的緩存數據、廣播變量、shuffle過程產生的磁盤小文件
包含4個重要對象:

  1. ConnectionManager:負責連接其他的BlockManagerSlave
  2. BlockTransferService:負責數據傳輸
  3. DiskStore:負責磁盤管理
  4. Memstore:負責內存管理

從:BlockManagerSlave,存在於Executor端

包含4個重要對象:

  1. ConnectionManager:負責連接其他的BlockManagerSlave
  2. BlockTransferService:負責數據傳輸
  3. DiskStore:負責磁盤管理
  4. Memstore:負責內存管理

Shuffle調優

配置參數的三種方式

  1. 在程序中硬編碼<
    br>例如sparkConf.set("spark.shuffle.file.buffer","64k")
  2. 提交application時在命令行指定<
    br>例如spark-submit --conf spark.shuffle.file.buffer=64k --conf 配置信息=配置值 ...
  3. 修改SPARK_HOME/conf/spark-default.conf配置文件

    薦使用第2種方式

spark.shuffle.file.buffer

默認值:32K
參數說明:該參數用於設置shuffle write task的BufferedOutputStream的buffer緩衝大小。將數據寫到磁盤文件之前,會先寫入buffer緩衝中,待緩衝寫滿之後,纔會溢寫到磁盤。
調優建議:如果作業可用的內存資源較爲充足的話,可以適當增加這個參數的大小(比如64k),從而減少shuffle write過程中溢寫磁盤文件的次數,也就可以減少磁盤IO次數,進而提升性能。在實踐中發現,合理調節該參數,性能會有1%~5%的提升。

spark.reducer.maxSizeInFlight

默認值:48M
參數說明:該參數用於設置shuffle read task的buffer緩衝大小,而這個buffer緩衝決定了每次能夠拉取多少數據。
調優建議:如果作業可用的內存資源較爲充足的話,可以適當增加這個參數的大小(比如96M),從而減少拉取數據的次數,也就可以減少網絡傳輸的次數,進而提升性能。在實踐中發現,合理調節該參數,性能會有1%~5%的提升。

spark.shuffle.io.maxRetries

默認值:3
參數說明:shuffle read task從shuffle write task所在節點拉取屬於自己的數據時,如果因爲網絡異常導致拉取失敗,是會自動進行重試的。該參數就代表了可以重試的最大次數。如果在指定次數之內拉取還是沒有成功,就可能會導致作業執行失敗。
調優建議:對於那些包含了特別耗時的shuffle操作的作業,建議增加重試最大次數(比如60次),以避免由於JVM的full gc或者網絡不穩定等因素導致的數據拉取失敗。在實踐中發現,對於針對超大數據量(數十億~上百億)的shuffle過程,調節該參數可以大幅度提升穩定性。

spark.shuffle.io.retryWait

默認值:5s
參數說明:具體解釋同上,該參數代表了每次重試拉取數據的等待間隔,默認是5s。
調優建議:建議加大間隔時長(比如60s),以增加shuffle操作的穩定性。

spark.shuffle.memoryFraction

默認值:0.2
參數說明:該參數代表了Executor內存中,分配給shuffle read task進行聚合操作的內存比例,默認是20%。
調優建議:在資源參數調優中講解過這個參數。如果內存充足,而且很少使用持久化操作,建議調高這個比例,給shuffle read的聚合操作更多內存,以避免由於內存不足導致聚合過程中頻繁讀寫磁盤。在實踐中發現,合理調節該參數可以將性能提升10%左右。

spark.shuffle.manager

默認值:sort
參數說明:該參數用於設置ShuffleManager的類型。Spark 1.5以後,有三個可選項:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默認選項,但是Spark 1.2以及之後的版本默認都是SortShuffleManager了。tungsten-sort與sort類似,但是使用了tungsten計劃中的堆外內存管理機制,內存使用效率更高。
調優建議:由於SortShuffleManager默認會對數據進行排序,因此如果你的業務邏輯中需要該排序機制的話,則使用默認的SortShuffleManager就可以;而如果你的業務邏輯不需要對數據進行排序,那麼建議參考後面的幾個參數調優,通過bypass機制或優化的HashShuffleManager來避免排序操作,同時提供較好的磁盤讀寫性能。這裏要注意的是,tungsten-sort要慎用,因爲之前發現了一些相應的bug。

spark.shuffle.sort.bypassMergeThreshold

默認值:200
參數說明:當ShuffleManager爲SortShuffleManager時,如果shuffle read task的數量小於這個閾值(默認是200),則shuffle write過程中不會進行排序操作,而是直接按照未經優化的HashShuffleManager的方式去寫數據,但是最後會將每個task產生的所有臨時磁盤文件都合併成一個文件,並會創建單獨的索引文件。
調優建議:當你使用SortShuffleManager時,如果的確不需要排序操作,那麼建議將這個參數調大一些,大於shuffle read task的數量。那麼此時就會自動啓用bypass機制,map-side就不會進行排序了,減少了排序的性能開銷。但是這種方式下,依然會產生大量的磁盤文件,因此shuffle write性能有待提高。

spark.shuffle.consolidateFiles

默認值:false
參數說明:如果使用HashShuffleManager,該參數有效。如果設置爲true,那麼就會開啓consolidate機制,會大幅度合併shuffle write的輸出文件,對於shuffle read task數量特別多的情況下,這種方法可以極大地減少磁盤IO開銷,提升性能。
調優建議:如果的確不需要SortShuffleManager的排序機制,那麼除了使用bypass機制,還可以嘗試將spark.shffle.manager參數手動指定爲hash,使用HashShuffleManager,同時開啓consolidate機制。在實踐中嘗試過,發現其性能比開啓了bypass機制的SortShuffleManager要高出10%~30%。

Spark內存管理

spark1.5之前默認爲靜態內存管理,之後默認爲統一的內存管理,如果對數據比較瞭解,那麼選用靜態內存管理可調控的參數多,若想使用靜態內存管理,將spark.memory.useLegacyMode從默認值false改爲true即可。
這裏闡述的是spark1.6版本的內存管理機制,想了解更前衛的版本,請戳spark2.1內存管理機制

靜態內存管理

Unrolling

The memory used for unrolling is borrowed from the storage space. If there are no existing blocks, unrolling can use all of the storage space. Otherwise, unrolling can drop up to M bytes worth of blocks from memory, where M is a fraction of the storage space configurable through spark.storage.unrollFraction(default0.2). Note that this sub­region is not staticallyreserved, but dynamically allocated by dropping existing blocks.

所以這裏的unrollFraction內存其實是個上限,不是靜態固定的0.2,而是動態分配的。

關於Unrolling的詳細解讀,請戳RDD緩存的過程

RDD在緩存到存儲內存之後,Partition被轉換成Block,Record在堆內或堆外存儲內存中佔用一塊連續的空間。將Partition由不連續的存儲空間轉換爲連續存儲空間的過程,Spark稱之爲“展開”(Unroll)。Block有序列化和非序列化兩種存儲格式,具體以哪種方式取決於該RDD的存儲級別。非序列化的Block以一種DeserializedMemoryEntry的數據結構定義,用一個數組存儲所有的Java對象,序列化的Block則以SerializedMemoryEntry的數據結構定義,用字節緩衝區(ByteBuffer)來存儲二進制數據。每個Executor的Storage模塊用一個鏈式Map結構(LinkedHashMap)來管理堆內和堆外存儲內存中所有的Block對象的實例[6],對這個LinkedHashMap新增和刪除間接記錄了內存的申請和釋放。

Reduce OOM怎麼辦?

  1. 減少每次拉取的數據量
  2. 提高shuffle聚合的內存比例
  3. 增加executor的內存

統一內存管理

統一內存管理中互相借用(申請,檢查,借用,歸還)這一環節會產生額外的計算開銷。
其中最重要的優化在於動態佔用機制,其規則如下:

  • 設定基本的存儲內存和執行內存區域(spark.storage.storageFraction 參數),該設定確定了雙方各自擁有的空間的範圍
  • 雙方的空間都不足時,則存儲到硬盤;若己方空間不足而對方空餘時,可借用對方的空間;(存儲空間不足是指不足以放下一個完整的 Block)
  • 執行內存的空間被對方佔用後,可讓對方將佔用的部分轉存到硬盤,然後”歸還”借用的空間


憑藉統一內存管理機制,Spark 在一定程度上提高了堆內和堆外內存資源的利用率,降低了開發者維護 Spark 內存的難度,但並不意味着開發者可以高枕無憂。譬如,所以如果存儲內存的空間太大或者說緩存的數據過多,反而會導致頻繁的全量垃圾回收,降低任務執行時的性能,因爲緩存的 RDD 數據通常都是長期駐留內存的 [5] 。所以要想充分發揮 Spark 的性能,需要開發者進一步瞭解存儲內存和執行內存各自的管理方式和實現原理。

Spark SQL

簡介

Spark SQL的前身是shark,Shark是基於Spark計算框架之上且兼容Hive語法的SQL執行引擎,由於底層的計算採用了Spark,性能比MapReduce的Hive普遍快2倍以上,當數據全部load在內存的話,將快10倍以上,因此Shark可以作爲交互式查詢應用服務來使用。除了基於Spark的特性外,Shark是完全兼容Hive的語法,表結構以及UDF函數等,已有的HiveSql可以直接進行遷移至Shark上。Shark底層依賴於Hive的解析器,查詢優化器,但正是由於Shark的整體設計架構對Hive的依賴性太強,難以支持其長遠發展,比如不能和Spark的其他組件進行很好的集成,無法滿足Spark的一棧式解決大數據處理的需求
Hive是Shark的前身,Shark是SparkSQL的前身,相對於Shark,SparkSQL有什麼優勢呢?

  • SparkSQL產生的根本原因,是因爲它完全脫離了Hive的限制
  • SparkSQL支持查詢原生的RDD,這點就極爲關鍵了。RDD是Spark平臺的核心概念,是Spark能夠高效的處理大數據的各種場景的基礎
  • 能夠在Scala中寫SQL語句。支持簡單的SQL語法檢查,能夠在Scala中寫Hive語句訪問Hive數據,並將結果取回作爲RDD使用

Spark和Hive有兩種組合

Hive on Spark類似於Shark,相對過時,現在公司一般都採用Spark on Hive。

Spark on Hive

Hive只是作爲了存儲的角色
SparkSQL作爲計算的角色

Hive on Spark

Hive承擔了一部分計算(解析SQL,優化SQL…)的和存儲
Spark作爲了執行引擎的角色

Dataframe

簡介

Spark SQL是Spark的核心組件之一,於2014年4月隨Spark 1.0版一同面世,在Spark 1.3當中,Spark SQL終於從alpha(內測版本)階段畢業。Spark 1.3更加完整的表達了Spark SQL的願景:讓開發者用更精簡的代碼處理儘量少的數據,同時讓Spark SQL自動優化執行過程,以達到降低開發成本,提升數據分析執行效率的目的。與RDD類似,DataFrame也是一個分佈式數據容器。然而DataFrame更像傳統數據庫的二維表格,除了數據以外,還掌握數據的結構信息,即schema。同時,與Hive類似,DataFrame也支持嵌套數據類型(struct、array和map)。從API易用性的角度上看,DataFrame API提供的是一套高層的關係操作,比函數式的RDD API要更加友好,門檻更低。

RDD VS DataFrame

DataFrame = SchemaRDD = RDD<ROW>

從圖蟲顏色來區分,DataFrame是列式存儲。當要取Age這一列時,RDD必須先取出person再取Age,而DataFrame可以直接取Age這一列。

DataFrame底層架構

Predicate Pushdown謂詞下推機制

執行如下SQL語句:


 

1

2

3


 

SELECT table1.name,table2.score

FROM table1 JOIN table2 ON (table1.id=table2.id)

WHERE table1.age>25 AND table2.score>90

我們比較一下普通SQL執行流程和Spark SQL的執行流程

DataFrame創建方式

1.讀JSON文件(不能嵌套)


 

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


 

/**

 * people.json

 * {"name":"Michael"}

   {"name":"Andy", "age":30}

   {"name":"Justin", "age":19}

 */

object DataFrameOpsFromFile {

  def main(args: Array[String]): Unit = {

    val conf = new SparkConf() 

    conf.setAppName("SparkSQL")

    conf.setMaster("local")

    val sc = new SparkContext(conf)

val sqlContext = new SQLContext(sc)

//val df = sqlContext.read.format("json").load("people.json")    

val df = sqlContext.read.json("people.json")

    

    //將DF註冊成一張臨時表,這張表是邏輯上的,數據並不會落地

    //people是臨時表的表名,後面的SQL直接FROM這個表名

    df.registerTempTable("people")

    //打印DataFrame的結構

df.printSchema()

/*

     * 結果:nullable=true代表該字段可以爲空

     * root

       |-- age: long (nullable = true)

       |-- name: string (nullable = true)

     */

 //查看DataFrame中的數據, df.show(int n)可以指定顯示多少條數據

     df.show()

    /*

      * 結果:

      * +----+-------+

        | age|   name|

        +----+-------+

        |null|Michael|

        |  30|   Andy|

        |  19| Justin|

        +----+-------+

      */

//SELECT name from table

df.select("name").show()

    

//SELECT name,age+10 from table

df.select(df("name"), df("age").plus(10)).show()

    

//SELECT * FROM table WHERE age > 10

df.filter(df("age")>10).show()

    

//SELECT count(*) FROM table GROUP BY age

df.groupBy("age").count.show()

sqlContext.sql("select * from people where age > 20").show()

  }

}

2.JSON格式的RDD轉爲DataFrame


 

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


 

public class DataFrameOpsFromJsonRdd {

     public static void main(String[] args) {

         SparkConf conf = new SparkConf()

.setAppName("DataFrameFromJsonRdd").setMaster("local");

         JavaSparkContext sc = new JavaSparkContext(conf);

         //若想使用SparkSQL必須創建SQLContext,必須是傳入SparkContext,不能是SparkConf

         SQLContext sqlContext = new SQLContext(sc);

         

         //創建一個本地的集合,類型String,集合中元素的格式爲json格式       

         List<String> nameList = Arrays.asList(

                            "{'name':'Tom', 'age':20}",

                            "{'name':'Jed', 'age':30}",

                            "{'name':'Tony', 'age':22}",

                            "{'name':'Jack', 'age':24}");

         List<String> scoreList = Arrays.asList(

                  "{'name':'Tom','score':100}",

                  "{'name':'Jed','score':99}" );

         

         JavaRDD<String> nameRDD = sc.parallelize(nameList);

         JavaRDD<String> scoreRDD = sc.parallelize(scoreList);

         

         DataFrame nameDF = sqlContext.read().json(nameRDD);

         DataFrame scoreDF = sqlContext.read().json(scoreRDD);

         

         /**

          * SELECT nameTable.name,nameTable.age,scoreTable.score

      FROM nameTable JOIN nameTable ON (nameTable.name = scoreTable.name)

          */

         nameDF.join(

scoreDF, nameDF.col("name").$eq$eq$eq(scoreDF.col("name"))

).select(

nameDF.col("name"),nameDF.col("age"),scoreDF.col("score"))

.show();         

         

          nameDF.registerTempTable("name");

         scoreDF.registerTempTable("score");

         String sql = "SELECT name.name,name.age,score.score "

                  + "FROM name join score ON (name.name = score.name)";

         

         sqlContext.sql(sql).show();

         /*

          * +----+---+-----+

            |name|age|score|

            +----+---+-----+

            | Tom| 20|  100|

            | Jed| 30|   99|

            +----+---+-----+

          */

     }

}

3.非JSON格式的RDD轉爲DataFrame

1.反射的方式

Person類


 

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


 

import org.apache.spark.{SparkConf, SparkContext}

import org.apache.spark.sql.SQLContext

object RDD2DataFrameByReflectionScala {

/**

*  * 使用反射的方式將RDD轉換成爲DataFrame

*  * 1.自定義的類必須是public

*  * 2.自定義的類必須是可序列化的

*  * 3.RDD轉成DataFrame的時候,會根據自定義類中的字段名進行排序

*  *所以這裏直接用case class類

*  * Peoples.txt內容:

*      1,Tom,7

*      2,Tony,11

*      3,Jack,5

*  * @author root

*  */

case class Person(name: String, age: Int)

def main(args: Array[String]): Unit = {

val conf = new SparkConf() //創建sparkConf對象

conf.setAppName("My First Spark App") //設置應用程序的名稱,在程序運行的監控頁面可以看到名稱

conf.setMaster("local")

val sc = new SparkContext(conf)

val sqlContext = new SQLContext(sc)

import sqlContext.implicits._

//傳入進去Person.class的時候,sqlContext是通過反射的方式創建DataFrame

//在底層通過反射的方式或得Person的所有field,結合RDD本身,就生成了DataFrame

val people = sc.textFile("/Users/Chant/Documents/大數據0627/14spark/code/SparkJavaOperator/Peoples.txt")

.map(_.split(",")).map(p => Person(p(1), p(2).trim.toInt)).toDF()

people.registerTempTable("people")

val teenagers = sqlContext.sql("SELECT name, age FROM people WHERE age >= 6 AND age <= 19")

/**

* 對dataFrame使用map算子後,返回類型是RDD<Row>

*/

// teenagers.map(t => "Name: " + t(0)).foreach(println)

// teenagers.map(t => "Name: " + t.get(0)).foreach(println)

// 不推薦此方法,因爲RDD轉成DataFrame的時候,他會根據自定義類中的字段名(按字典序)進行排序。

// 如果列很多,那你就得自己去按字典序排序列名,然後才知道該列對應的索引位。

// or by field name: 推薦用列名來獲取列值

// teenagers.map(t => "Name: " + t.getAs[String]("name")).foreach(println)

// teenagers.map(t => t.getAs("name")).foreach(println)//這樣會報錯java.lang.ClassCastException: java.lang.String cannot be cast to scala.runtime.Nothing$

teenagers.map(t => t.getAs[String]("name")).foreach(println)

}

}

2.動態創建Schema


 

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


 

import org.apache.spark.sql.types._

import org.apache.spark.sql.{Row, SQLContext}

import org.apache.spark.{SparkConf, SparkContext}

/**

* Created by Chant.

*/

object RDD2DataFrameByProgrammatically {

def main(args: Array[String]): Unit = {

val sc = new SparkContext(new SparkConf().setMaster("local").setAppName("RDD2DataFrameByProgrammatically"))

val sQLContext = new SQLContext(sc)

//將數據封裝到Row中,RDD[Row]就可以轉成DataFrame

val RowRDD = sc.textFile("/Users/Chant/Documents/大數據0627/14spark/code/SparkScalaOperator/Peoples.txt")

.map(_.split(",")).map(x => Row(x(0).toInt, x(1), x(2).toInt))

/**

* 讀取配置文件的方式

*/

val schemaString = "id:Int name:String age:Int"

val schema = StructType(schemaString.split(" ")

.map(x => StructField(x.split(":")(0), if (x.split(":")(1) == "String") StringType else IntegerType, true)))

//true代表該字段是否可以爲空

//構建StructType,用於最後DataFrame元數據的描述

//基於已有的MetaData以及RDD<Row> 來構造DataFrame

// //最後一定要寫else,不像java可以只有個if。

// val schema = StructType(schemaString.split(" ")

// .map(x =>{

// val pair = x.split(":")

// if (pair(1) == "String") StructField(pair(0), StringType,true)

// else if(pair(1) == "Int") StructField(pair(0), IntegerType,true)

// else StructField(pair(0), IntegerType,true)

// }))

/**

* 如果列的數據類型比較多,可以用match case

*/

// val schemaString = "id:Int name:String age:Int"

//

// val schema = StructType(schemaString.split(" ")

// .map(x => {

// val pair = x.split(":")

//// var dataType = null.asInstanceOf[DataType]//巧用這一招

// var dataType : DataType = null

// pair(1) match {

// case "String" => dataType = StringType

// case "Int" => dataType = IntegerType

// case "Double" => dataType = DoubleType

// case "Long" => dataType = LongType

// case _ => println("default, can't match")

// }

//

// StructField(pair(0),dataType,true)

// }))

/**

* 傻瓜式

*/

// val structField = Array(StructField("id", IntegerType, true), StructField("name", StringType, true), StructField("age", IntegerType, true))

// // val schema = StructType.apply(structField)

// val schema = StructType(structField)

val df = sQLContext.createDataFrame(RowRDD, schema)

df.printSchema()

df.show()

df.registerTempTable("people")

val res = sQLContext.sql("select * from people where age > 6")

res.show()

res.map(x => "Name: " + x.getAs[String]("name") + "\t Age: " + x.getAs[Int]("age")).foreach(println)

}

}

4. 讀取MySQL中的數據來創建DataFrame


 

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


 

package com.chant.sql.jdbc

import java.sql.{Connection, DriverManager, PreparedStatement}

import org.apache.spark.sql.SQLContext

import org.apache.spark.{SparkConf, SparkContext}

/**

* Created by Chant.

*/

object JDBCDataSource2 {

def main(args: Array[String]): Unit = {

val sc = new SparkContext(new SparkConf().setAppName("JDBCDataSource2").setMaster("local"))

val sqlContext = new SQLContext(sc)

val reader = sqlContext.read.format("jdbc")

reader.option("url", "jdbc:mysql://node01:3306/testdb")

reader.option("driver", "com.mysql.jdbc.Driver")

reader.option("user", "root")

reader.option("password", "123456")

reader.option("dbtable", "student_info")

val stuInfoDF = reader.load()

reader.option("dbtable","student_score")

val stuScoreDF = reader.load()

// 分別將mysql中兩張表的數據加載並註冊爲DataFrame

stuInfoDF.registerTempTable("stuInfos")

stuScoreDF.registerTempTable("stuScores")

val sql = "select stuInfos.name, age, score from stuInfos join stuScores " +

"on (stuInfos.name = stuScores.name) where stuScores.score > 80"

//執行sql,join兩個DF

val resDF = sqlContext.sql(sql)

resDF.show()

// 將join後的數據寫入的數據庫

resDF.rdd.foreachPartition(p =>{

Class.forName("com.mysql.jdbc.Driver")

var conn : Connection = null//這樣寫,在finnaly的時候才能訪問到並將其關閉

var ps : PreparedStatement = null

val sql2 = "insert into good_student_info values(?,?,?)"

try{

conn = DriverManager.getConnection("jdbc:mysql://node01:3306/testdb", "root", "123456")//獲取數據庫鏈接

conn.setAutoCommit(false)//關閉自動提交

ps = conn.prepareStatement(sql2)//準備sql

//迭代添加數據,並添加帶batch

p.foreach(row =>{

ps.setString(1, row.getAs[String]("name"))

ps.setInt(2, row.getAs[Int]("age"))

ps.setInt(3, row.getAs[Int]("score"))

ps.addBatch()

})

//執行並提交

ps.executeBatch()

conn.commit()//貌似數據量少的時候,不提交也會有數據寫入數據庫???尚存疑問。但是肯定要寫

}catch{

case e:Exception => e.printStackTrace()

}finally {

if(ps != null) ps.close()

if(conn != null) conn.close()

}

})

sc.stop()

}

}

5. 讀取Hive中的數據創建一個DataFrame(Spark on Hive)

Spark與Hive整合:
1) 編輯spark客戶端的配置文件hive-site.xml


 

1

2

3

4

5

6

7

8

9

10


 

node04:vi /opt/chant/spark-1.6.0/conf/hive-site.xml

<configuration>

<property>

<name>hive.metastore.uris</name>

<value>thrift://node04:9083</value>

<description>

Thrift uri for the remote metastore. Used by metastore client to connect to remote metastore.

</description>

</property>

</configuration>

2) 把hadoop的core-site.xml和hdfs-site.xml copy到SPARK_HOME/conf/下
3) node0{1,2,3}:zkServer.sh start
4) node01:start-dfs.sh
5) node01:service mysqld start
6) node04:hive –service metastore
7) node01:/opt/chant/spark-1.6.0/sbin/start-all.sh
8) node02:/opt/chant/spark-1.6.0/sbin/start-master.sh
9) node04:/opt/chant/spark-1.6.0/bin/spark-submit
–master spark://node01:7077,node02:7077
–class com.bjchant.java.spark.sql.hive.HiveDataSource
../TestHiveContext.jar
jar包中的測試代碼如下:


 

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


 

import org.apache.spark.SparkConf

import org.apache.spark.SparkContext

import org.apache.spark.sql.hive.HiveContext

object HiveDataSource {

def main(args: Array[String]): Unit = {

val conf = new SparkConf()

.setAppName("HiveDataSource");

val sc = new SparkContext(conf);

val hiveContext = new HiveContext(sc);

hiveContext.sql("DROP TABLE IF EXISTS student_infos");

hiveContext.sql("CREATE TABLE IF NOT EXISTS student_infos (name STRING, age INT) row format delimited fields terminated by '\t'");

hiveContext.sql("LOAD DATA "

+ "LOCAL INPATH '/root/resource/student_infos' "

+ "INTO TABLE student_infos");

hiveContext.sql("DROP TABLE IF EXISTS student_scores");

hiveContext.sql("CREATE TABLE IF NOT EXISTS student_scores (name STRING, score INT) row format delimited fields terminated by '\t'");

hiveContext.sql("LOAD DATA "

+ "LOCAL INPATH '/root/resource/student_scores' "

+ "INTO TABLE student_scores");

val goodStudentsDF = hiveContext.sql("SELECT si.name, si.age, ss.score "

+ "FROM student_infos si "

+ "JOIN student_scores ss ON si.name=ss.name "

+ "WHERE ss.score>=80");

hiveContext.sql("DROP TABLE IF EXISTS good_student_infos");

// goodStudentsDF.saveAsTable("good_student_infos");

hiveContext.sql("USE result")

//將goodStudentsDF裏面的值寫入到Hive表中,如果表不存在,會自動創建然後將數據插入到表中

goodStudentsDF.write.saveAsTable("good_student_infos")

}

}

DataFrame數據存儲

  1. 存儲到hive表中
    把hive表讀取爲dataFrame
    dataFrame = hiveContext().table("table_name");
    把dataFrame轉爲hive表存儲到hive中,若table不存在,自動創建
    dataFrame.write().saveAsTable("table_name");

2.存儲到MySQL/HBase/Redis…中


 

1

2

3


 

dataFrame.javaRDD().foreachPartition(new VoidFunction<Row>() {

……

})

3.存儲到parquet文件(壓縮比大,節省空間)中


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15


 

DataFrame usersDF =

sqlContext.read().format("parquet").load("hdfs://node01:9000/input/users.parquet");

usersDF.registerTempTable("users");

DataFrame resultDF = sqlContext.sql("SELECT * FROM users WHERE name = 'Tom'");

resultDF.write().format("parquet ").mode(SaveMode.Ignore)

.save("hdfs://node01:9000/output/result. parquet ");

resultDF.write().format("json").mode(SaveMode. Overwrite)

.save("hdfs://node01:9000/output/result.json");

public enum SaveMode {

Append, //如果文件已經存在,追加

Overwrite, //如果文件已經存在,覆蓋

ErrorIfExists, //如果文件已經存在,報錯

Ignore//如果文件已經存在,不對原文件進行任何修改,即不存儲DataFrame

}

parquet數據源會自動推斷分區,類似hive裏面的分區表的概念
文件存儲的目錄結構如下:


 

1

2

3

4

5


 

/users

|/country=US

|data:id,name

|/country=ZH

|data:id,name

當執行以下代碼


 

1

2

3

4

5

6

7

8

9

10


 

DataFrame usersDF = sqlContext.read().parquet(

"hdfs://node01:9000/users"); //路徑只寫到users

usersDF.printSchema();

/*

*(id int, name string, country string)

*/

usersDF.show();

usersDF.registerTempTable("table1");

sqlContext.sql("SELECT count(0) FROM table1 WHERE country = 'ZH'").show();

//執行這個sql只需要去country=ZH文件夾下去遍歷即可

自定義函數

UDF


 

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


 

import org.apache.spark.SparkConf

import org.apache.spark.SparkContext

import org.apache.spark.sql.SQLContext

import org.apache.spark.sql.Row

import org.apache.spark.sql.types.StructType

import org.apache.spark.sql.types.StructField

import org.apache.spark.sql.types.StringType

object UDF {

def main(args: Array[String]): Unit = {

val conf = new SparkConf()

.setMaster("local")

.setAppName("UDF")

val sc = new SparkContext(conf)

val sqlContext = new SQLContext(sc)

val names = Array("yarn", "Marry", "Jack", "Tom")

val namesRDD = sc.parallelize(names, 4)

val namesRowRDD = namesRDD.map { name => Row(name) }

val structType = StructType(Array(StructField("name", StringType, true)))

val namesDF = sqlContext.createDataFrame(namesRowRDD, structType)

// 註冊一張names表

namesDF.registerTempTable("names")

sqlContext.udf.register("strLen", (str: String) => str.length())

// 使用自定義函數

sqlContext.sql("select name,strLen(name) from names").show

}

}

UDAF:實現對某個字段進行count


 

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


 

import org.apache.spark.sql.expressions.UserDefinedAggregateFunction

import org.apache.spark.sql.types.StructType

import org.apache.spark.sql.types.DataType

import org.apache.spark.sql.expressions.MutableAggregationBuffer

import org.apache.spark.sql.Row

import org.apache.spark.sql.types.StructField

import org.apache.spark.sql.types.StringType

import org.apache.spark.sql.types.IntegerType

class StringCount extends UserDefinedAggregateFunction {

//輸入數據的類型

def inputSchema: StructType = {

StructType(Array(StructField("12321", StringType, true)))

}

// 聚合操作時,所處理的數據的類型

def bufferSchema: StructType = {

StructType(Array(StructField("count", IntegerType, true)))

}

def deterministic: Boolean = {

true

}

// 爲每個分組的數據執行初始化值

def initialize(buffer: MutableAggregationBuffer): Unit = {

buffer(0) = 0

}

/**

* update可以認爲是,一個一個地將組內的字段值傳遞進來實現拼接的邏輯

* buffer.getInt(0)獲取的是上一次聚合後的值

* 相當於map端的combiner,combiner就是對每一個map task的處理結果進行一次小聚合

* 大聚和發生在reduce端

*/

//每個組,有新的值進來的時候,進行分組對應的聚合值的計算

def update(buffer: MutableAggregationBuffer, input: Row): Unit = {

buffer(0) = buffer.getAs[Int](0) + 1

}

/**

* 合併 update操作,可能是針對一個分組內的部分數據,在某個節點上發生的

* 但是可能一個分組內的數據,會分佈在多個節點上處理

* 此時就要用merge操作,將各個節點上分佈式拼接好的串,合併起來

* buffer1.getInt(0) : 大聚和的時候上一次聚合後的值      

* buffer2.getInt(0) : 這次計算傳入進來的update的結果

*/

// 最後merger的時候,在各個節點上的聚合值,要進行merge,也就是合併

def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {

buffer1(0) = buffer1.getAs[Int](0) + buffer2.getAs[Int](0)

}

// 最終函數返回值的類型

def dataType: DataType = {

IntegerType

}

// 最後返回一個最終的聚合值 要和dataType的類型一一對應

def evaluate(buffer: Row): Any = {

buffer.getAs[Int](0)

}

}

開窗函數

row_number()開窗函數的作用:
按照我們每一個分組的數據,按其照順序,打上一個分組內的行號
id=2016 [111,112,113]
那麼對這個分組的每一行使用row_number()開窗函數後,三行數據會一次得到一個組內的行號
id=2016 [111 1,112 2,113 3]

SparkStreaming

Strom VS SparkStreaming

  1. Storm是一個純實時的流式處理框,SparkStreaming是一個準實時的流式處理框架,(微批處理:可以設置時間間隔)
  2. SparkStreaming的吞吐量比Storm高(因爲它是微批處理)
  3. Storm的事務機制要比SparkStreaming好(每個數據只處理一次)
  4. Storm支持動態資源調度( 可以在數據的低谷期使用更少的資源,而spark的粗粒度資源調度默認是不支持這樣的)
  5. SparkStreaming的應用程序中可以寫SQL語句來處理數據,可以和spark core及sparkSQL無縫結合,所以SparkingStreaming擅長複雜的業務處理,而Storm不擅長複雜的業務處理,它擅長簡單的彙總型計算(天貓雙十一銷量)

SparkStreaming執行流程

總結:
receiver task是7*24h一直在執行,一直接收數據,將接收到的數據保存到batch中,假設batch interval爲5s,那麼把接收到的數據每隔5s切割到一個batch,因爲batch是沒有分佈式計算的特性的,而RDD有,所以把batch封裝到RDD中,又把RDD封裝到DStream中進行計算,在第5s的時候,計算前5s的數據,假設計算5s的數據只需要3s,那麼第5-8s一邊計算任務,一邊接收數據,第9-11s只是接收數據,然後在第10s的時候,循環上面的操作。

如果job執行時間大於batch interval,那麼未執行的數據會越攢越多,最終導致Spark集羣崩潰。
測試:
1.開啓scoket server
[root@node01 ~]# nc -lk 9999
2.啓動spark集羣


 

1

2


 

[root@node01 ~]# /opt/chant/spark-1.6.0/sbin /start-all.sh

[root@node02 ~]# /opt/chant/spark-1.6.0/sbin /start-master.sh

3.運行測試程序


 

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


 

import org.apache.spark.SparkConf

import org.apache.spark.streaming.StreamingContext

import org.apache.spark.streaming.Durations

import org.apache.spark.storage.StorageLevel

/**

 * 1.local的模擬線程數必須大於等於2,因爲一條線程被receiver(接受數據的線程)佔用,另外一個線程是job執行

 * 2.Durations時間的設置,就是我們能接受的延遲度,這個我們需要根據集羣的資源情況以及監控,要考慮每一個job的執行時間

 * 3.創建StreamingContext有兩種方式 (sparkconf、sparkcontext)

 * 4.業務邏輯完成後,需要有一個output operator

 * 5.StreamingContext.start(),straming框架啓動之後是不能在次添加業務邏輯

 * 6.StreamingContext.stop()無參的stop方法會將sparkContext一同關閉,如果只想關閉StreamingContext,在stop()方法內傳入參數false

 * 7.StreamingContext.stop()停止之後是不能在調用start  

 */

object WordCountOnline {

def main(args: Array[String]): Unit = {

val sparkConf = new SparkConf()

sparkConf.setMaster("local[2]")

sparkConf.setAppName("WordCountOnline")

//在創建streaminContext的時候設置batch Interval

val ssc = new StreamingContext(sparkConf,Durations.seconds(5))

val linesDStream = ssc.socketTextStream("node01", 9999, StorageLevel.MEMORY_AND_DISK)

// val wordsDStream = linesDStream.flatMap { _.split(" ") }

val wordsDStream = linesDStream.flatMap(_.split(" "))

val pairDStream = wordsDStream.map { (_,1) }

val resultDStream = pairDStream.reduceByKey(_+_)

resultDStream.print()

 //outputoperator類的算子 

ssc.start()

ssc.awaitTermination()

ssc.stop()

}

}

結果:
在server 端輸入數據,例如,hello world,控制檯實時打印wordwount結果:(hello,1)(world,1)

Output Operations on DStreams

foreachRDD(func)


 

1

2

3

4

5

6

7

8

9


 

dstream.foreachRDD { rdd =>

rdd.foreachPartition { partitionOfRecords =>

// ConnectionPool is a static, lazily initialized pool of connections

val connection = ConnectionPool.getConnection()

partitionOfRecords.foreach(record => connection.send(record))

ConnectionPool.returnConnection(connection)

// return to the pool for future reuse

}

}

saveAsTextFiles(prefix, [suffix])

Save this DStream’s contents as text files.

saveAsObjectFiles(prefix, [suffix])

Save this DStream’s contents as SequenceFiles of serialized Java objects.

saveAsHadoopFiles(prefix, [suffix])

Save this DStream’s contents as Hadoop files.

Transformations on Dstreams

transform(func)

Return a new DStream by applying a RDD-to-RDD function to every RDD of the source DStream. This can be used to do arbitrary RDD operations on the DStream.


 

1

2

3

4

5

6

7


 

val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...)

// RDD containing spam information

val cleanedDStream = wordCounts.transform(rdd => {

rdd.join(spamInfoRDD).filter(...)

// join data stream with spam information to do data cleaning

...

})

updateStateByKey(func)

Return a new “state” DStream where the state for each key is updated by applying the given function on the previous state of the key and the new values for the key. This can be used to maintain arbitrary state data for each key.
UpdateStateByKey的主要功能:
1.Spark Streaming中爲每一個Key維護一份state狀態,state類型可以是任意類型的,可以是一個自定義的對象,那麼更新函數也可以是自定義的。
2.通過更新函數對該key的狀態不斷更新,對於每個新的batch而言,Spark Streaming會在使用updateStateByKey的時候爲已經存在的key進行state的狀態更新


 

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


 

import org.apache.spark.SparkConf

import org.apache.spark.streaming.{Durations, StreamingContext}

import spire.std.option

/** wordcount,實時計算結果,與之前的worCountOnline不同,它是增量計算的。

*

* UpdateStateByKey的主要功能:

* 1、Spark Streaming中爲每一個Key維護一份state狀態,state類型可以是任意類型的的, 可以是一個自定義的對象,那麼更新函數也可以是自定義的。

* 2、通過更新函數對該key的狀態不斷更新,對於每個新的batch而言,Spark Streaming會在使用updateStateByKey的時候爲已經存在的key進行state的狀態更新

* 第6s的計算結果1-5s

* hello,3

* world,2

*

* 第11s的時候 6-11秒

* 接收數據

* hello 1

* hello 1

* world 1

*計算邏輯

* hello 3+1+1

* world 2+1

*

* 第16s 11-15s

* 接收數據

* hello 1

* hello 1

* world 1

* 計算邏輯

* hello 5+1+1

* world 3+1

* 如果要不斷的更新每個key的state,就一定涉及到了狀態的保存和容錯,這個時候就需要開啓checkpoint機制和功能

*

* 全面的廣告點擊分析

*

* 有何用? 統計廣告點擊流量,統計這一天的車流量,統計。。。。點擊量

*/

object UpdateStateByKeyOperator {

def main(args: Array[String]): Unit = {

val conf = new SparkConf().setMaster("local[2]").setAppName("UpdateStateByKeyOperator")

val streamContext = new StreamingContext(conf, Durations.seconds(5))

/**

* 上一次的計算結果會保存兩份:

* 1.內存

* 2.我們設置的checkPoint目錄下

* 因爲內存不穩定,放在checkPoint目錄下更安全。

* 多久會將內存中的數據(每一個key所對應的狀態)寫入到磁盤上一份呢?

* 如果你的batch interval小於10s 那麼10s會將內存中的數據寫入到磁盤一份

* 如果bacth interval 大於10s,那麼就以bacth interval爲準

*/

streamContext.checkpoint("hdfs://node01:8020/sscheckpoint01")

val lines = streamContext.socketTextStream("node01",8888)

val wordsPair = lines.flatMap(_.split(" ")).map((_,1))

// val wordsPair = streamContext.socketTextStream("node01",8888).flatMap(_.split(" ")).map((_,1))

//注意這裏的各種泛型

val counts = wordsPair.updateStateByKey[Int]((values:Seq[Int], state:Option[Int]) =>{

var updateValue = 0

if(!state.isEmpty) updateValue = state.get

values.foreach(x =>{updateValue += x})

// Option.apply[Int](updateValue)

Some(updateValue)

})

counts.print()

streamContext.start()

streamContext.awaitTermination()

streamContext.stop()

}

}

Window Operations

總結:
batch interval:5s
每隔5s切割一次batch封裝成DStream
window length:15s
進行計算的DStream中包含15s的數據,這裏也就是3個batch。
sliding interval:10s
每隔10s取3個batch封裝的DStream,封裝成一個更大的DStream進行計算
window length和sliding interval必須是batch interval的整數倍
問題:
time3的RDD被計算兩次


 

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


 

import org.apache.spark.SparkConf

import org.apache.spark.streaming.{Durations, StreamingContext}

object WindowOperator {

def main(args: Array[String]): Unit = {

val conf = new SparkConf().setAppName("WindowOperator").setMaster("local[2]")

val streamingContext = new StreamingContext(conf, Durations.seconds(5))

//優化版必須設置cehckpoint目錄,因爲內存不穩定,保證數據不丟失。

streamingContext.checkpoint("hdfs://node01:8020/sscheckpoint02")

val logDStrem = streamingContext.socketTextStream("node01", 8888)

val wordsPair = logDStrem.flatMap(_.split(" ")).map((_, 1))

/** batch interval:5s

* sliding interval:10s

* window length:60s

* 所以每隔10s會取12個rdd,在計算的時候會將這12個rdd聚合起來

* 然後一起執行reduceByKeyAndWindow操作

* reduceByKeyAndWindow是針對窗口操作的而不是針對DStream操作的

*/

// val wordCountDStrem = wordsPair.reduceByKeyAndWindow((a, b) => a + b, Durations.seconds(60))

//爲什麼這裏一定要聲明參數類型???什麼時候需要聲明,什麼時候不用,什麼時候必須不聲明?

// val wordCountDStrem = wordsPair.reduceByKeyAndWindow((a: Int, b: Int) => a+b, Durations.seconds(60), Durations.seconds(10))

//優化版

val wordCountDStrem = wordsPair.reduceByKeyAndWindow((a, b) => a+b, (a, b) => a-b, Durations.minutes(1), Durations.seconds(10))

wordCountDStrem.print()

streamingContext.start()

streamingContext.awaitTermination()

streamingContext.stop()

}

}

優化:

假設batch=1s,window length=5s,sliding interval=1s,那麼每個DStream重複計算了5次,優化後,(t+4)時刻的Window由(t+3)時刻的Window和(t+4)時刻的DStream組成,由於(t+3)時刻的Window包含(t-1)時刻的DStream,而(t+4)時刻的Window中不需要包含(t-1)時刻的DStream,所以還需要減去(t-1)時刻的DStream,所以:
Window(t+4) = Window(t+3) + DStream(t+4) - DStream(t-1)

優化後的代碼:


 

1

2


 

//優化版必須要設置checkPoint目錄

val wordCountDStrem = wordsPair.reduceByKeyAndWindow((a, b) => a+b, (a, b) => a-b, Durations.minutes(1), Durations.seconds(10))

NOTE: updateStateByKey和優化版的reduceByKeyAndWindow都必須要設置checkPoint目錄。

Driver HA

提交任務時設置
spark-submit –supervise
Spark standalone or Mesos with cluster deploy mode only:
–supervise If given, restarts the driver on failure.
以集羣方式提交到yarn上時,Driver掛掉會自動重啓,不需要任何設置
提交任務,在客戶端啓動Driver,那麼不管是提交到standalone還是yarn,Driver掛掉後都無法重啓
代碼中配置
上面的方式重新啓動的Driver需要重新讀取application的信息然後進行任務調度,實際需求是,新啓動的Driver可以直接恢復到上一個Driver的狀態(可以直接讀取上一個StreamingContext的DSstream操作邏輯和job執行進度,所以需要把上一個StreamingContext的元數據保存到HDFS上),直接進行任務調度,這就需要在代碼層面進行配置。


 

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


 

import org.apache.spark.SparkConf

import org.apache.spark.streaming.{Durations, StreamingContext}

/** Spark standalone or Mesos 1with cluster deploy mode only:

* 在提交application的時候 添加 --supervise 選項 如果Driver掛掉 會自動啓動一個Driver

* SparkStreaming

*/

object SparkStreamingOnHDFS2 {

def main(args: Array[String]): Unit = {

val checkpointPath = "hdfs://node01:8020/sscheckpoint03"

// val ssc = new StreamingContext(conf, Durations.seconds(5))

val ssc = StreamingContext.getOrCreate(checkpointPath,() => {

println("Creating new context")

//這裏可以設置一個線程,因爲不需要一個專門接收數據的線程,而是監控一個目錄

val conf = new SparkConf().setAppName("SparkStreamingOnHDFS").setMaster("local[1]")

//每隔15s查看一下監控的目錄中是否新增了文件

val ssc = new StreamingContext(conf, Durations.seconds(15))

ssc.checkpoint(checkpointPath)

/** 只是監控文件夾下新增的文件,減少的文件是監控不到的 文件內容有改動也是監控不到 */

ssc

})

val wordCount = ssc.textFileStream("hdfs://node01:8020/hdfs/").flatMap(_.split(" ")).map((_, 1)).reduceByKey(_+_)

wordCount.print()

ssc.start()

ssc.awaitTermination()

ssc.stop()

}

}

執行一次程序後,JavaStreamingContext會在checkpointDirectory中保存,當修改了業務邏輯後,再次運行程序,JavaStreamingContext.getOrCreate(checkpointDirectory, factory);
因爲checkpointDirectory中有這個application的JavaStreamingContext,所以不會調用JavaStreamingContextFactory來創建JavaStreamingContext,而是直接checkpointDirectory中的JavaStreamingContext,所以即使業務邏輯改變了,執行的效果也是之前的業務邏輯,如果需要執行修改過的業務邏輯,可以修改或刪除checkpointDirectory。

Kafka

簡介
kafka是一個高吞吐的分部式消息系統

使用kafka和SparkStreaming組合的好處:

  1. 解耦,SparkStreaming不用關心數據源是什麼,只需要消費數據即可
  2. 緩衝,數據提交給kafka消息隊列,SparkStreaming按固定時間和順序處理數據,減輕數據量過大造成的負載
  3. 異步通信,kafka把請求隊列提交給服務端,服務端可以把響應消息也提交給kafka的消息隊列中,互不影響

消息系統特點

  1. 生產者消費者模式
  2. 可靠性

    自己不丟數據

    當消費者消費一條數據後,這條數據還會保存在kafka中,在一週(默認)後再刪除

    消費者不丟數據 

    消費者消費數據的策略,“至少一次”,即消費者至少處理這條數據一次,“嚴格一次”,即消費者必須且只能消費這條數據一次

Kafka架構

圖裏還少了個zookeeper,用於存儲元數據和消費偏移量(根據偏移量來保證消費至少一次和嚴格消費一次)
producer:消息生產者
consumer:消息消費者
broker:kafka集羣的每一臺節點叫做broker,負責處理消息讀、寫請求,存儲消息
topic:消息隊列/分類
kafka裏面的消息是有topic來組織的,簡單的我們可以想象爲一個隊列,一個隊列就是一個topic,然後它把每個topic又分爲很多個partition,這個是爲了做並行的,在每個partition裏面是有序的,相當於有序的隊列,其中每個消息都有個序號,比如0到12,從前面讀往後面寫。一個partition對應一個broker,一個broker可以管多個partition,比如說,topic有6個partition,有兩個broker,那每個broker就管理3個partition。這個partition可以很簡單想象爲一個文件,當數據發過來的時候它就往這個partition上面append,追加就行,kafka和很多消息系統不一樣,很多消息系統是消費完了我就把它刪掉,而kafka是根據時間策略刪除,而不是消費完就刪除,在kafka裏面沒有消費完這個概念,只有過期這個概念

一個topic分成多個partition,每個partition內部消息強有序,其中的每個消息都有一個序號叫offset,一個partition只對應一個broker,一個broker可以管多個partition,消息直接寫入文件,並不是存儲在內存中,根據時間策略(默認一週)刪除,而不是消費完就刪除,producer自己決定往哪個partition寫消息,可以是輪詢的負載均衡,或者是基於hash的partition策略,而這樣容易造成數據傾斜。所以建議使用輪詢的負載均衡

consumer自己維護消費到哪個offset,每個consumer都有對應的group,group內部是queue消費模型,各個consumer消費不同的partition,一個消息在group內只消費一次,各個group各自獨立消費,互不影響
partition內部是FIFO的,partition之間不是FIFO的,當然我們可以把topic設爲一個partition,這樣就是嚴格的FIFO(First Input First Output,先入先出隊列)

kafka特點

  • 高性能:單節點支持上千個客戶端,百MB/s吞吐
  • 持久性:消息直接持久化在普通磁盤上,性能好,直接寫到磁盤裏面去,就是直接append到磁盤裏面去,這樣的好處是直接持久話,數據不會丟,第二個好處是順序寫,然後消費數據也是順序的讀,所以持久化的同時還能保證順序讀寫
  • 分佈式:數據副本冗餘、流量負載均衡、可擴展

    分佈式,數據副本,也就是同一份數據可以到不同的broker上面去,也就是當一份數據,磁盤壞掉的時候,數據不會丟失,比如3個副本,就是在3個機器磁盤都壞掉的情況下數據纔會丟。
  • 靈活性:消息長時間持久化+Client維護消費狀態

    消費方式非常靈活,第一原因是消息持久化時間跨度比較長,一天或者一星期等,第二消費狀態自己維護消費到哪個地方了,可以自定義消費偏移量

kafka與其他消息隊列對比

  • RabbitMQ:分佈式,支持多種MQ協議,重量級
  • ActiveMQ:與RabbitMQ類似
  • ZeroMQ:以庫的形式提供,使用複雜,無持久化
  • redis:單機、純內存性好,持久化較差

    本身是一個內存的KV系統,但是它也有隊列的一些數據結構,能夠實現一些消息隊列的功能,當然它在單機純內存的情況下,性能會比較好,持久化做的稍差,當持久化的時候性能下降的會比較厲害
  • kafka:分佈式,較長時間持久化,高性能,輕量靈活

    天生是分佈式的,不需要你在上層做分佈式的工作,另外有較長時間持久化,在長時間持久化下性能還比較高,順序讀和順序寫,還通過sendFile這樣0拷貝的技術直接從文件拷貝到網絡,減少內存的拷貝,還有批量讀批量寫來提高網絡讀取文件的性能

零拷貝


“零拷貝”是指計算機操作的過程中,CPU不需要爲數據在內存之間的拷貝消耗資源。而它通常是指計算機在網絡上發送文件時,不需要將文件內容拷貝到用戶空間(User Space)而直接在內核空間(Kernel Space)中傳輸到網絡的方式。

Kafka集羣搭建

node01,node02,node03

1. 解壓

[root@node01 chant]# tar zxvf kafka_2.10-0.8.2.2.tgz

2. 修改server.properties配置文件


 

1

2

3

4

5

6


 

[root@node01 chant]# cd kafka_2.10-0.8.2.2/config

[root@node01 config]# vi server.properties

broker.id=0 #node01爲0,node02爲1,node03爲2

log.dirs=/var/kafka/logs #真實數據存儲路徑

auto.leader.rebalance.enable=true #leader均衡機制開啓

zookeeper.connect=node02:2181,node03:2181,node04:2181 #zookeeper集羣

3. 同步配置,記得修改每臺機器的broker.id

4. 啓動zookeeper集羣

5. 在每臺kafka節點上啓動kafka集羣


 

1


 

nohup /opt/chant/kafka_2.10-0.8.2.2/bin/kafka-server-start.sh /opt/chant/kafka_2.10-0.8.2.2/config/server.properties &

6. 測試

在node01上
創建topic:


 

1

2

3

4

5

6


 

/opt/chant/kafka_2.10-0.8.2.2/bin/kafka-topics.sh

--create

--zookeeper node02:2181,node03:2181,node04:2181

--replication-factor 3

--partitions 3

--topic test_create_topic

生產數據:


 

1

2

3


 

/opt/chant/kafka_2.10-0.8.2.2/bin/kafka-console-producer.sh

--broker-list node01:9092,node02:9092,node03:9092

--topic test_create_topic

在node02上啓動消費者


 

1

2

3

4


 

/opt/chant/kafka_2.10-0.8.2.2/bin/kafka-console-consumer.sh

--zookeeper node02:2181,node03:2181,node04:2181

--from-beginning

--topic test_create_topic

在node01輸入消息,在node02會接收並打印

查看在集羣中有哪些topic:


 

1

2

3


 

/opt/chant/kafka_2.10-0.8.2.2/bin/kafka-topics.sh

--list

--zookeeper node02:2181,node03:2181,node04:2181

結果:test_create_topic

查看某個topic信息:


 

1

2

3

4

5

6

7

8

9


 

/opt/chant/kafka_2.10-0.8.2.2/bin/kafka-topics.sh

--describe

--zookeeper node02:2181,node03:2181,node04:2181 --topic test_create_topic

結果:

Topic:test_create_topic PartitionCount:3 ReplicationFactor:3 Configs:

Topic: test_create_topic Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2

Topic: test_create_topic Partition: 1 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0

Topic: test_create_topic Partition: 2 Leader: 2 Replicas: 2,0,1 Isr: 2,0,1

解釋一下leader均衡機制(auto.leader.rebalance.enable=true):
每個partition是有主備結構的,當partition 1的leader,就是broker.id = 1的節點掛掉後,那麼leader 0 或leader 2成爲partition 1 的leader,那麼leader 0 或leader 2 會管理兩個partition的讀寫,性能會下降,當leader 1 重新啓動後,如果開啓了leader均衡機制,那麼leader 1會重新成爲partition 1 的leader,降低leader 0 或leader 2 的負載

Kafka和SparkStreaming整合

Receiver方式

原理:

SparkStreaming和Kafka整合原理

獲取kafka傳遞的數據來計算:


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24


 

SparkConf conf = new SparkConf()

    .setAppName("SparkStreamingOnKafkaReceiver")

    .setMaster("local[2]")

    .set("spark.streaming.receiver.writeAheadLog.enable","true");      

JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(5));

//設置持久化數據的目錄

jsc.checkpoint("hdfs://node01:8020/spark/checkpoint");

Map<String, Integer> topicConsumerConcurrency = new HashMap<String, Integer>();

//topic名    receiver task數量

topicConsumerConcurrency.put("test_create_topic", 1);

JavaPairReceiverInputDStream<String,String> lines = KafkaUtils.createStream(

    jsc,

    "node02:2181,node03:2181,node04:2181",

    "MyFirstConsumerGroup",

    topicConsumerConcurrency,

    StorageLevel.MEMORY_AND_DISK_SER());

/*

 * 第一個參數是StreamingContext

 * 第二個參數是ZooKeeper集羣信息(接受Kafka數據的時候會從Zookeeper中獲得Offset等元數據信息)

 * 第三個參數是Consumer Group

 * 第四個參數是消費的Topic以及併發讀取Topic中Partition的線程數

 * 第五個參數是持久化數據的級別,可以自定義

 */

//對lines進行其他操作……

注意

  1. 需要spark-examples-1.6.0-hadoop2.6.0.jar
  2. kafka_2.10-0.8.2.2.jar和kafka-clients-0.8.2.2.jar版本號要一致

kafka客戶端生產數據的代碼:


 

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


 

public class SparkStreamingDataManuallyProducerForKafka extends Thread {

    private String topic; //發送給Kafka的數據的類別

    private Producer<Integer, String> producerForKafka;

    public SparkStreamingDataManuallyProducerForKafka(String topic){

        this.topic = topic;

        Properties conf = new Properties();

        conf.put("metadata.broker.list",

"node01:9092,node02:9092,node03:9092");

        conf.put("serializer.class",  StringEncoder.class.getName());

        producerForKafka = new Producer<Integer, String>(

            new ProducerConfig(conf)) ;

     }

    @Override

    public void run() {

        while(true){

             counter ++;

             String userLog = createUserLog();

//生產數據這個方法可以根據實際需求自己編寫

             producerForKafka.send(new KeyedMessage<Integer, String>(topic, userLog));

             try {

                 Thread.sleep(1000);

             } catch (InterruptedException e) {

                 e.printStackTrace();

             }

        }

  }

    public static void main(String[] args) {

new SparkStreamingDataManuallyProducerForKafka(

"test_create_topic").start();

//test_create_topic是topic名

    }

}

Direct方式

把kafka當作一個存儲系統,直接從kafka中讀數據,SparkStreaming自己維護消費者的消費偏移量,不再將其存儲到zookeeper。
與Receiver方式相比,他有的優點是:

  1. one-to-one,直接讀取卡夫卡中數據,Partion數與kafka一致。
  2. Efficiency,不開啓WAL也不會丟失數據,因爲事實上kafka的的數據保留時間只要設定合適,可以直接從kafka恢復。
  3. Exactly-once semantics,其實這隻保證kafka與spark是一致的,之後寫數據的時候還需要自己注意才能保證整個事務的一致性。而在receiver模式下,offset交由kafka管理,而kafaka實際交由zookeeper管理,可能出現數據不一致。
    
     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    
     

    SparkConf conf = new SparkConf()

             .setAppName("SparkStreamingOnKafkaDirected")

             .setMaster("local[1]");

             

    JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(10));

             

    Map<String, String> kafkaParameters = new HashMap<String, String>();

    kafkaParameters.put("metadata.broker.list", "node01:9092,node02:9092,node03:9092");

             

    HashSet<String> topics = new HashSet<String>();

    topics.add("test_create_topic");

    JavaPairInputDStream<String,String> lines = KafkaUtils.createDirectStream(jsc,

            String.class,

            String.class,

            StringDecoder.class,

            StringDecoder.class,

            kafkaParameters,

            topics);

    //對lines進行其他操作……

兩種方式下提高SparkStreaming並行度的方法

Receiver方式調整SparkStreaming的並行度的方法:

  • spark.streaming.blockInterval

    假設batch interval爲5s,Receiver Task會每隔200ms(spark.streaming.blockInterval默認)將接收來的數據封裝到一個block中,那麼每個batch中包括25個block,batch會被封裝到RDD中,所以RDD中會包含25個partition,所以提高接收數據時的並行度的方法是:調低spark.streaming.blockInterval的值,建議不低於50ms
    其他配置:
  • spark.streaming.backpressure.enabled 

    默認false,設置爲true後,sparkstreaming會根據上一個batch的接收數據的情況來動態的調整本次接收數據的速度,但是最大速度不能超過spark.streaming.receiver.maxRate設置的值(設置爲n,那麼速率不能超過n/s)
  • spark.streaming.receiver.writeAheadLog.enable 默認false 是否開啓WAL機制

Direct方式並行度的設置:

第一個DStream的分區數是由讀取的topic的分區數決定的,可以通過增加topic的partition數來提高SparkStreaming的並行度

參考資料

  1. Spark Shuffle原理、Shuffle操作問題解決和參數調優
  2. Spark性能優化指南——高級篇
  3. Spark性能優化指南——基礎篇
  4. Unified Memory Management in Spark 1.6
  5. Garbage Collection Tuning
  6. spark2.1內存管理機制
  7. Spark內存管理詳解(下)——內存管理
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章