簡介
Spark是一種基於內存的快、通用、可擴展的大數據分析引擎
特點
-
快
Spark與Map Reduce相比,基於內存的運行要快100倍,基於硬盤的運算要快10倍以上。其中間結果可以緩存在內存中,達到複用的目的。
-
易用
Spark支持Java、Python、Scala的API,還支持超過80種高級算法,使用戶可以快速的構建不同的應用。而且Spark支持交互式的Python和Scalal的Shell。
-
通用
Spark提供了統一的解決方案,且這些方案可以應用在同一個應用中,如批處理、交互式查詢、實時流處理、機器學習和圖計算。減少了開發和維護的成本和部署平臺的物力成本。
-
兼容性
Spark可以非常方便地和其它的開源產品進行融合。
Spark快於Map Reduce的原因?
網上大部分說是因爲Spark是基於內存的,而MapReduce是基於磁盤的。
還有說Spark中具有DAG有向無環圖,DAG有向無環圖在此過程中減少了shuffle以及落地磁盤的次數。
上述描述確實都對,但是我更相信下面的說法。
以下內容來自
作者:連城
鏈接:https://www.zhihu.com/question/23079001/answer/23569986
來源:知乎
在Spark內部,單個executor進程內RDD的分片數據是用Iterator流式訪問的,Iterator的hasNext方法和next方法是由RDD lineage上各個transformation攜帶的閉包函數複合而成的。該複合Iterator每訪問一個元素,就對該元素應用相應的複合函數,得到的結果再流式地落地(對於shuffle stage是落地到本地文件系統留待後續stage訪問,對於result stage是落地到HDFS或送回driver端等等,視選用的action而定)。如果用戶沒有要求Spark cache該RDD的結果,那麼這個過程佔用的內存是很小的,一個元素處理完畢後就落地或扔掉了(概念上如此,實現上有buffer),並不會長久地佔用內存。只有在用戶要求Spark cache該RDD,且storage level要求在內存中cache時,Iterator計算出的結果纔會被保留,通過cache manager放入內存池。
簡單起見,暫不考慮帶shuffle的多stage情況和流水線優化。這裏拿最經典的log處理的例子來具體說明一下(取出所有以ERROR開頭的日誌行,按空格分隔並取第2列):
val lines = spark.textFile("hdfs://<input>")
val errors = lines.filter(_.startsWith("ERROR"))
val messages = errors.map(_.split(" ")(1))
messages.saveAsTextFile("hdfs://<output>")
按傳統單機immutable FP的觀點來看,上述代碼運行起來好像是:
- 把HDFS上的日誌文件全部拉入內存形成一個巨大的字符串數組,
- Filter一遍再生成一個略小的新的字符串數組,
- 再map一遍又生成另一個字符串數組。
真這麼玩兒的話Spark早就不用混了……
如前所述,Spark在運行時動態構造了一個複合Iterator。就上述示例來說,構造出來的Iterator的邏輯概念上大致長這樣:
new Iterator[String] {
private var head: String = _
private var headDefined: Boolean = false
def hasNext: Boolean = headDefined || {
do {
try head = readOneLineFromHDFS(...) // (1) read from HDFS
catch {
case _: EOFException => return false
}
} while (!head.startsWith("ERROR")) // (2) filter closure
true
}
def next: String = if (hasNext) {
headDefined = false
head.split(" ")(1) // (3) map closure
} else {
throw new NoSuchElementException("...")
}
}
模塊
重要角色
Driver
- 把用戶程序轉爲作業(JOB)
- 跟蹤Executor的運行狀況
- 爲執行器節點調度任務
- UI展示應用運行狀況
Executor
-
負責運行組成 Spark 應用的任務,並將結果返回給驅動器進程;
-
通過自身的塊管理器(Block Manager)爲用戶程序中要求緩存的RDD提供內存式存儲。RDD是直接緩存在Executor進程內的,因此任務可以在運行時充分利用緩存數據加速運算。
運行邏輯
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2pnUlnyD-1573975929640)(assets/1573956622450.png)]
Spark Core
實現了Spark的基本功能,包含任務調度、內存管理、錯誤恢復、與存儲系統交互等模塊。
Spark Core中還包含了對彈性分佈式數據集(Resilient Distributed DataSet,簡稱RDD)的API定義
RDD
RDD(Resilient Distributed Dataset)彈性分佈式數據集,是Spark中最基本的數據抽象。
代表一個不可變的、可分區、裏面的元素可並行計算的集合。
官方這樣定義RDD
* Internally, each RDD is characterized by five main properties:
*
* - A list of partitions
* - A function for computing each split
* - A list of dependencies on other RDDs
* - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
* - Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
在內部,每個RDD是由以下5個主要特徵組成的:
1. 每個RDD都有一組分區
2. 每個分區上都是一種計算邏輯
3. 各個RDD之間都有依賴關係
4. 可選,對於K-V類型的數據有一個分區器。(就是說,如果是K-V類型數據,就要告訴它怎麼去分區,默認是根據Keyhash)
5. 可選,一個存儲每個分區優先計算位置的列表(因爲計算是由Driver發給Executor的,
在HDFS上並不是每個節點都有所有的數據,而且節點之間的距離也影響着IO傳輸,所以需要知道哪個節點運行效率最高)
知識點
- RDD的算子分爲兩種,一種叫轉換算子,一種叫行動算子。只有當行動算子觸發時,轉換算子纔會依次執行。
- RDD裏面並沒有數據、RDD可以理解爲對於真實數據的計算描述
RDD的創建
object RDDLearning {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("RDDLearning").setMaster("local[*]")
val sc = new SparkContext(conf)
// 1. 由集合創建
//有兩種方式 parallelize 和 makeRDD
//其中makeRDD底層調用的就是parallelize 爲了好記,我常用makeRDD
//def makeRDD[T: ClassTag](
// seq: Seq[T],
// numSlices: Int = defaultParallelism): RDD[T] = withScope {
// parallelize(seq, numSlices)
//}
val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4, 5, 6, 7))
val rdd2: RDD[Int] = sc.parallelize(Array(1, 2, 3, 4, 5, 6, 7))
// 2. 由外部文件創建
val fileRdd: RDD[String] = sc.textFile("dir/in/data.txt")
// 3. 由其它RDD創建 其實就是轉換後的RDD
val rdd3: RDD[Array[String]] = fileRdd.map(_.split(" "))
sc.stop
}
}
RDD的分區
object RDDLearning {
def main(args: Array[String]): Unit = {
//local[3] 代表三個分區
val conf = new SparkConf().setAppName("RDDLearning").setMaster("local[3]")
val sc = new SparkContext(conf)
val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7))
// val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7),1) 也可以這樣指定分區
println("分區數:"+rdd1.partitions.length)//3
rdd1.mapPartitionsWithIndex {
case (num, datas) => {
datas.map((_, "分區號:" + num))
}
}.collect.foreach(println)
//(1,分區號:0)
//(2,分區號:0)
//(3,分區號:1)
//(4,分區號:1)
//(5,分區號:2)
//(6,分區號:2)
//(7,分區號:2)
sc.stop
}
}
分區邏輯
-
由集合生成的RDD分區邏輯
// 1. 我們可以跟蹤源碼 def parallelize[T: ClassTag]( seq: Seq[T], // 2. 如果沒有指定分區數,就會使用默認的並行度 numSlices: Int = defaultParallelism): RDD[T] = withScope { assertNotStopped() new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]()) } // 3. defaultParallelism 默認並行的的邏輯 def defaultParallelism: Int = { assertNotStopped() taskScheduler.defaultParallelism //這個 //org.apache.spark.scheduler.TaskSchedulerImpl#defaultParallelism //TaskSchedulerImpl是個特性 trait 所以找它的實現類 } // 4. 這裏隨便找了一個LocalSchedulerBackend override def defaultParallelism(): Int = scheduler.conf.getInt("spark.default.parallelism", totalCores) //結論: 如果設置中設置了spark.default.parallelism 就選用它的值 //沒有的話就算totalCores 總共的內核數 這也就是爲什麼Local[3] 就三個分區的原因
-
由外部文件生成的RDD
//1. 依舊看源碼 def textFile( path: String, //2. 這裏可以指定最小的分區數(之所以最小,是因爲最後還是根據文件大小分區) //如果沒指定 minPartitions: Int = defaultMinPartitions): RDD[String] = withScope { assertNotStopped() hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],minPartitions) .map(pair => pair._2.toString).setName(path) } //3. 沒指定就和普通集合生成的並行度與2取最小值 def defaultMinPartitions: Int = math.min(defaultParallelism, 2) //4. 再來看具體的分區運輸規則 hadoopFile(path,...,minPartitions) //分區數給了hadoopFile //5. hadoopFile裏面 new HadoopRDD( this, confBroadcast, Some(setInputPathsFunc), inputFormatClass, keyClass, valueClass, minPartitions).setName(path) //6. hadoopRDD裏面有這個方法 override def getPartitions: Array[Partition] = { val inputSplits = inputFormat.getSplits(jobConf, minPartitions) } //7. 這裏看一下org.apache.hadoop.mapred.FileInputFormat#getSplits 別的邏輯都差不多 //因爲篇幅,我只取了分區邏輯部分,不是所有代碼 public InputSplit[] getSplits(JobConf job, int numSplits(這是minPartitions)){ long totalSize = 0; for (FileStatus file: files) { totalSize += file.getLen(); //獲得所有文件的大小,因爲路徑可以是個目錄 } //常理走下來 goalSize=numSplits 這裏假設我們設置是local[*] //goalSize=所有文件的大小/2 long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits); //minSize=1 long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input. FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize); // generate splits ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits); for (FileStatus file: files) { long length = file.getLen(); if (isSplitable(fs, path)) { //本地跑的所以是32M long blockSize = file.getBlockSize(); long splitSize = computeSplitSize(goalSize=2, minSize=1, blockSize=32M); //邏輯Math.max(minSize, Math.min(goalSize, blockSize)); //splitSize=超過塊大小的文件就是塊大小,沒超過的就是文件總的大小 //未切分文件大小 long bytesRemaining = length; //以下的切片邏輯差不多就是如果小文件就一片 因爲bytesRemaining=splitSize //如果是大文件的話 就是以一個塊大小爲一片,如果剩餘文件大小/塊大小<=1.1 就直接一片 while (((double) bytesRemaining)/splitSize > 1.1) { String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length-bytesRemaining, splitSize, clusterMap); splits.add(makeSplit(path, length-bytesRemaining, splitSize, splitHosts[0], splitHosts[1])); bytesRemaining -= splitSize; } if (bytesRemaining != 0) { String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length - bytesRemaining, bytesRemaining, clusterMap); splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining, splitHosts[0], splitHosts[1])); } } else { String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,0,length,clusterMap); splits.add(makeSplit(path, 0, length, splitHosts[0], splitHosts[1])); } } else { //Create empty hosts array for zero length files splits.add(makeSplit(path, 0, length, new String[0])); } } }
重分區
object RDDLearning {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("RDDLearning").setMaster("local[3]")
val sc = new SparkContext(conf)
val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7))
//重分區也有兩個repartition 和coalesce
//def repartition(numPartitions: Int) RDD[T] = {
// coalesce(numPartitions, shuffle = true)
//}
rdd1.repartition(4).mapPartitionsWithIndex {
case (num, datas) => {
datas.map((_, "分區號:" + num))
}
}.collect.foreach(println)
//很奇怪,但是確實是hash分區的
//(2,分區號:0)
//(4,分區號:0)
//(6,分區號:0)
//(7,分區號:1)
//(1,分區號:3)
//(3,分區號:3)
//(5,分區號:3)
sc.stop
}
}
序列化
由於Spark的分佈式的分析引擎,數據的初始化在Driver端,但是實際運行程序是在Executor端,這就涉及到了跨進程通信,是需要序列化的。
class SearchFunctions(val query: String) extends Serializable {
//第一個方法是判斷輸入的字符串是否存在query 存在返回true,不存在返回false
def isMatch(s: String): Boolean = {
s.contains(query)
}
def getMatchesFunctionReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
// 需要序列化:"isMatch"表示"this.isMatch",因此我們要傳遞整個"this"
rdd.filter(isMatch)
}
def getMatchesFieldReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
// 需要序列化:"query"表示"this.query",因此我們要傳遞整個"this"
rdd.filter(x=>x.contains(query))
}
def getMatchesNoReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
// 不需要序列化:只把我們需要的字段拿出來放入局部變量中
val query_ = this.query
rdd.filter(x => x.contains(query_))
}
}
object SearchFunctions {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf()
conf.setAppName("SearchFunctions")
conf.setMaster("local[2]")
val sc: SparkContext = new SparkContext(conf)
val rdd = sc.parallelize(List("hello java", "hello scala hello", "hello hello"))
val sf = new SearchFunctions("hello")
val unit: RDD[String] = sf.getMatchesNoReference(rdd)
unit.foreach(println)
}
}
緩存
RDD通過persist方法或cache方法可以將前面的計算結果緩存,默認情況下persist() 會把數據以序列化的形式緩存在JVM 的堆空間中。
存儲級別
NONE 不緩存
DISK_ONLY 只存磁盤
DISK_ONLY_2 只存磁盤,存兩份
MEMORY_ONLY
MEMORY_ONLY_2
MEMORY_ONLY_SER 只存內存中並且序列化存儲
MEMORY_ONLY_SER_2
MEMORY_AND_DISK
MEMORY_AND_DISK_2
MEMORY_AND_DISK_SER
MEMORY_AND_DISK_SER_2
OFF_HEAP 存堆外內存中
緩存有可能丟失,或者存儲存儲於內存的數據由於內存不足而被刪除,RDD的緩存容錯機制保證了即使緩存丟失也能保證計算的正確執行。通過基於RDD的一系列轉換,丟失的數據會被重算,由於RDD的各個Partition是相對獨立的,因此只需要計算丟失的部分即可,並不需要重算全部Partition。
object RDD_Cache {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("RDD_Cache").setMaster("local[2]")
val sc = new SparkContext(conf)
val timeRdd = sc.makeRDD(List("zzy"))
val mapRDD1 = timeRdd.map(x => (x, System.currentTimeMillis()))
println(mapRDD1.collect.toBuffer)
println(mapRDD1.collect.toBuffer)
println(mapRDD1.collect.toBuffer) //上面三個時間都不同
mapRDD1.cache() //mapRDD1.persist()
println(mapRDD1.collect.toBuffer)
println(mapRDD1.collect.toBuffer)
println(mapRDD1.collect.toBuffer) //上面三個時間都相同 緩存了
}
}
分區器
- 只有Key-Value類型的RDD纔有分區器的,非Key-Value類型的RDD分區器的值是None
- 每個RDD的分區ID範圍:0~numPartitions-1,決定這個值是屬於那個分區的。
object SubjectDemo3 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("SubjectDemo").setMaster("local")
val sc = new SparkContext(conf)
// 1.對數據進行切分
val tuples: RDD[(String, Int)] =
sc.textFile("C:\\Users\\Administrator\\Desktop\\subjectaccess\\access.txt").map(line => {
val fields: Array[String] = line.split("\t")
//取出url
val url = fields(1)
(url, 1)
})
//將相同url進行聚合,得到了各個學科的訪問量
val sumed: RDD[(String, Int)] = tuples.reduceByKey(_ + _).cache()
//從url中獲取學科的字段 數據組成式 學科, url 統計數量
val subjectAndUC = sumed.map(tup => {
val url = tup._1 //用戶url
val count = tup._2 // 統計的訪問數量
val subject = new URL(url).getHost //學科
(subject, (url, count))
})
//將所有學科取出來
val subjects: Array[String] = subjectAndUC.keys.distinct.collect
//創建自定義分區器對象
val partitioner: SubjectPartitioner = new SubjectPartitioner(subjects)
//分區
val partitioned: RDD[(String, (String, Int))] = subjectAndUC.partitionBy(partitioner)
//取top3
val rs = partitioned.mapPartitions(it => {
val list = it.toList
val sorted = list.sortBy(_._2._2).reverse
val top3: List[(String, (String, Int))] = sorted.take(3)
//因爲方法的返回值需要一個iterator
top3.iterator
})
//存儲數據
rs.saveAsTextFile("out2")
sc.stop()
}
}
/**
* 自定義分區器需要繼承Partitioner並實現對應方法
*/
class SubjectPartitioner(subjects: Array[String]) extends Partitioner {
//創建一個map集合用來存到分區號和學科
val subject = new mutable.HashMap[String, Int]()
//定義一個計數器,用來生成分區好
var i = 0
for (s <- subjects) {
//存學科和分區
subject += (s -> i)
i += 1 //分區自增
} // 獲取分區數
override def numPartitions: Int = subjects.size
//獲取分區號(如果傳入的key不存在,默認將數據存儲到0分區)
override def getPartition(key: Any): Int = subject.getOrElse(key.toString, 0)
}
累加器
解決的問題:分佈式只寫共享變量
object AccumulatorDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("AccumulatorDemo").setMaster("local[2]")
val sc = new SparkContext(conf)
val dataRDD= sc.makeRDD(1 to 10)
var sum = 0 //由於sum只存在在driver端,而foreach(內得內容要發送到Executor上執行,並沒有sum,所以累計失敗)
dataRDD.foreach(x=>sum=sum+x)
println(sum) // 0
//1. 創建一個累加器
val acc=new LongAccumulator
//2. 註冊
sc.register(acc)
//3. 使用
dataRDD.foreach(x=>acc.add(x))
println(acc.value) //55
sc.stop()
}
}
自定義累加器
class MyAccumulator extends AccumulatorV2[Int,Int]{
//創建一個輸出值的變量
private var sum:Int = _
//必須重寫如下方法:
//檢測方法是否爲空
override def isZero: Boolean = sum == 0
//拷貝一個新的累加器
override def copy(): AccumulatorV2[Int, Int] = {
//需要創建當前自定累加器對象
val myaccumulator = new MyAccumulator()
//需要將當前數據拷貝到新的累加器數據裏面
//也就是說將原有累加器中的數據拷貝到新的累加器數據中
//ps:個人理解應該是爲了數據的更新迭代
myaccumulator.sum = this.sum
myaccumulator
}
//重置一個累加器 將累加器中的數據清零
override def reset(): Unit = sum = 0
//每一個分區中用於添加數據的方法(分區中的數據計算)
override def add(v: Int): Unit = {
//v 即 分區中的數據
//當累加器中有數據的時候需要計算累加器中的數據
sum += v
}
//合併每一個分區的輸出(將分區中的數進行彙總)
override def merge(other: AccumulatorV2[Int, Int]): Unit = {
//將每個分區中的數據進行彙總
sum += other.value
}
//輸出值(最終累加的值)
override def value: Int = sum
}
object MyAccumulator{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("MyAccumulator").setMaster("local[*]")
//2.創建SparkContext 提交SparkApp的入口
val sc = new SparkContext(conf)
val numbers = sc .parallelize(List(1,2,3,4,5,6),2)
val accumulator = new MyAccumulator()
//需要註冊
sc.register(accumulator,"acc")
//切記不要使用Transformation算子 會出現無法更新數據的情況
//應該使用Action算子
//若使用了Map會得不到結果
numbers.foreach(x => accumulator.add(x))
println(accumulator.value)
}
}
廣播變量
解決的問題:分佈式只讀共享變量
object BroadcastDemo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("BroadcastDemo").setMaster("local[2]")
val sc = new SparkContext(conf)
//list是在driver端創建也相當於是本地變量
val list = List("hello java")
val lines = sc.textFile("dir/file")
//算子部分是在Excecutor端執行
val filterStr = lines.filter(list.contains(_))
filterStr.foreach(println)
}
}
上面的代碼有個問題,就是Driver會把list以task的方式發送到executor上執行,可以粗略的認爲一個分區就算一個task,那麼list就可能會在一個executor上重複多份。如果list稍微大點可能就會造成內存溢出。
object BroadcastDemo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("BroadcastDemo").setMaster("local[2]")
val sc = new SparkContext(conf)
//list是在driver端創建也相當於是本地變量
val list = List("hello java")
//封裝廣播變量
val broadcast = sc.broadcast(list)
//算子部分是在Excecutor端執行
val lines = sc.textFile("dir/file")
//使用廣播變量進行數據處理 value可以獲取廣播變量的值
val filterStr = lines.filter(broadcast.value.contains(_))
filterStr.foreach(println)
}
}