本文將通過描述 Spark RDD ——彈性分佈式數據集(RDD,Resilient Distributed Datasets)的五大核心要素來描述 RDD,若希望更全面瞭解 RDD 的知識,請移步 RDD 論文:RDD:基於內存的集羣計算容錯抽象
RDD是Spark的最基本抽象,是對分佈式內存的抽象使用,實現了以操作本地集合的方式來操作分佈式數據集的抽象實現。RDD是Spark最核心的東西,它表示已被分區,不可變的並能夠被並行操作的數據集合,不同的數據集格式對應不同的RDD實現。RDD必須是可序列化的。RDD可以cache到內存中,每次對RDD數據集的操作之後的結果,都可以存放到內存中,下一個操作可以直接從內存中輸入,省去了MapReduce大量的磁盤IO操作。這對於迭代運算比較常見的機器學習算法, 交互式數據挖掘來說,效率提升非常大。
Spark 的五大核心要素包括:
- partition
- partitioner
- compute func
- dependency
- preferredLocation
下面一一來介紹
(一): partition
partition 個數怎麼定
RDD 由若干個 partition 組成,共有三種生成方式:
- 從 Scala 集合中創建,通過調用
SparkContext#makeRDD
或SparkContext#parallelize
- 加載外部數據來創建 RDD,例如從 HDFS 文件、mysql 數據庫讀取數據等
- 由其他 RDD 執行 transform 操作轉換而來
那麼,在使用上述方法生成 RDD 的時候,會爲 RDD 生成多少個 partition 呢?一般來說,加載 Scala 集合或外部數據來創建 RDD 時,是可以指定 partition 個數的,若指定了具體值,那麼 partition 的個數就等於該值,比如:
val rdd1 = sc.makeRDD( scalaSeqData, 3 ) //< 指定 partition 數爲3
val rdd2 = sc.textFile( hdfsFilePath, 10 ) //< 指定 partition 數爲10
若沒有指定具體的 partition 數時的 partition 數爲多少呢?
- 對於從 Scala 集合中轉換而來的 RDD:默認的 partition 數爲 defaultParallelism,該值在不同的部署模式下不同:
- Local 模式:本機 cpu cores 的數量
- Mesos 模式:8
- Yarn:max(2, 所有 executors 的 cpu cores 個數總和)
- 對於從外部數據加載而來的 RDD:默認的 partition 數爲
min(defaultParallelism, 2)
- 對於執行轉換操作而得到的 RDD:視具體操作而定,如 map 得到的 RDD 的 partition 數與 父 RDD 相同;union 得到的 RDD 的 partition 數爲父 RDDs 的 partition 數之和...
partition 的定義
我們常說,partition 是 RDD 的數據單位,代表了一個分區的數據。但這裏千萬不要搞錯了,partition 是邏輯概念,是代表了一個分片的數據,而不是包含或持有一個分片的數據。
真正直接持有數據的是各個 partition 對應的迭代器,要再次注意的是,partition 對應的迭代器訪問數據時也不是把整個分區的數據一股腦加載持有,而是像常見的迭代器一樣一條條處理。舉個例子,我們把 HDFS 上10G 的文件加載到 RDD 做處理時,並不會消耗10G 的空間,如果沒有 shuffle 操作(shuffle 操作會持有較多數據在內存),那麼這個操作的內存消耗是非常小的,因爲在每個 task 中都是一條條處理處理的,在某一時刻只會持有一條數據。這也是初學者常有的理解誤區,一定要注意 Spark 是基於內存的計算,但不會傻到什麼時候都把所有數據全放到內存。
讓我們來看看 Partition 的定義幫助理解:
trait Partition extends Serializable {
def index: Int
override def hashCode(): Int = index
}
在 trait Partition 中僅包含返回其索引的 index 方法。很多具體的 RDD 也會有自己實現的 partition,比如:
KafkaRDDPartition 提供了獲取 partition 所包含的 kafka msg 條數的方法
class KafkaRDDPartition(
val index: Int,
val topic: String,
val partition: Int,
val fromOffset: Long,
val untilOffset: Long,
val host: String,
val port: Int
) extends Partition {
/** Number of messages this partition refers to */
def count(): Long = untilOffset - fromOffset
}
UnionRDD 的 partition 類 UnionPartition 提供了獲取依賴的父 partition 及獲取優先位置的方法
private[spark] class UnionPartition[T: ClassTag](
idx: Int,
@transient private val rdd: RDD[T],
val parentRddIndex: Int,
@transient private val parentRddPartitionIndex: Int)
extends Partition {
var parentPartition: Partition = rdd.partitions(parentRddPartitionIndex)
def preferredLocations(): Seq[String] = rdd.preferredLocations(parentPartition)
override val index: Int = idx
}
partition 與 iterator 方法
RDD 的 def iterator(split: Partition, context: TaskContext): Iterator[T]
方法用來獲取 split 指定的 Partition 對應的數據的迭代器,有了這個迭代器就能一條一條取出數據來按 compute chain 來執行一個個transform 操作。iterator 的實現如下:
final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
if (storageLevel != StorageLevel.NONE) {
SparkEnv.get.cacheManager.getOrCompute(this, split, context, storageLevel)
} else {
computeOrReadCheckpoint(split, context)
}
}
def 前加了 final 說明該函數是不能被子類重寫的,其先判斷 RDD 的 storageLevel 是否爲 NONE,若不是,則嘗試從緩存中讀取,讀取不到則通過計算來獲取該 Partition 對應的數據的迭代器;若是,嘗試從 checkpoint 中獲取 Partition 對應數據的迭代器,若 checkpoint 不存在則通過計算來獲取。
剛剛介紹瞭如果從 cache 或者 checkpoint 無法獲得 Partition 對應的數據的迭代器,則需要通過計算來獲取,這將會調用到 def compute(split: Partition, context: TaskContext): Iterator[T]
方法,各個 RDD 最大的不同也體現在該方法中。後文會詳細介紹該方法
(二): partitioner
partitioner 即分區器,說白了就是決定 RDD 的每一條消息應該分到哪個分區。但只有 k, v 類型的 RDD 纔能有 partitioner(當然,非 key, value 類型的 RDD 的 partitioner 爲 None。
partitioner 爲 None 的 RDD 的 partition 的數據要麼對應數據源的某一段數據,要麼來自對父 RDDs 的 partitions 的處理結果。
我們先來看看 Partitioner 的定義及註釋說明:
abstract class Partitioner extends Serializable {
//< 返回 partition 數量
def numPartitions: Int
//< 返回 key 應該屬於哪個 partition
def getPartition(key: Any): Int
}
Partitioner 共有兩種實現,分別是 HashPartitioner 和 RangePartitioner
HashPartitioner
先來看 HashPartitioner 的實現(省去部分代碼):
class HashPartitioner(partitions: Int) extends Partitioner {
require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")
def numPartitions: Int = partitions
def getPartition(key: Any): Int = key match {
case null => 0
case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
}
...
}
// x 對 mod 求於,若結果爲正,則返回該結果;若結果爲負,返回結果加上 mod
def nonNegativeMod(x: Int, mod: Int): Int = {
val rawMod = x % mod
rawMod + (if (rawMod < 0) mod else 0)
}
numPartitions
直接返回主構造函數中傳入的 partitions 參數,之前在有本書裏看到說 Partitioner 不僅決定了一條 record 應該屬於哪個 partition,還決定了 partition 的數量,其實這句話的後半段的有誤的,Partitioner 並不能決定一個 RDD 的 partition 數,Partitioner 方法返回的 partition 數是直接返回外部傳入的值。
getPartition
方法也不復雜,主要做了:
- 爲參數 key 計算一個 hash 值
- 若該哈希值對 partition 個數取餘結果爲正,則該結果即該 key 歸屬的 partition index;否則,以該結果加上 partition 個數爲 partition index
從上面的分析來看,當 key, value 類型的 RDD 的 key 的 hash 值分佈不均勻時,會導致各個 partition 的數據量不均勻,極端情況下一個 partition 會持有整個 RDD 的數據而其他 partition 則不包含任何數據,這顯然不是我們希望看到的,這時就需要 RangePartitioner 出馬了。
RangePartitioner
上文也提到了,HashPartitioner 可能會導致各個 partition 數據量相差很大的情況。這時,初衷爲使各個 partition 數據分佈儘量均勻的 RangePartitioner 便有了用武之地。
RangePartitioner 將一個範圍內的數據映射到 partition,這樣兩個 partition 之間要麼是一個 partition 的數據都比另外一個大,或者小。RangePartitioner採用水塘抽樣算法,比 HashPartitioner 耗時,具體可見:Spark分區器HashPartitioner和RangePartitioner代碼詳解
(三): compute func
在前一篇文章中提到,當調用 RDD#iterator
方法無法從緩存或 checkpoint 中獲取指定 partition 的迭代器時,就需要調用 compute
方法來獲取,該方法聲明如下:
def compute(split: Partition, context: TaskContext): Iterator[T]
每個具體的 RDD 都必須實現自己的 compute 函數。從上面的分析我們可以聯想到,任何一個 RDD 的任意一個 partition 都首先是通過 compute 函數計算出的,之後才能進行 cache 或 checkpoint。接下來我們來對幾個常用 transformation 操作對應的 RDD 的 compute 進行分析
map
首先來看下 map 的實現:
def map[U: ClassTag](f: T => U): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}
我們調用 map 時,會傳入匿名函數 f: T => U
,該函數將一個類型 T 實例轉換成一個類型 U 的實例。在 map 函數中,將該函數進一步封裝成 (context, pid, iter) => iter.map(cleanF)
的函數,該函數以迭代器作爲參數,對迭代出的每一個元素執行 f 函數,然後以該封裝後的函數作爲參數來構造 MapPartitionsRDD,接下來看看 MapPartitionsRDD#compute
是怎麼實現的:
override def compute(split: Partition, context: TaskContext): Iterator[U] =
f(context, split.index, firstParent[T].iterator(split, context))
上面代碼中的 firstParent 是指本 RDD 的依賴 dependencies: Seq[Dependency[_]]
中的第一個,MapPartitionsRDD 的依賴中只有一個父 RDD。而 MapPartitionsRDD 的 partition 與其唯一的父 RDD partition 是一一對應的,所以其 compute 方法可以描述爲:對父 RDD partition 中的每一個元素執行傳入 map 的方法得到自身的 partition 及迭代器
groupByKey
與 map、union 不同,groupByKey 是一個會產生寬依賴的 transform,其最終生成的 RDD 是 ShuffledRDD,來看看其 compute 實現:
override def compute(split: Partition, context: TaskContext): Iterator[(K, C)] = {
val dep = dependencies.head.asInstanceOf[ShuffleDependency[K, V, C]]
SparkEnv.get.shuffleManager.getReader(dep.shuffleHandle, split.index, split.index + 1, context)
.read()
.asInstanceOf[Iterator[(K, C)]]
}
可以看到,ShuffledRDD 的 compute 使用 ShuffleManager 來獲取一個 reader,該 reader 將從本地或遠程 BlockManager 拉取 map output 的 file 數據,每個 reduce task 拉取一個 partition 數據。
對於其他生成 ShuffledRDD 的 transform 的 compute 操作也是如此,比如 reduceByKey,join 等
(四): dependency
RDD 依賴是一個 Seq 類型:dependencies_ : Seq[Dependency[_]]
,因爲一個 RDD 可以有多個父 RDD。共有兩種依賴:
- 窄依賴:父 RDD 的 partition 至多被一個子 RDD partition 依賴
- 寬依賴:父 RDD 的 partition 被多個子 RDD partitions 依賴
窄依賴共有兩種實現,一種是一對一的依賴,即 OneToOneDependency:
@DeveloperApi
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
override def getParents(partitionId: Int): List[Int] = List(partitionId)
}
從其 getParents 方法可以看出 OneToOneDependency 的依賴關係下,子 RDD 的 partition 僅依賴於唯一 parent RDD 的相同 index 的 partition。另一種窄依賴的實現是 RangeDependency,它僅僅被 UnionRDD 使用,UnionRDD 把多個 RDD 合成一個 RDD,這些 RDD 是被拼接而成,其 getParents 實現如下:
override def getParents(partitionId: Int): List[Int] = {
if (partitionId >= outStart && partitionId < outStart + length) {
List(partitionId - outStart + inStart)
} else {
Nil
}
}
寬依賴只有一種實現,即 ShuffleDependency,寬依賴支持兩種 Shuffle Manager,即 HashShuffleManager
和 SortShuffleManager
,Shuffle 相關內容以後會專門寫文章介紹
(五): preferedLocation
preferedLocation 即 RDD 每個 partition 對應的優先位置,每個 partition 對應一個Seq[String]
,表示一組優先節點的 host。
要注意的是,並不是每個 RDD 都有 preferedLocation,比如從 Scala 集合中創建的 RDD 就沒有,而從 HDFS 讀取的 RDD 就有,其 partition 對應的優先位置及對應的 block 所在的各個節點。
作者:牛肉圓粉不加蔥
鏈接:https://www.jianshu.com/p/207607888767