實際工作中的 Map Reduce優化

接觸Hadoop體系,在實際工作中使用MapReduce編程,已經有一段時間.
但最近個工作重心在實時ETL開發, 對MapReduce編程接觸較少,擔心時間久了會將過去的經驗遺忘.
就將之前工作中應用過的MapReduce開發的優化總結一下,用作備忘.

之間工作中應用的Hadoop集羣內存資源在1~10T(各省的集羣資源不同), 每天處理的數據量在幾十T,甚至上百T
希望這種大規模數據的處理經驗也能對大家有所幫助

本篇博客主要要點如下 :

一.MapReduce編程優化

二.參數調優

MapReduce編程優化

Map端編程優化
小文件合併
Hadoop的框架設計,決定了它適合處理少量的大文件而不是大量的小文件.
大量的小文件會產生大量的map任務(默認場景下一個小文件對應一個map任務),增大map任務裝載次數,這裏消耗大量時間將嚴重影響MR程序執行效率
這種情形下,我們使用CombineTextInputFormat來替代默認的TextInputFormat.
CombineTextInputFormat可以將多個小文件從邏輯上規劃到一個切片中
這樣,多個小文件就可以交給一個maptask進行處理

實現如下:

    job.setInputFormatClass(CombineTextInputFormat.class)
    CombineTextInputFormat.setMaxInputSplitSize(job, 2147483648);    // 2G
    CombineTextInputFormat.setMinInputSplitSize(job, 1073741824);    // 1G 
以上的代碼表示 : 每個切片將處理1G~2G的數據量, 這裏因爲我們處理數據量大,所以切片設置的比較大, 具體大小要根據實際情況進行配置
多個數據輸入路徑
嚴格的講,這並不算是MR優化,但是實際需求裏,我們同一個Map的數據源往往不止一種,這種情形下就需要採用下面的方式來處理多種數據源
實現如下:
MultipleInputs.addInputPath(job, new Path("inpath1"), CombineTextInputFormat.class, Class<? extends Mapper>);
MultipleInputs.addInputPath(job, new Path("inpath2"), CombineTextInputFormat.class, Class<? extends Mapper>);
儘可能複用對象

以Hadoop2.9.2官網文檔的WordCount的Map過程舉例

hadoop2.9.2官方文檔鏈接 :

public static class TokenizerMapper
       extends Mapper<Object, Text, Text, IntWritable>{

    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();

    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
      }
    }
  }

上面官網的代碼一目瞭然,但實際開發中很多程序員往往採用下面的寫法

public static class TokenizerMapper
       extends Mapper<Object, Text, Text, IntWritable>{
    private Text word = new Text();

    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, new IntWritable(1));
      }
    }
  }

即 : 本來方法裏只需要創建一次的IntWritable對象,變成了每次寫入的時候都創建該對象,
效率上,和程序穩定性的表現將會是天壤之別!

合理運用Combine過程
Mapreduce中的Combiner就是爲了減少map任務和reduce任務之間的數據傳輸而設置的,Hadoop允許用戶針對map task的輸出指定一個合併函數。即爲了減少傳輸到Reduce中的數據量。它主要是爲了削減Mapper的輸出從而減少網絡帶寬和Reducer之上的負載。

Combine實現,依舊以wordCount舉例:
  public static class WordCountCombine
       extends Reducer<Text,IntWritable,Text,IntWritable> {
    private IntWritable result = new IntWritable();

    public void reduce(Text key, Iterable<IntWritable> values,
                       Context context
                       ) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }

從上面的寫法中可以看到 : 除了類名,剩下的和Reduce完全一致

需要注意的是,並不是所有的場景都需要使用Combine過程
Combiner 能夠應用的前提是不能影響最終的業務邏輯
而且,Combiner 的輸出 kv類型要對應reducer的輸入kv 
使用分佈式緩存DistributedCache

執行MapReduce時,可能Mapper之間需要共享一些信息
若信息量不大,可以使用分佈式緩存將其從HDFS加載到內存中,以此提高程序性能

分佈式緩存使用示例:
1 在main方法中加載共享文件的HDFS路徑,路徑可以是目錄也可以是文件。在map階段使用別名需要在路徑末尾追加“#”+別名

String cache = “hdfs://10.105.xxx.xxx:8082/cache/file”;//目錄或文件
cache = cache + “#myfile”;//myfile是文件的別名
job.addCacheFile(new Path(cache),toUri().conf);//添加到job設置

2 在Mapper類或Reduce的setup方法中,用輸入流獲取分佈式緩存的文件
在mapper類中的setup方法

protected void setup(Context context)throws IOException,
          interruptedException{
        FileReader reader = new FileReader("myfile");//路徑參數爲別名
        BufferedReader br = new BufferedReader(reader);
        ......
        }

此方法只執行一次,在map方法執行之前,每個從節點各自都緩存一份相同的共享數據。若共享數據太大,可以將共享數據分批緩存,重複執行作業。

Shuffle端編程優化

官網給出的Shuffle過程如下圖 :

TIM圖片20190811100210.png

Shuffle過程橫跨Map,Reduce,實際生產中的MR程序出現的問題大多數都是由於Shuffle階段不合理導致的
所以,這部分的優化是實際開發中的重點

自定義分區
MR執行過程中,往往會出現數據傾斜的情況,即存在某個key或者某些key數據量過大
具體表現爲 : 大多數ReduceTask都已經執行完畢,但有一個或者是幾個遲遲不能跑完
導致集羣資源不能被有效利用,程序運行效率嚴重低下,
甚至到最後報Out Of Memory 或者GC overhead limit exceeded這種問題導致程序失敗

如果沒有自定義的 partitioning,則默認的 partition 算法,即根據每一條數據的 key
的 hashcode 值摸運算(%)reduce 的數量,得到的數字就是“分區號“。所以,某個key數據量過大的時候,就會導致數據傾斜
這種問題,很多情況下,是可以通過合理的自定義分區規避掉的
自定義分區需要繼承Partitioner類,並實現裏面的getPartition方法

下面是我們應用的一個實例 : 我們業務場景裏有一個關鍵字段幾乎是隨機的
用這個字段的hash值對  reduce的數量取餘,就能基本上保證每個reduce處理的數據量均勻
具體的自定義分區方式要根據業務場景靈活處理
job.setPartitionerClass(PartitionerTest.class); // 設置自定義分區

public static class PartitionerTest extends Partitioner<CellTimeKey, Text> 
	{
	
		@Override
		public int getPartition(CellTimeKey key, Text value, int numOfReducer)
		{
			return Math.abs(String.valueOf(key.getEci()).hashCode() % numOfReducer);
		}
	}
自定義分組
舉例 : 假設我每個reduce需要處理的數據是某個key一天的數據量
但是我實際處理的時候想要針對這個key 10分鐘的數據進行更精細化的處理
這個時候,就是自定義分組大顯身手的時候

自定義分組需要WritableComparator,重寫裏面的compare方法
	job.setGroupingComparatorClass(CellSortKeyGroupComparator.class); // 設置自定義分組

自定義分組實現 :

	public static class CellSortKeyGroupComparator extends WritableComparator
	{
		@Override
		public int compare(WritableComparable a, WritableComparable b)
		{
			CellTimeKey s1 = (CellTimeKey) a;
			CellTimeKey s2 = (CellTimeKey) b;

			if (s1.getEci() > s2.getEci())
			{
				return 1;
			}
			else if (s1.getEci() < s2.getEci())
			{
				return -1;
			}
			else
			{
				if (s1.getTimeSpan() > s2.getTimeSpan())
				{
					return 1;
				}
				else if (s1.getTimeSpan() < s2.getTimeSpan())
				{
					return -1;
				}
				return 0;
			}
		}
	}

上面的代碼主要是二次排序 : 先根據key, 然後根據時間(10分鐘內的數據timeSpan的值相同)
基於此,我們就實現了對每個key, 10分鐘的數據爲一組的初衷

自定義排序
如果在自定義分組的基礎上,我們想對數據進行進一步精細化的處理
比如說,在上面的自定義分組過程中,我們實現了 : 每個key, 10分鐘的數據爲一組
但是, 這個key, 10分鐘的數據 分爲了多種數據源~ 每種數據源的先後處理順序必須要嚴格指定
這種情況下,就需要用到自定義排序來進行進一步細分
job.setSortComparatorClass(類名.class); // 設置自定義排序

自定義排序的實現和自定義分組實現相同,只不過是compare方法裏的比較粒度更加精細化,所以就不過多贅述

通過以上,我們就基本上保證reduce階段處理數據是按照我們期望的方式進行
Reduce端編程優化
合理設置reduce個數
reduce個數設置太少,會導致每個task處理的數據量過大,嚴重時會導致Out Of Memory
同時,個數設置過少也可能導致集羣資源利用不夠充分,導致程序效率降低
reduce個數設置太多,會導致程序在啓動reduce任務上面消耗過多資源,使程序執行效率降低
    job.setNumReduceTasks(reduceNum);  //設置reduce數量方法
我在實際的開發過程中,使用下面的方式來設置reduce的數量:
首先對輸入路徑進行遍歷
對每一個文件,使用FileStatus getLen()方法獲取文件大小
通過此方式得到總的文件大小
接下來,用總文件大小 / 每個reduce處理的數據量, 來得到reduce數量的大小

通過此方式,保證了reduce數量基本上是合理的
既能保證資源的利用,又能保證程序執行的效率和穩定性
對中間結果進行壓縮以減少資源佔用
MR執行的過程中,會有大量的中間結果落地磁盤上,
在磁盤空間不是很充裕的情況下,就需要對中間結果進行壓縮處理以減少資源佔用
我們對中間結果採用的是LZO的壓縮方式
設置方式如下:
    conf.set("io.compression.codecs",
			"org.apache.hadoop.io.compress.BZip2Codec,org.apache.hadoop.io.compress.DefaultCodec,org.apache.hadoop.io.compress.DeflateCodec,org.apache.hadoop.io.compress.GzipCodec,org.apache.hadoop.io.compress.Lz4Codec,org.apache.hadoop.io.compress.SnappyCodec,com.hadoop.compression.lzo.LzoCodec,com.hadoop.compression.lzo.LzopCodec");
	conf.set("mapreduce.map.output.compress", "LD_LIBRARY_PATH=/usr/local/hadoop/lzo/lib");
	conf.set("mapreduce.map.output.compress", "true");
	conf.set("mapreduce.map.output.compress.codec", "com.hadoop.compression.lzo.LzoCodec");

注意: LZO壓縮方式並不是拿來即用的,是需要集羣做好相關的配置之後纔可以使用!!

規避不必要的Reduce過程
其實Reduce階段並不是必須的
比如 : 
1. 使用bulkload方式對HBase數據進行批量入庫的時候,就不需要我們提供reduce過程 (這是僞規避,因爲實際上使用的是HBase提供的reduce過程)
2. 在Mapping階段就可以將數據落地的情形(比如單純的數據過濾,或者是一些簡單的數據轉換,提取)

規避使用 reduce,就可以避免Reduce 在連接數據集的時候產生大量的網絡消耗,提高程序運行效率

參數優化

針對Map Reduce的參數,官方文檔提供的很詳細,但有很多我們實際上是用不到的,還有一些可能自己理解也不夠深刻
下面介紹的這些參數,以及參數配置,都是實際生產中真正使用到,對程序執行確實起到幫助的參數

隊列設置

hadoop程序執行時,默認選用default隊列, 但更多場景下,我們需要使用指定的隊列,設置方法如下:
conf.set("mapreduce.job.queuename", queueName);
官方默認的map,reduce資源配置,往往不能夠滿足需求,需要我們自行設置

Map Reduce資源設置

//一個 Map Task 可使用的資源上限(單位:MB),默認爲 1024。如果 Map Task 實際使用的資源量超過該值,則會被強制殺死。
// 因爲我們map,reduce處理的數據量,和集羣資源都還比較大,所以這裏設置8GB,這裏要根據具體情況而定
conf.set("mapreduce.map.memory.mb", "8192"); 
// 一個 Reduce Task 可使用的資源上限(單位:MB),默認爲 1024。如果 Reduce Task實際使用的資源量超過該值,則會被強制殺死。
conf.set("mapreduce.reduce.memory.mb", "8192");
//每個 Map task 可使用的最多 cpu core 數目,默認值: 1
conf.set("mapreduce.map.cpu.vcores", "2");   
//每個 Reduce task 可使用的最多 cpu core 數目,默認值: 1
conf.set("mapreduce.reduce.cpu.vcores", "2"); 

shuffle優化參數

shuffle階段作爲MR過程中的重要一步,針對shuffle階段的參數優化也是十分必要的
//  shuffle 的環形緩衝區大小,默認 100m
// 我們shuffle階段操作的數據量還是比較大的,所以這裏設置的較大寫,1個G
conf.set("mapreduce.task.io.sort.mb", "1024");
// 一個單一的shuffle的最大內存使用限制, 默認值爲0.25
conf.set("mapreduce.reduce.shuffle.memory.limit.percent", "0.1");

容錯機制

很多時候,程序會因爲map,reduce階段的個別task失敗,導致程序終止
這個時候,我們就需要根據業務場景,來進行適當的容錯
// map階段允許失敗的比例,默認0
// 因爲我們的業務需求,要求的是數據的整體結果比較準確即可,並不要求所有的數據必須完全準確,所以設置了允許5%的任務失敗
conf.set("mapred.max.map.failures.percent", "5");
//reduce階段允許失敗的比例,默認0
conf.set("mapred.max.reduce.failures.percent", "5");
//每個 Map Task 最大重試次數,一旦重試參數超過該值,則認爲 Map Task 運行失敗,默認值:4
conf.set("mapreduce.map.maxattempts","6")
// 每個 Reduce Task 最大重試次數,一旦重試參數超過該值,則認爲 Map Task 運行失敗,默認值:4。
conf.set("mapreduce.reduce.maxattempts","6")
/*
    Task 超時時間
    該參數表達的意思爲:如果一個 task 在一定時間內沒
    有任何進入,即不會讀取新的數據,也沒有輸出
    數據,則認爲該 task 處於 block 狀態,可能是卡
    住了,也許永遠會卡主,爲了防止因爲用戶程序
    永遠 block 住不退出,則強制設置了一個該超時時
    間(單位毫秒),默認是 600000。
    因爲我的程序對每條輸入數據的處理時間過長(比如會訪問數
    據庫,通過網絡拉取數據等),所以將該參數設置的比較大,爲1個小時
    
    該參數過小常出現的錯誤提示是
    “ AttemptID:attempt_14267829456721_123456_m
    _000224_0 Timed out after 300 secsContainer killed
    by the ApplicationMaster.”。
*/
conf.set("mapreduce.task.timeout", "3600000");
// 開始“跳過”模式,讀取失敗超過則開啓“skip

conf.setInt("Mapred.skip.attempts.to.start.skipping", 1); 
// 設置跳過的最大記錄數爲1000
conf.setInt("Mapred.skip.Map.max.skip.reords", 1000);

推測執行

一個作業由若干個 Map 任務和 Reduce 任務構成。因硬件老化、軟件 Bug 等,某些任務可能運行非常慢。
典型案例:系統中有 99%的 Map 任務都完成了,只有少數幾個 Map 進度很慢,完不成.
這種場景下可能就需要推測執行機制:

原理 : 發現拖後腿的任務,比如某個任務運行速度遠慢於任務平均速度。爲拖後腿任務啓動一
個備份任務,同時運行。誰先運行完,則採用誰的結果

不適用的場景 :
1.  向數據庫寫數據
2. 存在負載均衡問題
3. 大多數任務運行起來都不快,並且集羣資源有限

我們的業務場景  : 1個reduce過程要處理大量的數據,並且reduce過程業務操作很複雜,所以每個reduce執行都不快
爲了防止推測執行功能帶來的資源開銷,我們是關閉了推測功能的
    // reduce階段停止推測功能, 默認爲true
	conf.set("mapreduce.reduce.speculative", "false");

jvm重用

如果我們實際開發中存在這樣的場景 : 每個map或者reduce處理的數據量並不大,但是map reduce的數量又有很多,
這種時候,Hadoop爲每個任務啓動一個新的虛擬機,java虛擬機開啓過多(因爲任務被劃分得過於細粒度),資源損耗會過大。
此時,我們爲了減少資源的不必要浪費,就需要啓用jvm重用功能

因爲我們的map,reduce的數量是很多的,爲了減少資源的損耗,我們開啓了jvm重用功能
/*
    默認情況下, tasktracker會爲每個需要執行的task新開JVM.這會引入創建與銷燬JVM的開銷.
    可以設置JVM重用.
    在執行完task後,tasktracker不必銷燬該JVM,而執行新的task. 默認爲1,也就是執行過一個task後就銷燬,
    我這裏根據業務需要設置爲-1, 一直保持重用.
*/
conf.set("mapreduce.job.jvm.numtasks", "-1"); 

參考資料

分佈式緩存DistributedCache

hadoop2.9.2官方文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章