目錄
一、RDD中的函數傳遞(序列化問題)
在實際開發中我們往往需要自己定義一些對於RDD的操作,那麼此時需要主要的是,初始化工作是在Driver端進行的,而實際運行程序是在Executor端進行的,這就涉及到了跨進程通信,是需要序列化的。
1、傳遞一個方法
package com.xin
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
class SearchTwo(query: String) extends java.io.Serializable{
// 必須加extends //Serializable 因爲類初始化在Driver端
// 而實際在executor端需要調用類中的方法
//因此涉及到了網絡通信需要序列化
def isMatch(s:String):Boolean={
s.contains(query)
}
def getMatchRdd(rdd:RDD[String])={
rdd.filter(isMatch) //isMatch相當於x => x.contains(query)
}
}
object serializeable {
def main(args: Array[String]): Unit = {
//1.初始化配置信息及SparkContext
val conf = new SparkConf().setMaster("local[*]").setAppName("serializeable")
val sc = new SparkContext(conf)
//2.創建一個RDD
val rdd:RDD[String] = sc.parallelize(Array("hadoop", "spark","hive"))
//3.創建一個Search對象
val search = new SearchTwo("h")
//4.運用第一個過濾函數並打印結果
search.getMatchRdd(rdd).collect().foreach(println)
}
}
在這個方法中所調用的方法isMatch()是定義在Search這個類中的,實際上調用的是this. isMatch(),this表示Search這個類的對象,程序在運行過程中需要將Search對象序列化以後傳遞到Executor端。
2、傳遞一個屬性
package com.xin
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
//class SearchTwo(query: String) extends java.io.Serializable{
class SearchTwo(query: String) {
def isMatch(s:String):Boolean={
s.contains(query)
}
def getMatchlRdd2(rdd:RDD[String])={
val query_ = this.query
rdd.filter(x => x.contains(query_))
//必須賦值給局部變量,x.contains(this.query)不管用
}
}
object serializeable {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("serializeable")
val sc = new SparkContext(conf)
val rdd:RDD[String] = sc.parallelize(Array("hadoop", "spark","hive"))
val search = new SearchTwo("h")
search.getMatchlRdd2(rdd).collect().foreach(println)
}
}
個人總結:調用類中定義的屬性,需要實例化,因爲類是在Driver中實例化,而調用是在Executer。也可以聲明一個局部變量來傳遞類中的屬性,即可以不實例化
二、RDD依賴關係
1、Lineage(容錯機制)
RDD只支持粗粒度轉換(寫是粗顆粒,批處理,讀是細顆粒,一行一行),即在大量記錄上執行的單個操作。將創建RDD的一系列Lineage(血統)記錄下來,以便恢復丟失的分區。RDD的Lineage會記錄RDD的元數據信息和轉換行爲,當該RDD的部分分區數據丟失時,它可以根據這些信息來重新運算和恢復丟失的數據分區。(chekpoint)
實踐代碼:
object Lineage {
def main(args: Array[String]): Unit = {
val sc = new SparkContext(new SparkConf().setAppName("myspark2").setMaster("local[*]"))
val wordAndOne = sc.textFile("in2/test.txt").flatMap(_.split(",")).map((_,1))
val wordAndCount = wordAndOne.reduceByKey(_+_)
val debugString: String = wordAndOne.toDebugString //查看“wordAndOne”的Lineage
val toDebugString: String = wordAndCount.toDebugString
val dependencies: Seq[Dependency[_]] = wordAndOne.dependencies
val dependencies1: Seq[Dependency[_]] = wordAndCount.dependencies
println("----------------wordAndOne toDebugString------------------")
println(debugString) //HadoopRDD[0] at textFile---MapPartitionsRDD[1] at textFile--flatMap--map
println("----------------wordAndCount toDebugString------------------")
println(toDebugString) //HadoopRDD[0] at textFile---MapPartitionsRDD[1] at textFile--flatMap--map--ShuffledRDD[4] at reduceByKey
println("----------------wordAndOne dependencies------------------")
println(dependencies.toString()) //org.apache.spark.OneToOneDependency@79e66b2f
println("----------------wordAndCount dependencies------------------")
println(dependencies1.toBuffer) //org.apache.spark.ShuffleDependency@17273273
}
}
注意:RDD和它依賴的父RDD(s)的關係有兩種不同的類型,即窄依賴(narrow dependency)和寬依賴(wide dependency)。
2、窄依賴
窄依賴(narrow dependency)指每一個父RDD的Partition最多被子RDD的一個Partition使用,窄依賴形象的比喻爲獨生子女
3、寬依賴
寬依賴(wide dependency)指多個子RDD的Partition會依賴同一個父RDD的Partition,會引起shuffle
總結:寬依賴我們形象的比喻爲超生
總結:寬窄依賴區別在於父RDD的分區被一個還是多個子RDD分區繼承,多個RDD繼承即產生了shuffle
4、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。
4)Task:Stage是一個TaskSet,將Stage劃分的結果發送到不同的Executor執行即爲一個Task。
注意:Application->Job->Stage-> Task每一層都是1對n的關係。
6、RDD緩存
RDD通過persist方法或cache方法可以將前面的計算結果緩存,默認情況下 persist() 會把數據以序列化的形式緩存在 JVM 的堆空間中。 但並不是被調用時立即緩存,而是觸發後面的action時,該RDD將會被緩存在計算節點的內存中,並供後面重用。
緩存有可能丟失,或者存儲存儲於內存的數據由於內存不足而被刪除,RDD的緩存容錯機制(lineage)保證了即使緩存丟失也能保證計算的正確執行。通過基於RDD的一系列轉換,丟失的數據會被重算,由於RDD的各個Partition是相對獨立的,因此只需要計算丟失的部分即可,並不需要重算全部Partition。
val mapRDD: RDD[String] = LineRDD.map(_+System.currentTimeMillis())
val cacheRDD: RDD[String] = LineRDD.map(_+System.currentTimeMillis()).cache() //緩存
for( a <- 1 to 5){
mapRDD.foreach(println) //五次輸出時間戳不同
}
for( a <- 1 to 5){
if(a==1)println("------------------")
cacheRDD.foreach(println) // 五次輸出結果一致
}
7、RDD CheckPoint
檢查點(本質是通過將RDD寫入Disk做檢查點)是爲了通過lineage做容錯的輔助,lineage過長(操作記錄)會造成容錯成本過高,這樣就不如在中間階段做檢查點容錯,如果之後有節點出現問題而丟失分區,從做檢查點的RDD開始重做Lineage,就會減少開銷。檢查點通過將數據寫入到HDFS文件系統實現了RDD的檢查點功能。設置檢查點操作,會創建一個二進制的文件,並存儲到checkpoint目錄中,該目錄是用SparkContext.setCheckpointDir()設置的。在checkpoint的過程中,該RDD的所有依賴於父RDD中的信息將全部被移除。對RDD進行checkpoint操作並不會馬上被執行,必須執行Action操作才能觸發。
案例實操:
(1)設置檢查點
scala> sc.setCheckpointDir("hdfs://hadoop102:9000/checkpoint")
(2)創建一個RDD
scala> val rdd = sc.parallelize(Array("atguigu"))
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[14] at parallelize at <console>:24
(3)將RDD轉換爲攜帶當前時間戳並做checkpoint
scala> val ch = rdd.map(_+System.currentTimeMillis)
ch: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[16] at map at <console>:26
scala> ch.checkpoint
(4)多次打印結果
scala> ch.collect
res55: Array[String] = Array(atguigu1538981860336)
scala> ch.collect
res56: Array[String] = Array(atguigu1538981860504)
scala> ch.collect
res57: Array[String] = Array(atguigu1538981860504)
scala> ch.collect
res58: Array[String] = Array(atguigu1538981860504)
三、鍵值對RDD數據分區器
Spark目前支持Hash分區和Range分區,用戶也可以自定義分區,Hash分區爲當前的默認分區,Spark中分區器直接決定了RDD中分區的個數、RDD中每條數據經過Shuffle過程屬於哪個分區和Reduce的個數
注意:(1)只有Key-Value類型的RDD纔有分區器的,非Key-Value類型的RDD分區器的值是None
(2)每個RDD的分區ID範圍:0~numPartitions-1,決定這個值是屬於哪個分區的。
1、獲取分區
(1)創建一個pairRDD
scala> val pairs = sc.parallelize(List((1,1),(2,2),(3,3)))
pairs: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[3] at parallelize at <console>:24
(2)查看RDD的分區器
scala> pairs.partitioner
res1: Option[org.apache.spark.Partitioner] = None
(3)導入HashPartitioner類
scala> import org.apache.spark.HashPartitioner
import org.apache.spark.HashPartitioner
(4)使用HashPartitioner對RDD進行重新分區
scala> val partitioned = pairs.partitionBy(new HashPartitioner(2))
partitioned: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[4] at partitionBy at <console>:27
(5)查看重新分區後RDD的分區器
scala> partitioned.partitioner
res2: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@2)
2、Hash分區
HashPartitioner分區的原理:對於給定的key,計算其hashCode,併除以分區的個數取餘,如果餘數小於0,則用餘數+分區的個數(否則加0),最後返回的值就是這個key所屬的分區ID。
3、Ranger分區
HashPartitioner分區弊端:可能導致每個分區中數據量的不均勻,極端情況下會導致某些分區擁有RDD的全部數據。
RangePartitioner作用:將一定範圍內的數映射到某一個分區內,儘量保證每個分區中數據量的均勻,而且分區與分區之間是有序的,一個分區中的元素肯定都是比另一個分區內的元素小或者大,但是分區內的元素是不能保證順序的。簡單的說就是將一定範圍內的數映射到某一個分區內。實現過程爲:
第一步:先從整個RDD中抽取出樣本數據,將樣本數據排序,計算出每個分區的最大key值,形成一個Array[KEY]類型的數組變量rangeBounds;
第二步:判斷key在rangeBounds中所處的範圍,給出該key值在下一個RDD中的分區id下標;該分區器要求RDD中的KEY類型必須是可以排序的
4、自定義分區
要實現自定義的分區器,你需要繼承 org.apache.spark.Partitioner 類
class CustomerPartitioner(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
}
}
將RDD使用自定義的分區類進行重新分區
val par = data.partitionBy(new CustomerPartitioner(2))
四、數據讀取與保存
1、MySQL數據庫連接
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.27</version>
</dependency>
讀取mysql數據,轉換成RDD
import java.sql.DriverManager
import org.apache.spark.rdd.JdbcRDD
import org.apache.spark.{SparkConf, SparkContext}
//TODO 讀取mysql數據轉換成RDD
object MysqlRDD {
def main(args: Array[String]): Unit = {
//2.創建SparkContext
val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("JdbcRDD"))
//3.定義連接mysql的參數
val driver = "com.mysql.jdbc.Driver"
val url = "jdbc:mysql://hdp-1:3306/sqoop"
val userName = "root"
val passWd = "123456"
//創建JdbcRDD
val rdd = new JdbcRDD(sc, () => {
Class.forName(driver)
DriverManager.getConnection(url, userName, passWd)
},
"select * from tb3 where id >= ? and id <= ?;",
1, //id最小範圍 對應第一個問號
5, //id最大範圍 對應第二個問號
1, //分區數
r => (r.getInt(1), r.getString(2)) //結果集處理
)
//打印最後結果
println(rdd.count())
rdd.foreach(println)
sc.stop()
}
}
把RDD數據寫入Mysql
//TODO 把RDD寫入Mysql:
object writeMysql{
def main(args: Array[String]) {
val sc = new SparkContext(new SparkConf().setMaster("local[2]").setAppName("MysqlApp"))
val data = sc.parallelize(List("Female", "Male","Female"))
data.foreachPartition(insertData)
}
def insertData(iterator: Iterator[String]): Unit = {
Class.forName ("com.mysql.jdbc.Driver").newInstance()
val conn = java.sql.DriverManager.getConnection("jdbc:mysql://hdp-1:3306/sqoop", "root", "123456")
iterator.foreach(data => {
val ps = conn.prepareStatement("insert into tb3(username) values (?)")
ps.setString(1, data)
ps.executeUpdate()
})
}
}
2、HBase數據庫
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-server</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>1.3.1</version>
</dependency>
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.{HBaseConfiguration, HColumnDescriptor, HTableDescriptor, TableName}
import org.apache.hadoop.hbase.client.{HBaseAdmin, Put, Result}
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapred.TableOutputFormat
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.mapred.JobConf
object ReadHbase {
def main(args: Array[String]): Unit = {
//創建SparkContext
val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("HBaseRDD"))
//構建HBase配置信息
val conf: Configuration = HBaseConfiguration.create()
conf.set("hbase.zookeeper.quorum", "hdp-1,hdp-2,hdp-3")
conf.set(TableInputFormat.INPUT_TABLE, "stu")
//從HBase讀取數據形成RDD
val hbaseRDD: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
conf,
classOf[TableInputFormat],
classOf[ImmutableBytesWritable],
classOf[Result])
val count: Long = hbaseRDD.count()
println(count)
//對hbaseRDD進行處理
hbaseRDD.foreach {
case (_, result) =>
val key: String = Bytes.toString(result.getRow)
val name: String = Bytes.toString(result.getValue(Bytes.toBytes("infos"), Bytes.toBytes("name")))
val age: String = Bytes.toString(result.getValue(Bytes.toBytes("infos"), Bytes.toBytes("age")))
println("RowKey:" + key + ",Name:" + name + ",age:" + age)
}
//關閉連接
sc.stop()
}
}
object writeHbase{
def main(args: Array[String]): Unit = {
//獲取Spark配置信息並創建與spark的連接
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("HBaseApp")
val sc = new SparkContext(sparkConf)
//創建HBaseConf
val conf = HBaseConfiguration.create()
conf.set("hbase.zookeeper.quorum", "hdp-1,hdp-2,hdp-3")
val jobConf = new JobConf(conf)
jobConf.setOutputFormat(classOf[TableOutputFormat])
jobConf.set(TableOutputFormat.OUTPUT_TABLE, "fruit_spark")
//構建Hbase表描述器
val fruitTable = TableName.valueOf("fruit_spark")
val tableDescr = new HTableDescriptor(fruitTable)
tableDescr.addFamily(new HColumnDescriptor("info".getBytes))
//創建Hbase表
val admin = new HBaseAdmin(conf)
if (admin.tableExists(fruitTable)) {
admin.disableTable(fruitTable)
admin.deleteTable(fruitTable)
}
admin.createTable(tableDescr)
//定義往Hbase插入數據的方法
def convert(triple: (String, String, String)) = {
val put = new Put(Bytes.toBytes(triple._1))
put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(triple._2))
put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("price"), Bytes.toBytes(triple._3))
(new ImmutableBytesWritable, put)
}
//創建一個RDD 數值型容易出現編碼錯誤,可以改成 "1"
val initialRDD = sc.parallelize(List(("1","apple","11"), ("2","banana","12"), ("3","pear","13")))
//將RDD內容寫到HBase
val localData = initialRDD.map(convert)
localData.saveAsHadoopDataset(jobConf)
}
}
五、RDD編程進階
1、累加器
累加器用來對信息進行聚合,通常在向 Spark傳遞函數時,比如使用 map() 函數或者用 filter() 傳條件時,可以使用驅動器程序中定義的變量,但是集羣中運行的每個任務都會得到這些變量的一份新的副本,更新這些副本的值也不會影響驅動器中的對應變量。如果我們想實現所有分片處理時更新共享變量的功能,那麼累加器可以實現我們想要的效果。
系統累加器:
package com.xin
import org.apache.spark.{SparkConf, SparkContext}
//TODO 針對一個輸入的日誌文件,如果我們想計算文件中所有空行的數量,我們可以編寫以下程序:
object leijia {
def main(args: Array[String]): Unit = {
val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("HBaseRDD"))
val notice = sc.textFile("in2/test.txt")
// 創建出存有初始值0的累加器
val blanklines = sc.accumulator(0)
val tmp = notice.flatMap(line => {
if (line == "") {
blanklines += 1 //累加器進行計算
}
line.split(",")
})
val count: Long = tmp.count()
println("單詞個數:"+count)
val num: Int = blanklines.value
println("空行個數:"+num) //如果文本最後一行是空值,不進行計算
}
}
通過在驅動器Driver中調用SparkContext.accumulator(initialValue)方法,創建出存有初始值的累加器。Spark閉包(局部變量)裏的執行器代碼可以使用累加器的 += 方法(在Java中是 add)增加累加器的值。 驅動器程序可以調用累加器的value屬性(在Java中使用value()或setValue())來訪問累加器的值。
注意:工作節點上的任務不能訪問累加器的值。從這些任務的角度來看,累加器是一個只寫變量。對於要在行動操作中使用的累加器,Spark只會把每個任務對各累加器的修改應用一次。因此,如果想要一個無論在失敗還是重複計算時都絕對可靠的累加器,我們必須把它放在 foreach() 這樣的行動操作中,轉化操作中累加器可能會發生不止一次更新。
自定義累加器:
繼承AccumulatorV2並至少覆寫下例中出現的方法,下面這個累加器可以用於在程序運行過程中收集一些文本類信息,最終以Set[String]的形式返回。
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 sc=new SparkContext(new SparkConf().setAppName("LogAccumulator").setMaster("local[3]"))
val accum = new LogAccumulator //new自定義的累加器
sc.register(accum, "logAccum") //通過sc註冊累加器
val sum: Int = sc.parallelize(Array("1", "2a", "3", "4b", "5", "xin", "7cd", "8", "9"), 2).filter(line => {
val pattern = """^-?(\d+)""" //^:行開頭 (.\d+)?:括號裏內出現0或1次 表數字
val flag: Boolean = line.matches(pattern) //匹配是否符合pattern
if (!flag) { //!flag -->包含字母
accum.add(line)
}
flag
}).map(_.toInt).reduce(_ + _)
println("sum: " + sum)
for (v <- accum.value) print(v + " ")
println()
sc.stop()
}
}
2、廣播變量(調優策略)
廣播變量用來高效分發較大的對象。向所有工作節點發送一個較大的只讀值,以供一個或多個Spark操作使用。比如,如果你的應用需要向所有節點發送一個較大的只讀查詢表,甚至是機器學習算法中的一個很大的特徵向量,廣播變量用起來都很順手。 在多個並行操作中使用同一個變量,但是 Spark會爲每個任務分別發送。
val broadcastVar = sc.broadcast(Array(1, 2, 3))
print(broadcastVar.value.mkString(" "))
使用廣播變量的過程如下:
(1) 調用 SparkContext.broadcast 創建出一個 Broadcast[T] 對象。 任何可序列化的類型都可以這麼實現。
(2) 通過 value 屬性訪問該對象的值(在 Java 中爲 value() 方法)。
(3) 變量只會被髮到各個節點一次,應作爲只讀值處理(修改這個值不會影響到別的節點)。