--------------------------------------------------------------shuffle操作圖--------------------------------------------------------------
基本概念:
spark中的partition 是彈性分佈式數據集RDD的最小單元
spark.shuffle.manager有三種方式:
hash,sort,tungsten-sort
shuffle示意圖如下(最清晰易懂的):
根據上面的圖可以看到,shuffle其實就是得到reduceByKey的結果的示意圖,
[18]Any join, cogroup, or ByKey operation involves holding objects in hashmaps or in-memory buffers to group or sort(提到了哪些操作會導致shuffle)
[19]
cogroup
groupWith
join: hash partition
leftOuterJoin: hash partition
rightOuterJoin: hash partition
groupByKey: hash partition
reduceByKey: hash partition
combineByKey: hash partition
sortByKey: range partition
distinct
intersection: hash partition
repartition
coalesce(提到了哪些操作會導致shuffle)
#---------------------------------------job-stage-task-partition區別----------------------------------------------------------------------
執行一個 rdd 的 action 的時候,會生成一個 job
stage 是一個 job 的組成單位,就是說,一個 job 會被切分成 1 個或 1 個以上的 stage,然後各個 stage 會按照執行順序依次執行
A unit of work within a stage, corresponding to one RDD partition。即 stage 下的一個任務執行單元,一般來說,一個 rdd 有多少個 partition(spark中的partition 是彈性分佈式數據集RDD的最小單元),就會有多少個 task,因爲每一個 task 只是處理一個 partition 上的數據。從 web ui 截圖上我們可以看到,這個 job 一共有 2 個 stage,66 個 task,平均下來每個 stage 有 33 個 task,相當於每個 stage 的數據都有 33 個 partition [注意:這裏是平均下來的哦,並不都是每個 stage 有 33 個 task,有時候也會有一個 stage 多,另外一個 stage 少的情況,就看你有沒有在不同的 stage 進行 repartition 類似的操作了。
準備數據集[1]:
hdfs dfs -put names user/mercury
下面輸入代碼(來自[14]):
### packages
import pandas as pd
from operator import add
### spark UDF (User Defined Functions)
def map_extract(element):
file_path, content = element
year = file_path[-8:-4]
return [(year, i) for i in content.split("\r\n") if i]
### spark logic
res = sc.wholeTextFiles('hdfs://Desktop:9000/user/mercury/names',minPartitions=40).map(map_extract) \
.flatMap(lambda x: x) \
.map(lambda x: (x[0], int(x[1].split(',')[2]))) \
.reduceByKey(add) \
.collect()
### result displaying
data = pd.DataFrame.from_records(res, columns=['year', 'birth'])\
.sort_values(by='year', ascending=True)
ax = data.plot(x="year", y="birth",
figsize=(20, 6),
title='US Baby Birth Data from 1897 to 2014',
linewidth=3)
ax.set_facecolor('white')
ax.grid(color='gray', alpha=0.2, axis='y')
想要繪製出圖形的話需要pyspark+jupyter Notebook環境的搭建(沒有必要特意去折騰pyspark環境配置,因爲pyspark有個官方bug[15])
$ pyspark --master yarn
然後逐句輸入上述代碼運行即可
#-------------------------------------------------下面是實驗結果分析-------------------------------------------------------------------------------------------------
下面打開web UI界面,步驟如下:
②點擊Tracking UI下面的History跳轉到:
這個圖怎麼看呢?點擊stage0我們看到:
\
也就是說對應上面代碼到reduceByKey(add) 爲止。
同樣,點開stage1,看到的是:
很簡單,對應的是代碼中的collect()操作
這兩個DAG圖有一個好處,就是當你不確定你的代碼中哪個部分會導致shuffle操作的時候,你可以上DAG圖看一下,來幫你對代碼中導致 data skew(數據傾斜,很多情況下都是shuffle操作導致的)的部分進行定位
[14]當觸發 rdd 的 shuffle 操作時 : 在我們的應用中就是 reduceByKey
這個操作,官方文檔: rdd.reduceByKey
當觸發 rdd 的 action 時 : 在我們的應用中就是最後的 collect
操作,關於這個操作的說明,可以看官方文檔: rdd.collect
-
第一個 stage,即截圖中 stage id 爲 0 的 stage,其執行了
sc.wholeTextFiles().map().flatMap().map().reduceByKey()
這幾個步驟,因爲這是一個Shuffle
操作,所以後面會有Shuffle Read
和Shuffle Write
。具體來說,就是在 stage 0 這個 stage 中,發生了一個 Shuffle 操作,這個操作讀入 22.5 MB 的數據,生成 41.7 KB 的數據,並把生成的數據寫在了硬盤上。 -
第二個 stage,即截圖中 stage id 爲 1 到 stage,其執行了
collect()
這個操作,因爲這是一個action
操作,並且它上一步是一個 Shuffle 操作,且沒有後續操作,所以這裏collect()
這個操作被獨立成一個 stage 了。這裏它把上一個 Shuffle 寫下的數據讀取進來,然後一起返回到 driver 端,所以這裏可以看到他的Shuffle Read
這裏剛好讀取了上一個 stage 寫下的數據。
#--------------------------------------------------------------------附錄-----------------------------------------------------------------------------------------
哪些是action操作?
參考[20],如下:
行動 | 涵義 |
---|---|
reduce(func) | 使用傳入的函數參數 func 對數據集中的元素進行匯聚操作 (兩兩合併). 該函數應該具有可交換與可結合的性質, 以便於能夠正確地進行並行計算. |
collect() | 在 driver program 上將數據集中的元素作爲一個數組返回. 這在執行一個 filter 或是其他返回一個足夠小的子數據集操作後十分有用. |
count() | 返回數據集中的元素個數 |
first() | 返回數據集中的第一個元素 (與 take(1) 類似) |
take(n) | 返回數據集中的前 n 個元素 |
takeSample(withReplacement, num, [seed]) | 以數組的形式返回數據集中隨機採樣的 num 個元素. |
takeOrdered(n, [ordering]) | 以其自然序或使用自定義的比較器返回 RDD 的前 n 元素 |
saveAsTextFile(path) | 將數據集中的元素寫入到指定目錄下的一個或多個文本文件中, 該目錄可以存在於本地文件系統, HDFS 或其他 Hadoop 支持的文件系統. Spark 將會對每個元素調用 toString 將其轉換爲文件的一行文本. |
saveAsSequenceFile(path)(Java and Scala) | 對於本地文件系統, HDFS 或其他任何 Hadoop 支持的文件系統上的一個指定路徑, 將數據集中的元素寫爲一個 Hadoop SequenceFile. 僅適用於實現了 Hadoop Writable 接口的 kay-value pair 的 RDD. 在 Scala 中, 同樣適用於能夠被隱式轉換成 Writable 的類型上 (Spark 包含了對於 Int, Double, String 等基本類型的轉換). |
saveAsObjectFile(path)(Java and Scala) | 使用 Java 序列化將數據集中的元素簡單寫爲格式化數據, 可以通過 SparkContext.objectFile() 進行加載. |
countByKey() | 僅適用於 (K, V) 類型的 RDD. 返回每個 key 的 value 數的一個 hashmap (K, int) pair. |
foreach(func) | 對數據集中的每個元素執行函數 func. 這通常用於更新一個 Accumulator 或與外部存儲系統交互時的副作用. 注意: 修改 foreach() 外的非 Accumulator 變量可能導致未定義的行爲. 更多細節請查看 Understanding closures. |
可以看出 action 的所有操作都是針對數據集中 “元素” (element) 級別的動作, action 的主要內容是 存儲 和 計算. 引用 《Learning Spark》 裏的說法:
Actions are operations that return a result to the driver program or write it to storage, and kick off a computation, such as count() and first().
哪些操作會導致shuffle?(也可以通過DAG圖進行觀看細節中是否帶有shuffle),參考[16][17]
1、repartition類的操作:比如repartition、repartitionAndSortWithinPartitions、coalesce等
2、byKey類的操作:比如reduceByKey、groupByKey、sortByKey等
3、join類的操作:比如join、cogroup等
重分區: 一般會shuffle,因爲需要在整個集羣中,對之前所有的分區的數據進行隨機,均勻的打亂,然後把數據放入下游新的指定數量的分區內
byKey類的操作:因爲你要對一個key,進行聚合操作,那麼肯定要保證集羣中,所有節點上的,相同的key,一定是到同一個節點上進行處理
join類的操作:兩個rdd進行join,就必須將相同join
key的數據,shuffle到同一個節點上,然後進行相同key的兩個rdd數據的笛卡爾乘積
排序
def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length): RDD[(K, V)]
def sortBy[K](f: (T) => K, ascending: Boolean = true, numPartitions: Int = this.partitions.length)(implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
集合或者表操作
def intersection(other: RDD[T]): RDD[T]
def intersection(other: RDD[T], partitioner: Partitioner)(implicit ord: Ordering[T] = null): RDD[T]
def intersection(other: RDD[T], numPartitions: Int): RDD[T]
def subtract(other: RDD[T], numPartitions: Int): RDD[T]
def subtract(other: RDD[T], p: Partitioner)(implicit ord: Ordering[T] = null): RDD[T]
def subtractByKey[W: ClassTag](other: RDD[(K, W)]): RDD[(K, V)]
def subtractByKey[W: ClassTag](other: RDD[(K, W)], numPartitions: Int): RDD[(K, V)]
def subtractByKey[W: ClassTag](other: RDD[(K, W)], p: Partitioner): RDD[(K, V)]
def join[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (V, W))]
def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
def join[W](other: RDD[(K, W)], numPartitions: Int): RDD[(K, (V, W))]
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
#----------------------------------------------------------參考文獻如下---------------------------------------------------------------------------------------------------
Reference:
[1]https://www.ssa.gov/oact/babynames/names.zip
[18]https://stackoverflow.com/questions/31386590/when-does-shuffling-occur-in-apache-spark?rq=1
[19]https://intellipaat.com/community/7852/what-are-the-spark-transformations-that-causes-a-shuffle
[14]『 Spark 』6. 深入研究 spark 運行原理之 job, stage, task
[15]py4j.protocol.Py4JJavaError: An error occurred while calling o90.save(官方bug,目前沒有解決,還沒寫完)
[16]spark shuffle特點和導致shuffle的算子