接觸Hadoop體系,在實際工作中使用MapReduce編程,已經有一段時間.
但最近個工作重心在實時ETL開發, 對MapReduce編程接觸較少,擔心時間久了會將過去的經驗遺忘.
就將之前工作中應用過的MapReduce開發的優化總結一下,用作備忘.
之間工作中應用的Hadoop集羣內存資源在1~10T(各省的集羣資源不同), 每天處理的數據量在幾十T,甚至上百T
希望這種大規模數據的處理經驗也能對大家有所幫助
本篇博客主要要點如下 :
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過程舉例
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過程如下圖 :
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");