前言
spark系列教程
spark-core–RDD入門實戰(詳解各大api,基於IDEA開發)
目錄:
- search類:
class search(query:String) {
def isMatch(s:String): Boolean ={
s.contains(query);
}
def Match(s:RDD[String]):RDD[String]={
s.filter(x=>isMatch(x));
}
}
- 主函數:
var conf=new SparkConf().setAppName("WordCount").setMaster("local");
var sc=new SparkContext(conf);
sc.setLogLevel("ERROR")
val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive", "atguigu"))
val s=new search("had");
val r=s.Match(rdd);
r.collect().foreach(println);
這個時候運行一下報錯:
- 問題說明
在這個方法中所調用的方法isMatch()是定義在Search這個類中的,實際上調用的是this. isMatch(),this表示Search這個類的對象,所以我們需要將search對象傳遞到Executor,需要先將search對象序列化 - 解決方案
使類繼承scala.Serializable即可。
class Search() extends Serializable{…}
RDD依賴關係
1 Lineage
RDD只支持粗粒度轉換,即在大量記錄上執行的單個操作。將創建RDD的一系列Lineage(血統)記錄下來,以便恢復丟失的分區。RDD的Lineage會記錄RDD的元數據信息和轉換行爲,當該RDD的部分分區數據丟失時,它可以根據這些信息來重新運算和恢復丟失的數據分區。
現在以讀取文件爲例:
val words=sc.textFile("in/word.txt").flatMap(_.split("\t")).map((_,1))
val count=words.reduceByKey(_+_);
查看words的Lineage
println(words.toDebugString);
查看“count”的Lineage
println(count.toDebugString);
結果:
(1) MapPartitionsRDD[3] at map at test.scala:17 []
| MapPartitionsRDD[2] at flatMap at test.scala:17 []
| in/word.txt MapPartitionsRDD[1] at textFile at test.scala:17 []
| in/word.txt HadoopRDD[0] at textFile at test.scala:17 []
(1) ShuffledRDD[4] at reduceByKey at test.scala:19 []
+-(1) MapPartitionsRDD[3] at map at test.scala:17 []
| MapPartitionsRDD[2] at flatMap at test.scala:17 []
| in/word.txt MapPartitionsRDD[1] at textFile at test.scala:17 []
| in/word.txt HadoopRDD[0] at textFile at test.scala:17 []
會發現有層層的依賴關係,每個操作記錄都被記錄了下來,以便於數據的恢復
注意:RDD和它依賴的父RDD(s)的關係有兩種不同的類型,即窄依賴(narrow dependency)和寬依賴(wide dependency)。
窄依賴
窄依賴指的是每一個父RDD的Partition最多被子RDD的一個Partition使用,窄依賴我們形象的比喻爲獨生子女
寬依賴
寬依賴指的是多個子RDD的Partition會依賴同一個父RDD的Partition,會引起shuffle,總結:寬依賴我們形象的比喻爲超生
DAG
DAG(Directed Acyclic Graph)叫做有向無環圖,原始的RDD通過一系列的轉換就就形成了DAG,根據RDD之間的依賴關係的不同將DAG劃分成不同的Stage,對於窄依賴,partition的轉換處理在Stage中完成計算。對於寬依賴,由於有Shuffle的存在,只能在parent RDD處理完成後,才能開始接下來的計算,因此寬依賴是劃分Stage的依據。
5 任務劃分
RDD任務切分中間分爲:Application、Job、Stage和Task
1)Application:初始化一個SparkContext即生成一個Application
2)Job:一個Action算子就會生成一個Job
3)Stage:根據RDD之間的依賴關係的不同將Job劃分成不同的Stage,遇到一個寬依賴則劃分一個Stage。
- Task:Stage是一個TaskSet,將Stage劃分的結果發送到不同的Executor執行即爲一個Task。
注意:Application->Job->Stage-> Task每一層都是1對n的關係。
RDD緩存
-
RDD通過persist方法或cache方法可以將前面的計算結果緩存,默認情況下 persist() 會把數據以序列化的形式緩存在 JVM 的堆空間中。
-
但是並不是這兩個方法被調用時立即緩存,而是觸發後面的action時,該RDD將會被緩存在計算節點的內存中,並供後面重用。
-
通過查看源碼發現cache最終也是調用了persist方法,默認的存儲級別都是僅在內存存儲一份,Spark的存儲級別還有好多種,存儲級別在object StorageLevel中定義的。
-
緩存有可能丟失,或者存儲存儲於內存的數據由於內存不足而被刪除,RDD的緩存容錯機制保證了即使緩存丟失也能保證計算的正確執行。通過基於RDD的一系列轉換,丟失的數據會被重算,由於RDD的各個Partition是相對獨立的,因此只需要計算丟失的部分即可,並不需要重算全部Partition。
val rdd=sc.makeRDD(Array("test"));
將rdd轉化爲攜帶時間戳且不緩存
var noCache=rdd.map(_.toString+System.currentTimeMillis());
多次打印
noCache.collect().foreach(println);
noCache.collect().foreach(println);
noCache.collect().foreach(println);
帶緩存
var haveCache=rdd.map(_.toString+System.currentTimeMillis()).cache();
haveCache.collect().foreach(println)
haveCache.collect().foreach(println)
haveCache.collect().foreach(println)
-----------
test1589891383596
test1589891383718
test1589891383751
test1589891383829
test1589891383829
test1589891383829
鍵值對RDD數據分區器
Spark目前支持Hash分區和Range分區,用戶也可以自定義分區,Hash分區爲當前的默認分區,Spark中分區器直接決定了RDD中分區的個數、RDD中每條數據經過Shuffle過程屬於哪個分區和Reduce的個數
注意:
- 只有Key-Value類型的RDD纔有分區器的,非Key-Value類型的RDD分區器的值是None
- 每個RDD的分區ID範圍:0~numPartitions-1,決定這個值是屬於那個分區的。
Hash分區
HashPartitioner分區的原理:對於給定的key,計算其hashCode,併除以分區的個數取餘,如果餘數小於0,則用餘數+分區的個數(否則加0),最後返回的值就是這個key所屬的分區ID。相關源碼如下:
def getPartition(key: Any): Int = key match {
case null => 0
case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
}
def nonNegativeMod(x: Int, mod: Int): Int = {
val rawMod = x % mod
rawMod + (if (rawMod < 0) mod else 0)
}
Ranger分區
HashPartitioner分區弊端:可能導致每個分區中數據量的不均勻,極端情況下會導致某些分區擁有RDD的全部數據。
RangePartitioner作用:將一定範圍內的數映射到某一個分區內,儘量保證每個分區中數據量的均勻,而且分區與分區之間是有序的,一個分區中的元素肯定都是比另一個分區內的元素小或者大,但是分區內的元素是不能保證順序的。簡單的說就是將一定範圍內的數映射到某一個分區內。實現過程爲:
- 第一步:先從整個RDD中抽取出樣本數據,將樣本數據排序,計算出每個分區的最大key值,形成一個Array[KEY]類型的數組變量rangeBounds;
- 第二步:判斷key在rangeBounds中所處的範圍,給出該key值在下一個RDD中的分區id下標;該分區器要求RDD中的KEY類型必須是可以排序的
4 自定義分區
要實現自定義的分區器,你需要繼承 org.apache.spark.Partitioner 類並實現下面三個方法。
(1)numPartitions: Int:返回創建出來的分區數。
(2)getPartition(key: Any): Int:返回給定鍵的分區編號(0到numPartitions-1)。
(3)equals():Java 判斷相等性的標準方法。這個方法的實現非常重要,Spark 需要用這個方法來檢查你的分區器對象是否和其他分區器實例相同,這樣 Spark 纔可以判斷兩個 RDD 的分區方式是否相同。
需求:將相同後綴的數據寫入相同的文件,通過將相同後綴的數據分區到相同的分區並保存輸出來實現。
class myPatitioner(numParts:Int) extends org.apache.spark.Partitioner {
override def numPartitions: Int = numParts
override def getPartition(key: Any): Int = {
val ckey: String = key.toString
ckey.substring(ckey.length-1).toInt%numParts;
}
}
val data = sc.parallelize(Array((1,1),(2,2),(3,3),(4,4),(5,5),(6,6)))
使用自定義分區
val result=data.partitionBy(new myPatitioner(2));
輸出
result.mapPartitionsWithIndex((index,items)=>items.map((index,_))).collect.foreach(println);
---0-結果
(0,(2,2))
(0,(4,4))
(0,(6,6))
(1,(1,1))
(1,(3,3))
(1,(5,5))
數據讀取與保存
Spark的數據讀取及數據保存可以從兩個維度來作區分:文件格式以及文件系統。
- 文件格式分爲:Text文件、Json文件、Csv文件、Sequence文件以及Object文件;
- 文件系統分爲:本地文件系統、HDFS、HBASE以及數據庫。
.1文件類數據讀取與保存
1 Text文件
1)數據讀取:textFile(String)
2)數據保存: saveAsTextFile(String)
2 JSON文件
如果JSON文件中每一行就是一個JSON記錄,那麼可以通過將JSON文件當做文本文件來讀取,然後利用相關的JSON庫對每一條數據進行JSON解析。
注意:每一行都是json格式,不是原有的Json文件格式,如下。:
{“name”: “王小二”, “age”: 25.2, “school”: “福州大學”}
//讀取
val json=sc.textFile("in/test.json");
val result = json.map(JSON.parseFull);
result.collect.foreach(println);
3 Sequence文件
SequenceFile文件是Hadoop用來存儲二進制形式的key-value對而設計的一種平面文件(Flat File)。Spark 有專門用來讀取 SequenceFile 的接口。在 SparkContext 中,可以調用 sequenceFile[ keyClass, valueClass]。
將RDD保存爲Sequence文件
rdd.saveAsSequenceFile("file:///opt/module/spark/seqFile")
讀取Sequence文件
val seq = sc.sequenceFile[Int,Int]("file:///opt/module/spark/seqFile")打印讀取後的Sequence文件
seq.collect.foreach(println)
Array[(Int, Int)] = Array((1,2), (3,4), (5,6))
4 對象文件
對象文件是將對象序列化後保存的文件,採用Java的序列化機制。可以通過objectFile[k,v] 函數接收一個路徑,讀取對象文件,返回對應的 RDD,也可以通過調用saveAsObjectFile() 實現對對象文件的輸出。因爲是序列化所以要指定類型。
文件系統類數據讀取與保存
連接mysql數據庫
讀數據
pom.xml:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
def main(args: Array[String]): Unit = {
//上下文
var conf=new SparkConf().setAppName("WordCount").setMaster("local");
var sc=new SparkContext(conf);
sc.setLogLevel("ERROR")
val getConnection=()=>{
DriverManager.getConnection("jdbc:mysql://localhost:3306/edupfm?serverTimezone=GMT%2B8", "root", "123456")
}
val sql="select id,username from user where id>=? and id<=?"
val rdd=new JdbcRDD(sc,
getConnection,
sql,
1,
20,
//分區數
2,
(rs)=>{
(rs.getInt(1),rs.getString(2));
// println(rs.getInt(1)+"--"+rs.getString(2));
}
)
println(rdd.collect().toBuffer);
}
------
ArrayBuffer((1,小衡), (2,小紅), (3,小章), (13,李四), (14,張三), (19,王二麻), (20,王五))
注,關於如何連接hdfs和es等數據庫,我會另外寫一篇文章來記錄
RDD累加器
- 累加器用來對信息進行聚合,通常在向 Spark傳遞函數時,比如使用 map() 函數或者用 filter() 傳條件時,可以使用驅動器程序中定義的變量,但是集羣中運行的每個任務都會得到這些變量的一份新的副本,更新這些副本的值也不會影響驅動器中的對應變量。如果我們想實現所有分片處理時更新共享變量的功能,那麼累加器可以實現我們想要的效果。
1 系統累加器
針對一個輸入的日誌文件,如果我們想計算文件中所有空行的數量,我們可以編寫以下程序:
val notice = sc.textFile("./NOTICE")
val blanklines = sc.accumulator(0)
val tmp = notice.flatMap(line => {
if (line == "") {
blanklines += 1
}
line.split(" ")
})
tmp.count()
res31: Long = 3213
blanklines.value
res32: Int = 171
累加器的用法如下所示。
- 通過在驅動器中調用SparkContext.accumulator(initialValue)方法,創建出存有初始值的累加器。返回值爲 org.apache.spark.Accumulator[T] 對象,其中 T 是初始值 initialValue 的類型。Spark閉包裏的執行器代碼可以使用累加器的 += 方法(在Java中是 add)增加累加器的值。 驅動器程序可以調用累加器的value屬性(在Java中使用value()或setValue())來訪問累加器的值。
- 注意:工作節點上的任務不能訪問累加器的值。從這些任務的角度來看,累加器是一個只寫變量。
- 對於要在行動操作中使用的累加器,Spark只會把每個任務對各累加器的修改應用一次。因此,如果想要一個無論在失敗還是重複計算時都絕對可靠的累加器,我們必須把它放在 foreach() 這樣的行動操作中。轉化操作中累加器可能會發生不止一次更新
2自定義累加器
AccumulatorV2來提供更加友好的自定義類型累加器的實現方式。實現自定義類型累加器需要繼承AccumulatorV2並至少覆寫下例中出現的方法,下面這個累加器可以用於在程序運行過程中收集一些文本類信息,最終以Set[String]的形式返回。1
在這裏插入代碼片package com.atguigu.spark
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.JavaConversions._
class LogAccumulator extends org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]] {
private val _logArray: java.util.Set[String] = new java.util.HashSet[String]()
override def isZero: Boolean = {
_logArray.isEmpty
}
override def reset(): Unit = {
_logArray.clear()
}
override def add(v: String): Unit = {
_logArray.add(v)
}
override def merge(other: org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]]): Unit = {
other match {
case o: LogAccumulator => _logArray.addAll(o.value)
}
}
override def value: java.util.Set[String] = {
java.util.Collections.unmodifiableSet(_logArray)
}
override def copy():org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]] = {
val newAcc = new LogAccumulator()
_logArray.synchronized{
newAcc._logArray.addAll(_logArray)
}
newAcc
}
}
// 過濾掉帶字母的
object LogAccumulator {
def main(args: Array[String]) {
val conf=new SparkConf().setAppName("LogAccumulator")
val sc=new SparkContext(conf)
val accum = new LogAccumulator
sc.register(accum, "logAccum")
val sum = sc.parallelize(Array("1", "2a", "3", "4b", "5", "6", "7cd", "8", "9"), 2).filter(line => {
val pattern = """^-?(\d+)"""
val flag = line.matches(pattern)
if (!flag) {
accum.add(line)
}
flag
}).map(_.toInt).reduce(_ + _)
println("sum: " + sum)
for (v <- accum.value) print(v + "")
println()
sc.stop()
}
}
廣播變量(調優策略)
廣播變量用來高效分發較大的對象。向所有工作節點發送一個較大的只讀值,以供一個或多個Spark操作使用。比如,如果你的應用需要向所有節點發送一個較大的只讀查詢表,甚至是機器學習算法中的一個很大的特徵向量,廣播變量用起來都很順手。 在多個並行操作中使用同一個變量,但是 Spark會爲每個任務分別發送。
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(35)
scala> broadcastVar.value
res33: Array[Int] = Array(1, 2, 3)
使用廣播變量的過程如下:
- 通過對一個類型 T 的對象調用 SparkContext.broadcast 創建出一個 Broadcast[T] 對象。 任何可序列化的類型都可以這麼實現。
- 通過 value 屬性訪問該對象的值(在 Java 中爲 value() 方法)。
- 變量只會被髮到各個節點一次,應作爲只讀值處理(修改這個值不會影響到別的節點)。
今天先學到這裏,明天學習>>>spark-sql,會有相應的文章更新!