shuffle 的過程是在 MapTask 之後 reducerTask 之前的這麼一段對數據處理傳遞的過程
分區
mapTask 執行數據操作後, 將輸出數據存儲到 環形緩衝區 中, 當環形緩衝區內數據量達到最大量(默認 100M)的 80%時, 將內部數據溢寫到磁盤中存儲,然後環形緩衝區再進行反向寫入剩餘數據; 寫入磁盤時會對數據進行分區,默認分區爲 0(不分區),分區數,會影響最終reduceTask 寫入的文件數
使用場景-希望將分析後的數據根據不同的屬性, 寫到多個文件中
例如: 分析 2020 年疫情, 各省市區疫情情況, 將分析結果按照省份分別寫入不同的文件
-
自定義分區器
驅動類設置自定義分區器生效
job.setNumReduceTasks(3);//reduceTasks 數不能小於分區數 job.setPartitionerClass(CustomerPartitioner.class;
實現自定義分區器
- 繼承Partitioner抽象類
- 指定泛型 key- value 爲 mapper 輸出 key-value 類型
- 實現方法 getPartition 內部是分區邏輯
/** * <h3>study-all</h3> * * <p>自定義分區器實現</p> * * @Author zcz * @Date 2020-04-05 11:29 */ public class CustomerPartitioner extends Partitioner<Comparable, Writable> { @Override public int getPartition(WritableComparable key, Writable value, int i) { job.setNumReduceTasks(); job.setPartitionerClass(); //key 是 mapper 的輸出 key //value 是 mapper 的輸出 value //根據具體需求完成分區, //返回值是分區位置, 例如三個分區0,1,2 邏輯上應該是分區 0 的 return 0; 應該是分區 1 的 return 1; 應該是分區 2 的 return 2; return 0; } }
排序
-
分類
部分排序: 各分區進行排序
全排序: 保證最後輸出只有一個文件, 並且內部是有順序的
輔助排序(分組排序):在 reduce 階段, 對 key 進行一定規則的分組排序
二次排序: 排序方法 compareTo 中的判斷條件爲兩個以上
MapTask和 reduceTask 都會對輸入數據的 key 進行排序, 默認使用字典順序排序, 算法使用快排
-
MapReduce 過程中的排序
上面分區中描述到, mapTask將輸出數據寫入到 環形緩衝區 中, 當緩衝區中的數據達到閾值後, 會對緩衝區內的數據進行分區計算, 並進行分區內排序(默認字典順序快排), 然後將緩衝區內經過分區計算的數據溢寫到磁盤中存儲, 在 mapper 階段會有多個 MapTask執行, 所以會有多組分區, (可選操作)當所有 MapTask 執行完後, 將所有mapTask 的分區, 按各個分區規則進行歸併排序
mapTask執行結束後, reduceTask 階段會到 mapTask 下讀取各組分區數據到reduceTask 內存中, 當讀入的文件大小到達一定的閾值後, 會將文件溢寫到磁盤中, 當磁盤中的文件達到一定數量, 會對數據進行按分區歸併排序, 當所有數據拷貝完後,reduceTask 會將內存中和磁盤中的數據進行按分區歸併排序
-
自定義排序
首先說在排序中都是對輸入或輸出的 key 進行排序的, 所有 key 都需要是可排序的, 實現 WritableComparable接口, 實現 compareTo 方法, 就可以排序了
/** * <h3>study-all</h3> * <p>自定義可排序類</p> * * @Author zcz * @Date 2020-04-05 18:51 */ public class CustomerCompareBean implements WritableComparable<CustomerCompareBean> { private Integer column1; private Integer column2; public CustomerCompareBean(Integer column1, Integer column2) { this.column1 = column1; this.column2 = column2; } public CustomerCompareBean() { } public Integer getColumn1() { return column1; } public void setColumn1(Integer column1) { this.column1 = column1; } public Integer getColumn2() { return column2; } public void setColumn2(Integer column2) { this.column2 = column2; } @Override public void write(DataOutput out) throws IOException { out.writeInt(column1); out.writeInt(column2); } @Override public void readFields(DataInput in) throws IOException { this.column1 = in.readInt(); this.column2 = in.readInt(); } /** * 實現比較方法可進行排序 * */ @Override public int compareTo(CustomerCompareBean o) { return (this.column1 != o.getColumn1())? (this.column1 - o.getColumn1()): (this.column2 - o.getColumn2()); } }
-
全排序: 只需要 mapper 輸出 key 和 reducer 讀取 key 可排序
-
分區排序: 在全排序的基礎上, 增加分區設置後可分區排序(自定義分區見分區章節)
-
分組排序
-
自定義分組排序類CustomerGroupingComparator繼承 WritableComparator,
-
重寫compare 方法
@Override public int compare(WritableComparable a, WritableComparable b) { // 比較的業務邏輯 return result; }
-
重寫無參構造方法, 調用 super(OrderBean.class, , true)
protected OrderGroupingComparator() { super(OrderBean.class, true); }
例:
public class CustomerGroupingComparator extends WritableComparator { //重寫無參構造 protected OrderGroupingComparator() { super(OrderBean.class, true); } @Override public int compare(WritableComparable a, WritableComparable b) { //a與b的比較邏輯 return result; } }
Driver 設置分組排序
job.setGroupingComparatorClass(OrderGroupingComparator.class);
-
Combiner 合併
-
Combiner 不是必須的
-
Combiner 本質就是 reducer的子類
-
Combiner 和 reducer 的區別是執行位置:
Combiner 在每個 MapTask 後面執行
reducer 是在所有 MapTask 後面執行
-
Combiner 的意義是爲每個 MapTask 的輸出進行彙總, 減少reducerTask讀取數據的網絡傳輸量
-
使用 Combiner 不能影響最終結果
-
Combiner 的key-value輸入類型是Mapper的輸出類型, Combiner 的輸出類型是 Reducer 的輸出類型
自定義 Combiner
/**
* <h3>study-all</h3>
*
* <p>自定義 Combiner</p>
*
* @Author zcz
* @Date 2020-04-05 19:27
*/
public class CustomerCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable value = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable i : values) {
sum = sum+ i.get();
}
value.set(sum);
context.write(key, value);
}
}
Driver 中設置啓動 Combiner
conf.setConbinerClass(CustomerCombiner.class);
Shuffle 過程總結
- MapTask 對數據進行讀取, 並操作數據對數據進行整理後, 將整理後的數據調用 Partitioner 進行數據分區, 將分區後的數據寫入環形緩衝區環形緩衝區內對數據分區排序, 並對區內部數據進行以 key 值排序(快排)
- 當數據緩衝區內數據達到閾值(100M*80%), 如果設置了 Combiner, 先對分區數據執行 Combiner 合併, 然後將各分區合併後的數據溢寫到磁盤(分區數據, 區內有序)
- 如果有需要可以對寫入到磁盤的分區數據進行 Combiner 操作, 並執行歸併排序後再寫入磁盤
- 將分區數據的元信息寫到內存索引數據結構SpillRecord中,其中每個分區的元信息包括在臨時文件中的偏移量、壓縮前數據大小和壓縮後數據大小。如果當前內存索引大小超過1MB,則將內存索引寫到文件output/spillN.out.index中
- 如果設置了 Combiner , mapTask對所有數據進行一次 Combiner 合併, 保證每個 mapTask 只生成一個文件
- mapTask 全部結束後開始啓動執行 reducerTask
- reducerTask將 mapTask生成的數據拷貝到 reducerTask 執行節點內存中,如果拷貝數據超過閾值, 將數據寫到磁盤中
- reducerTask 將內存中和磁盤中的數據進行一次合併, 防止內存中數據過多, 和防止磁盤中文件過多
- reducerTask 將數據進行一次全局排序(歸併)
- shuffle 過程結束, reducer 將處理數據寫到 HDFS