shuffle過程
shuffle概念
shuffle的本意是洗牌、混洗的意思,把一組有規(guī)則的數(shù)據(jù)盡量打亂成無規(guī)則的數(shù)據(jù)。而在MapReduce中,shuffle更像是洗牌的逆過程,指的是將map端的無規(guī)則輸出按指定的規(guī)則“打亂”成具有一定規(guī)則的數(shù)據(jù),以便reduce端接收處理。其在MapReduce中所處的工作階段是map輸出后到reduce接收前,具體可以分為map端和reduce端前后兩個(gè)部分。在shuffle之前,也就是在map階段,MapReduce會(huì)對(duì)要處理的數(shù)據(jù)進(jìn)行分片(split)操作,為每一個(gè)分片分配一個(gè)MapTask任務(wù)。接下來map()函數(shù)會(huì)對(duì)每一個(gè)分片中的每一行數(shù)據(jù)進(jìn)行處理得到鍵值對(duì)(key,value),其中key為偏移量,value為一行的內(nèi)容。此時(shí)得到的鍵值對(duì)又叫做“中間結(jié)果”。此后便進(jìn)入shuffle階段,由此可以看出shuffle階段的作用是處理“中間結(jié)果”。
block塊(物理劃分)
block是HDFS中的基本存儲(chǔ)單位,hadoop1.x默認(rèn)大小為64
M而hadoop2.x默認(rèn)塊大小為128
M。文件上傳到HDFS,就要?jiǎng)澐謹(jǐn)?shù)據(jù)成塊,這里的劃分屬于物理的劃分(實(shí)現(xiàn)機(jī)制也就是設(shè)置一個(gè)read
方法,每次限制最多讀128M
的數(shù)據(jù)后調(diào)用write
進(jìn)行寫入到hdfs
),塊的大小可通過 dfs.block.size
配置。block采用冗余機(jī)制保證數(shù)據(jù)的安全:默認(rèn)為3份,可通過dfs.replication配置。注意:當(dāng)更改塊大小的配置后,新上傳的文件的塊大小為新配置的值,以前上傳的文件的塊大小為以前的配置值
split分片(邏輯劃分)
Hadoop中split
劃分屬于邏輯
上的劃分,目的只是為了讓map task
更好地獲取數(shù)據(jù)。split是通過hadoop中的InputFormat
接口中的getSplit()
方法得到的。那么,split的大小具體怎么得到呢?
首先介紹幾個(gè)數(shù)據(jù)量:
totalSize:整個(gè)mapreduce job輸入文件的總大小。
numSplits:來自job.getNumMapTasks(),即在job啟動(dòng)時(shí)用戶利用 org.apache.hadoop.mapred.JobConf.setNumMapTasks(int n)設(shè)置的值,從方法的名稱上看,是用于設(shè)置map的個(gè)數(shù)。但是,最終map的個(gè)數(shù)也就是split的個(gè)數(shù)并不一定取用戶設(shè)置的這個(gè)值,用戶設(shè)置的map個(gè)數(shù)值只是給最終的map個(gè)數(shù)一個(gè)提示,只是一個(gè)影響因素,而不是決定因素。
goalSize:totalSize/numSplits,即期望的split的大小,也就是每個(gè)mapper處理多少的數(shù)據(jù)。但也僅僅是期望。
minSize:split的最小值,該值可由兩個(gè)途徑設(shè)置:
1.通過子類重寫方法protected void setMinSplitSize(long minSplitSize)進(jìn)行設(shè)置。一般情況為1,特殊情況除外
2.通過配置文件中的mapred.min.split.size進(jìn)行設(shè)置
3.最終取兩者中的最大值!
split計(jì)算公式:finalSplitSize=max(minSize,min(goalSize,blockSize))
shuffle流程概括
因?yàn)轭l繁的磁盤I/O操作會(huì)嚴(yán)重的降低效率,因此“中間結(jié)果”不會(huì)立馬寫入磁盤,而是優(yōu)先存儲(chǔ)到map節(jié)點(diǎn)的“環(huán)形內(nèi)存緩沖區(qū)”
,在寫入的過程中進(jìn)行分區(qū)
(partition),也就是對(duì)于每個(gè)鍵值對(duì)來說,都增加了一個(gè)partition屬性值
,然后連同鍵值對(duì)一起序列化成字節(jié)數(shù)組寫入到緩沖區(qū)
(緩沖區(qū)采用的就是字節(jié)數(shù)組,默認(rèn)大小為100M
)。當(dāng)寫入的數(shù)據(jù)量達(dá)到預(yù)先設(shè)置的闕值后(mapreduce.map.io.sort.spill.percent
,默認(rèn)0.80,或者80%)便會(huì)啟動(dòng)溢寫出線程
將緩沖區(qū)
中的那部分?jǐn)?shù)據(jù)溢出寫(spill)到磁盤
的臨時(shí)文件中,并在寫入前根據(jù)key進(jìn)行排序
(sort)和合并
(combine,可選操作)。溢出寫過程按輪詢方式將緩沖區(qū)中的內(nèi)容寫到mapreduce.cluster.local.dir屬性指定的目錄中。當(dāng)整個(gè)map任務(wù)完成溢出寫后,會(huì)對(duì)磁盤中這個(gè)map任務(wù)產(chǎn)生的所有臨時(shí)文件(spill文件
)進(jìn)行歸并
(merge)操作生成最終的正式輸出文件,此時(shí)的歸并是將所有spill文件中的相同partition合并到一起,并對(duì)各個(gè)partition中的數(shù)據(jù)再進(jìn)行一次排序(sort),生成key和對(duì)應(yīng)的value-list,文件歸并時(shí),如果溢寫文件數(shù)量超過參數(shù)min.num.spills.for.combine
的值(默認(rèn)為3)時(shí),可以再次進(jìn)行合并
。至此,map端shuffle過程結(jié)束,接下來等待reduce task來拉取數(shù)據(jù)。對(duì)于reduce端的shuffle過程來說,reduce task在執(zhí)行之前的工作就是不斷地拉取當(dāng)前job里每個(gè)map task的最終結(jié)果,然后對(duì)從不同地方拉取過來的數(shù)據(jù)不斷地做merge最后合并成一個(gè)分區(qū)相同的大文件,然后對(duì)這個(gè)文件中的鍵值對(duì)按照key
進(jìn)行sort排序
,排好序之后緊接著進(jìn)行分組
,分組完成后才將整個(gè)文件
交給reduce task
處理
shuffle詳細(xì)流程
Map端shuffle
①分區(qū)partition
②寫入環(huán)形內(nèi)存緩沖區(qū)
③執(zhí)行溢出寫
- 排序sort(根據(jù)key)--->合并combiner--->生成溢出寫文件
④歸并merge(也涉及到key的sort及combiner)
① 分區(qū)Partition
在將map()
函數(shù)處理后得到的(key,value)
對(duì)寫入到緩沖區(qū)之前,需要先進(jìn)行分區(qū)
操作,這樣就能把map任務(wù)處理的結(jié)果發(fā)送給指定的reducer去執(zhí)行,從而達(dá)到負(fù)載均衡,避免數(shù)據(jù)傾斜。MapReduce提供默認(rèn)的分區(qū)類(HashPartitioner),其核心代碼如下:
public class HashPartitioner<K, V> extends Partitioner<K, V> {
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K key, V value,
int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
②寫入環(huán)形內(nèi)存緩沖區(qū)
因?yàn)轭l繁的磁盤I/O操作會(huì)嚴(yán)重的降低效率,因此“中間結(jié)果”不會(huì)立馬寫入磁盤,而是優(yōu)先存儲(chǔ)到map節(jié)點(diǎn)的“環(huán)形內(nèi)存緩沖區(qū)
”,并做一些預(yù)排序
以提高效率,當(dāng)寫入的數(shù)據(jù)量達(dá)到預(yù)先設(shè)置的闕值后便會(huì)執(zhí)行一次I/O操作將數(shù)據(jù)寫入到磁盤。每個(gè)map任務(wù)
都會(huì)分配一個(gè)環(huán)形內(nèi)存緩沖區(qū)
,用于存儲(chǔ)map任務(wù)輸出的鍵值對(duì)(默認(rèn)大小100
MB,mapreduce.task.io.sort.mb調(diào)整)以及對(duì)應(yīng)的partition
,被緩沖的(key,value)對(duì)
已經(jīng)被序列化(為了寫入磁盤)
- 個(gè)人總結(jié):寫入緩沖區(qū)前,先partion->根據(jù)partion及key排序預(yù)排序后->寫入磁盤中
③執(zhí)行溢寫出
一旦緩沖區(qū)
內(nèi)容達(dá)到閾值
(mapreduce.map.io.sort.spill.percent,默認(rèn)0.80
,或者80%),就會(huì)會(huì)鎖定這80%的內(nèi)存,并在每個(gè)分區(qū)
中對(duì)其中的鍵值
對(duì)按鍵進(jìn)行sort排序
,具體是將數(shù)據(jù)按照partition和key
兩個(gè)關(guān)鍵字進(jìn)行排序,排序結(jié)果為緩沖區(qū)內(nèi)的數(shù)據(jù)按照partition為單位聚集在一起,同一個(gè)partition內(nèi)的數(shù)據(jù)按照key有序。排序完成后會(huì)創(chuàng)建一個(gè)溢出寫文件
(臨時(shí)文件),然后開啟一個(gè)后臺(tái)線程
把這部分?jǐn)?shù)據(jù)以一個(gè)臨時(shí)文件的方式溢出寫(spill
)到本地磁盤
中(如果客戶端自定義了Combiner
(相當(dāng)于map階段的reduce),則會(huì)在分區(qū)排序后到溢寫出前自動(dòng)調(diào)用combiner
,將相同的key的value相加
,這樣的好處就是減少溢寫到磁盤的數(shù)據(jù)量。這個(gè)過程叫“合并
”)。剩余的20%的內(nèi)存在此期間可以繼續(xù)寫入map輸出的鍵值對(duì)。溢出寫過程按輪詢方式將緩沖區(qū)中的內(nèi)容寫到mapreduce.cluster.local.dir屬性指定的目錄中
合并Combiner
如果指定了Combiner,可能在兩個(gè)地方被調(diào)用:
1.當(dāng)為作業(yè)設(shè)置Combiner類后,緩存溢出
線程將緩存存放到磁盤時(shí),就會(huì)調(diào)用;
2.緩存溢出的數(shù)量超過mapreduce.map.combine.minspills(默認(rèn)3)時(shí),在緩存溢出文件合并
的時(shí)候會(huì)調(diào)用
- 合并(Combine)和歸并(Merge)的區(qū)別:
兩個(gè)鍵值對(duì)<“a”,1>和<“a”,1>,如果合并,會(huì)得到<“a”,2>,如果歸并,會(huì)得到<“a”,<1,1>>
特殊情況:當(dāng)數(shù)據(jù)量很小,達(dá)不到緩沖區(qū)闕值時(shí),怎么處理?
對(duì)于這種情況,目前看到有兩種不一樣的說法:
①不會(huì)有寫臨時(shí)文件到磁盤的操作,也不會(huì)有后面的合并。
②最終也會(huì)以臨時(shí)文件的形式存儲(chǔ)到本地磁盤
至于真實(shí)情況是怎么樣的,該文章的大佬說他也不清楚。。。
④歸并merge
當(dāng)一個(gè)map task處理的數(shù)據(jù)很大,以至于超過緩沖區(qū)內(nèi)存時(shí),就會(huì)生成多個(gè)spill文件。此時(shí)就需要對(duì)同一個(gè)map任務(wù)產(chǎn)生的多個(gè)spill文件
進(jìn)行歸并
生成最終的一個(gè)已分區(qū)且已排序的大文件。配置屬性mapreduce.task.io.sort.factor控制著一次最多能合并多少流,默認(rèn)值是10。這個(gè)過程包括排序和合并
(可選),歸并得到的文件內(nèi)鍵值對(duì)有可能擁有相同的key,這個(gè)過程如果client設(shè)置過Combiner,也會(huì)合并相同的key值的鍵值對(duì)
(根據(jù)上面提到的combine的調(diào)用時(shí)機(jī)可知)。
溢出寫文件歸并完畢后,Map將刪除所有的臨時(shí)溢出寫文件,并告知NodeManager任務(wù)已完成,只要其中一個(gè)MapTask完成,ReduceTask就開始復(fù)制它的輸出(Copy階段分區(qū)輸出文件通過http的方式提供給reducer)
壓縮
寫磁盤時(shí)壓縮map端的輸出,因?yàn)檫@樣會(huì)讓寫磁盤的速度更快,節(jié)約磁盤空間,并減少傳給reducer的數(shù)據(jù)量。默認(rèn)情況下,輸出是不壓縮的(將mapreduce.map.output.compress設(shè)置為true即可啟動(dòng))
Reduce端shuffle
①復(fù)制copy
②歸并merge
③reduce
結(jié)合下面這張圖可以直觀感受reduce端的shuffle過程
①復(fù)制copy
Reduce進(jìn)程
啟動(dòng)一些數(shù)據(jù)copy線程
,通過HTTP
方式請(qǐng)求MapTask所在的NodeManager以獲取輸出文件。
NodeManager需要為分區(qū)文件運(yùn)行reduce任務(wù)。并且reduce任務(wù)需要集群上若干個(gè)map任務(wù)的map輸出作為其特殊的分區(qū)文件。而每個(gè)map任務(wù)的完成時(shí)間可能不同,因此只要有一個(gè)任務(wù)完成,reduce任務(wù)就開始復(fù)制其輸出
reduce任務(wù)有少量復(fù)制線程,因此能夠并行取得map輸出。默認(rèn)線程數(shù)為5,但這個(gè)默認(rèn)值可以通過mapreduce.reduce.shuffle.parallelcopies屬性進(jìn)行設(shè)置。
【Reducer如何知道自己應(yīng)該處理哪些數(shù)據(jù)呢?】
因?yàn)镸ap端進(jìn)行partition的時(shí)候,實(shí)際上就相當(dāng)于指定了每個(gè)Reducer要處理的數(shù)據(jù)(partition就對(duì)應(yīng)了Reducer),所以Reducer在拷貝數(shù)據(jù)的時(shí)候只需拷貝與自己對(duì)應(yīng)的partition中的數(shù)據(jù)即可。每個(gè)Reducer會(huì)處理一個(gè)或者多個(gè)partition。
【reducer如何知道要從哪臺(tái)機(jī)器上去的map輸出呢?】
map任務(wù)完成后,它們會(huì)使用心跳機(jī)制通知它們的application master、因此對(duì)于指定作業(yè),application master知道m(xù)ap輸出和主機(jī)位置之間的映射關(guān)系。reducer中的一個(gè)線程定期詢問master以便獲取map輸出主機(jī)的位置。知道獲得所有輸出位置
②歸并merge
Copy 過來的數(shù)據(jù)會(huì)先放入內(nèi)存緩沖區(qū)中,這里的緩沖區(qū)大小要比 map 端的更為靈活,它基于 JVM 的 heap size 設(shè)置,因?yàn)?Shuffle 階段 Reducer 不運(yùn)行,所以應(yīng)該把絕大部分的內(nèi)存都給 Shuffle 用。
Copy過來的數(shù)據(jù)會(huì)先放入內(nèi)存緩沖區(qū)中,如果內(nèi)存緩沖區(qū)中能放得下這次數(shù)據(jù)的話就直接把數(shù)據(jù)寫到內(nèi)存中,即內(nèi)存到內(nèi)存merge。Reduce要向每個(gè)Map去拖取數(shù)據(jù),在內(nèi)存中每個(gè)Map對(duì)應(yīng)一塊數(shù)據(jù),當(dāng)內(nèi)存緩存區(qū)中存儲(chǔ)的Map數(shù)據(jù)占用空間達(dá)到一定程度的時(shí)候,開始啟動(dòng)內(nèi)存中merge,把內(nèi)存中的數(shù)據(jù)merge輸出到磁盤上一個(gè)文件中,即內(nèi)存到磁盤merge。與map端的溢寫類似,在將buffer中多個(gè)map輸出合并寫入磁盤之前,如果設(shè)置了Combiner,則會(huì)化簡壓縮合并的map輸出。Reduce的內(nèi)存緩沖區(qū)可通過mapred.job.shuffle.input.buffer.percent配置,默認(rèn)是JVM的heap size的70%。內(nèi)存到磁盤merge的啟動(dòng)門限可以通過mapred.job.shuffle.merge.percent配置,默認(rèn)是66%
當(dāng)屬于該reducer的map輸出全部拷貝完成,則會(huì)在reducer上生成多個(gè)文件(如果拖取的所有map數(shù)據(jù)總量都沒有內(nèi)存緩沖區(qū),則數(shù)據(jù)就只存在于內(nèi)存中),這時(shí)開始執(zhí)行合并
操作,即磁盤到磁盤merge
,Map的輸出數(shù)據(jù)已經(jīng)是有序的,Merge進(jìn)行一次合并排序
,所謂Reduce端的sort過程就是這個(gè)合并
的過程,采取的排序方法跟map階段不同,因?yàn)槊總€(gè)map端傳過來的數(shù)據(jù)是排好序的,因此眾多排好序的map輸出文件在reduce端進(jìn)行合并時(shí)采用的是歸并排序
(MAP端shaffer 是堆排序),針對(duì)鍵進(jìn)行歸并排序。一般Reduce是一邊copy一邊sort,即copy和sort兩個(gè)階段是重疊而不是完全分開的。最終Reduce shuffle過程會(huì)輸出一個(gè)整體有序的數(shù)據(jù)塊
③reduce
當(dāng)一個(gè)reduce任務(wù)完成全部的復(fù)制和排序后,就會(huì)針對(duì)已根據(jù)鍵排好序的Key構(gòu)造對(duì)應(yīng)的Value迭代器。這時(shí)就要用到分組,默認(rèn)的根據(jù)鍵分組,自定義的可是使用 job.setGroupingComparatorClass()
方法設(shè)置分組函數(shù)類。對(duì)于默認(rèn)分組來說,只要這個(gè)比較器比較的兩個(gè)Key相同,它們就屬于同一組,它們的 Value就會(huì)放在一個(gè)Value迭代器
,而這個(gè)迭代器的Key使用屬于同一個(gè)組的所有Key的第一個(gè)Key。
在reduce階段,reduce()方法的輸入是所有的Key和它的Value迭代器。此階段的輸出直接寫到輸出文件系統(tǒng),一般為HDFS。如果采用HDFS,由于NodeManager也運(yùn)行數(shù)據(jù)節(jié)點(diǎn),所以第一個(gè)塊副本將被寫到本地磁盤。
1、當(dāng)reduce將所有的map上對(duì)應(yīng)自己partition的數(shù)據(jù)下載完成后,reducetask真正進(jìn)入reduce函數(shù)的計(jì)算階段。由于reduce計(jì)算時(shí)同樣是需要內(nèi)存作為buffer,可以用mapreduce.reduce.input.buffer.percent(default 0.0)(源代碼MergeManagerImpl.java:674行)來設(shè)置reduce的緩存。
這個(gè)參數(shù)默認(rèn)
情況下為0
,也就是說,reduce是全部從磁盤開始讀處理數(shù)據(jù)。如果這個(gè)參數(shù)大于0,那么就會(huì)有一定量的數(shù)據(jù)被緩存在內(nèi)存并輸送給reduce,當(dāng)reduce計(jì)算邏輯消耗內(nèi)存很小時(shí),可以分一部分內(nèi)存用來緩存數(shù)據(jù),可以提升計(jì)算的速度。所以默認(rèn)情況下都是從磁盤讀取數(shù)據(jù),如果內(nèi)存足夠大的話,務(wù)必設(shè)置該參數(shù)讓reduce直接從緩存讀數(shù)據(jù),這樣做就有點(diǎn)Spark Cache的感覺。
2、Reduce在這個(gè)階段,框架為已分組的輸入數(shù)據(jù)中的每個(gè)鍵值對(duì)對(duì)調(diào)用一次 reduce(WritableComparable,Iterator, OutputCollector, Reporter)方法。Reduce任務(wù)的輸出通常是通過調(diào)用 OutputCollector.collect(WritableComparable,Writable)寫入文件系統(tǒng)的。