Cloudera提供給客戶的服務內容之一就是調整和優(yōu)化MapReduce job執(zhí)行性能。MapReduce和HDFS組成一個復雜的分布式系統(tǒng),并且它們運行著各式各樣用戶的代碼,這樣導致沒有一個快速有效的規(guī)則來實現(xiàn)優(yōu)化代碼性能的目的。在我看來,調整cluster或job的運行更像一個醫(yī)生對待病人一樣,找出關鍵的“癥狀”,對于不同的癥狀有不同的診斷和處理方式。
在醫(yī)學領域,沒有什么可以代替一位經(jīng)驗豐富的醫(yī)生;在復雜的分布式系統(tǒng)上,這個道理依然正確—有經(jīng)驗的用戶和操作者在面對很多常見問題上都會有“第六感”。我曾經(jīng)為Cloudera不同行業(yè)的客戶解決過問題,他們面對的工作量、數(shù)據(jù)集和cluster硬件有很大區(qū)別,因此我在這方面積累了很多的經(jīng)驗,并且想把這些經(jīng)驗分享給諸位。 在這篇blog里,我會高亮那些提高MapReduce性能的建議。前面的一些建議是面向整個cluster的,這可能會對cluster 操作者和開發(fā)者有幫助。后面一部分建議是為那些用Java編寫MapReduce job的開發(fā)者而提出。在每一個建議中,我列出一些“癥狀”或是“診斷測試”來說明一些針對這些問題的改進措施,可能會對你有所幫助。 請注意,這些建議中包含很多我以往從各種不同場景下總結出來的直觀經(jīng)驗。它們可能不太適用于你所面對的特殊的工作量、數(shù)據(jù)集或cluster,如果你想使用它,就需要測試使用前和使用后它在你的cluster環(huán)境中的表現(xiàn)。對于這些建議,我會展示一些對比性的數(shù)據(jù),數(shù)據(jù)產生的環(huán)境是一個4個節(jié)點的cluster來運行40GB的Wordcount job。應用了我以下所提到的這些建議后,這個job中的每個map task大概運行33秒,job總共執(zhí)行了差不多8分30秒。 第一點 正確地配置你的Cluster 診斷結果/癥狀: 1. Linux top命令的結果顯示slave節(jié)點在所有map和reduce slot都有task運行時依然很空閑。 2. top命令顯示內核的進程,如RAID(mdX_raid*)或pdflush占去大量的CPU時間。 3. Linux的平均負載通常是系統(tǒng)CPU數(shù)量的2倍。 4. 即使系統(tǒng)正在運行job,Linux平均負載總是保持在系統(tǒng)CPU數(shù)量的一半的狀態(tài)。 5. 一些節(jié)點上的swap利用率超過幾MB 優(yōu)化你的MapReduce性能的第一步是確保你整個cluster的配置文件被調整過。對于新手,請參考這里關于配置參數(shù)的一篇blog:配置參數(shù)。 除了這些配置參數(shù) ,在你想修改job參數(shù)以期提高性能時,你應該參照下我這里的一些你應該注意的項: 1. 確保你正在DFS和MapReduce中使用的存儲mount被設置了noatime選項。這項如果設置就不會啟動對磁盤訪問時間的記錄,會顯著提高IO的性能。 2. 避免在TaskTracker和DataNode的機器上執(zhí)行RAID和LVM操作,這通常會降低性能 3. 在這兩個參數(shù)mapred.local.dir和dfs.data.dir 配置的值應當是分布在各個磁盤上目錄,這樣可以充分利用節(jié)點的IO讀寫能力。運行 Linux sysstat包下的iostat -dx 5命令可以讓每個磁盤都顯示它的利用率。 4. 你應該有一個聰明的監(jiān)控系統(tǒng)來監(jiān)控磁盤設備的健康狀態(tài)。MapReduce job的設計是可容忍磁盤失敗,但磁盤的異常會導致一些task重復執(zhí)行而使性能下降。如果你發(fā)現(xiàn)在某個TaskTracker被很多job中列入黑名單,那么它就可能有問題。 5. 使用像Ganglia這樣的工具監(jiān)控并繪出swap和網(wǎng)絡的利用率圖。如果你從監(jiān)控的圖看出機器正在使用swap內存,那么減少mapred.child.java.opts屬性所表示的內存分配。 基準測試: 很遺憾我不能為這個建議去生成一些測試數(shù)據(jù),因為這需要構建整個cluster。如果你有相關的經(jīng)驗,請把你的建議及結果附到下面的留言區(qū)。 第二點 使用LZO壓縮 診斷結果/癥狀: 1. 對 job的中間結果數(shù)據(jù)使用壓縮是很好的想法。 2. MapReduce job的輸出數(shù)據(jù)大小是不可忽略的。 3. 在job運行時,通過linux top 和 iostat命令可以看出slave節(jié)點的iowait利用率很高。 幾乎每個Hadoop job都可以通過對map task輸出的中間數(shù)據(jù)做LZO壓縮獲得較好的空間效益。盡管LZO壓縮會增加一些CPU的負載,但在shuffle過程中會減少磁盤IO的數(shù)據(jù)量,總體上總是可以節(jié)省時間的。 當一個job需要輸出大量數(shù)據(jù)時,應用LZO壓縮可以提高輸出端的輸出性能。這是因為默認情況下每個文件的輸出都會保存3個幅本,1GB的輸出文件你將要保存3GB的磁盤數(shù)據(jù),當采用壓縮后當然更能節(jié)省空間并提高性能。 為了使LZO壓縮有效,請設置參數(shù)mapred.compress.map.output值為true。 基準測試: 在我的cluster里,Wordcount例子中不使用LZO壓縮的話,job的運行時間只是稍微增加。但FILE_BYTES_WRITTEN計數(shù)器卻從3.5GB增長到9.2GB,這表示壓縮會減少62%的磁盤IO。在我的cluster里,每個數(shù)據(jù)節(jié)點上磁盤數(shù)量對task數(shù)量的比例很高,但Wordcount job并沒有在整個cluster中共享,所以cluster中IO不是瓶頸,磁盤IO增長不會有什么大的問題。但對于磁盤因很多并發(fā)活動而受限的環(huán)境來說,磁盤IO減少60%可以大幅提高job的執(zhí)行速度。 第三點 調整map和reduce task的數(shù)量到合適的值 診斷結果/癥狀: 1. 每個map或reduce task的完成時間少于30到40秒。 2. 大型的job不能完全利用cluster中所有空閑的slot。 3. 大多數(shù)map或reduce task被調度執(zhí)行了,但有一到兩個task還在準備狀態(tài),在其它task完成之后才單獨執(zhí)行 調整job中map和reduce task的數(shù)量是一件很重要且常常被忽略的事情。下面是我在設置這些參數(shù)時的一些直觀經(jīng)驗: 1. 如果每個task的執(zhí)行時間少于30到40秒,就減少task的數(shù)量。Task的創(chuàng)建與調度一般耗費幾秒的時間,如果task完成的很快,我們就是在浪費時間。同時,設置JVM重用也可以解決這個問題。 2. 如果一個job的輸入數(shù)據(jù)大于1TB,我們就增加block size到256或者512,這樣可以減少task的數(shù)量。你可以使用這個命令去修改已存在文件的block size: hadoop distcp -Ddfs.block.size=$[256*1024*1024] /path/to/inputdata /path/to/inputdata-with/largeblocks。在執(zhí)行完這個命令后,你就可以刪除原始的輸入文件了(/path/to/inputdata)。 3. 只要每個task運行至少30到40秒,那么就增加map task的數(shù)量,增加到整個cluster上map slot總數(shù)的幾倍。如果你的cluster中有100個map slot,那就避免運行一個有101個map task的job — 如果運行的話,前100個map同時執(zhí)行,第101個task會在reduce執(zhí)行之前單獨運行。這個建議對于小型cluste和小型job是很重要的。 4. 不要調度太多的reduce task — 對于大多數(shù)job來說,我們推薦reduce task的數(shù)量應當?shù)扔诨蚴锹孕∮赾luster中reduce slot的數(shù)量。 基準測試: 為了讓Wordcount job有很多的task運行,我設置了如下的參數(shù):Dmapred.max.split.size=$[16*1024*1024]。以前默認會產生360個map task,現(xiàn)在就會有2640個。當完成這個設置之后,每個task執(zhí)行耗費9秒,并且在JobTracker的Cluster Summar視圖中可以觀看到,正在運行的map task數(shù)量在0到24之間浮動。job在17分52秒之后結束,比原來的執(zhí)行要慢兩倍多。 第四點 為job添加一個Combiner 診斷結果/癥狀: 1. job在執(zhí)行分類的聚合時,REDUCE_INPUT_GROUPS計數(shù)器遠小于REDUCE_INPUT_RECORDS計數(shù)器。 2. job執(zhí)行一個大的shuffle任務(例如,map的輸出數(shù)據(jù)每個節(jié)點就是好幾個GB)。 3. 從job計數(shù)器中看出,SPILLED_RECORDS遠大于MAP_OUTPUT_RECORDS。 如果你的算法涉及到一些分類的聚合,那么你就可以使用Combiner來完成數(shù)據(jù)到達reduce端之前的初始聚合工作。MapReduce框架很明智地運用Combiner來減少寫入磁盤以及通過網(wǎng)絡傳輸?shù)絩educe端的數(shù)據(jù)量。 基準測試: 我刪去Wordcount例子中對setCombinerClass方法的調用。僅這個修改就讓map task的平均運行時間由33秒增長到48秒,shuffle的數(shù)據(jù)量也從1GB提高到1.4GB。整個job的運行時間由原來的8分30秒變成15分42秒,差不多慢了兩倍。這次測試過程中開啟了map輸出結果的壓縮功能,如果沒有開啟這個壓縮功能的話,那么Combiner的影響就會變得更加明顯。 第五點 為你的數(shù)據(jù)使用最合適和簡潔的Writable類型 診斷/癥狀: 1. Text 對象在非文本或混合數(shù)據(jù)中使用。 2. 大部分的輸出值很小的時候使用IntWritable 或 LongWritable對象。 當一個開發(fā)者是初次編寫MapReduce,或是從開發(fā)Hadoop Streaming轉到Java MapReduce,他們會經(jīng)常在不必要的時候使用Text 對象。盡管Text對象使用起來很方便,但它在由數(shù)值轉換到文本或是由UTF8字符串轉換到文本時都是低效的,且會消耗大量的CPU時間。當處理那些非文本的數(shù)據(jù)時,可以使用二進制的Writable類型,如IntWritable, FloatWritable等。 除了避免文件轉換的消耗外,二進制Writable類型作為中間結果時會占用更少的空間。當磁盤IO和網(wǎng)絡傳輸成為大型job所遇到的瓶頸時,減少些中間結果的大小可以獲得更好的性能。在處理整形數(shù)值時,有時使用VIntWritable或VLongWritable類型可能會更快些—這些實現(xiàn)了變長整形編碼的類型在序列化小數(shù)值時會更節(jié)省空間。例如,整數(shù)4會被序列化成單字節(jié),而整數(shù)10000會被序列化成兩個字節(jié)。這些變長類型用在統(tǒng)計等任務時更加有效,在這些任務中我們只要確保大部分的記錄都是一個很小的值,這樣值就可以匹配一或兩個字節(jié)。 如果Hadoop自帶的Writable類型不能滿足你的需求,你可以開發(fā)自己的Writable類型。這應該是挺簡單的,可能會在處理文本方面更快些。如果你編寫了自己的Writable類型,請務必提供一個RawComparator類—你可以以內置的Writable類型做為例子。 基準測試: 對于Wordcount例子,我修改了它在map計數(shù)時的中間變量,由IntWritable改為Text。并且在reduce統(tǒng)計最終和時使用Integer.parseString(value.toString)來轉換出真正的數(shù)值。這個版本比原始版本要慢近10%—整個job完成差不多超過9分鐘,且每個map task要運行36秒,比之前的33秒要慢。盡量看起來整形轉換還是挺快的,但這不說明什么情況。在正常情況下,我曾經(jīng)看到過選用合適的Writable類型可以有2到3倍的性能提升的例子。 第六點 重用Writable類型 診斷/癥狀: 1. 在mapred.child.java.opts參數(shù)上增加-verbose:gc -XX:+PriintGCDetails,然后查看一些task的日志。如果垃圾回收頻繁工作且消耗一些時間,你需要注意那些無用的對象。 2. 在你的代碼中搜索"new Text" 或"new IntWritable"。如果它們出現(xiàn)在一個內部循環(huán)或是map/reduce方法的內部時,這條建議可能會很有用。 3. 這條建議在task內存受限的情況下特別有用。 很多MapReduce用戶常犯的一個錯誤是,在一個map/reduce方法中為每個輸出都創(chuàng)建Writable對象。例如,你的Wordcout mapper方法可能這樣寫: Java代碼
這樣會導致程序分配出成千上萬個短周期的對象。Java垃圾收集器就要為此做很多的工作。更有效的寫法是: Java代碼
基準測試: 當我以上面的描述修改了Wordcount例子后,起初我發(fā)現(xiàn)job運行時與修改之前沒有任何不同。這是因為在我的cluster中默認為每個task都分配一個1GB的堆大小 ,所以垃圾回收機制沒有啟動。當我重新設置參數(shù),為每個task只分配200MB的堆時,沒有重用Writable對象的這個版本執(zhí)行出現(xiàn)了很嚴重的減緩 —job的執(zhí)行時間由以前的大概8分30秒變成現(xiàn)在的超過17分鐘。原始的那個重用Writable的版本,在設置更小的堆時還是保持相同的執(zhí)行速度。因此重用Writable是一個很簡單的問題修正,我推薦大家總是這樣做。它可能不會在每個job的執(zhí)行中獲得很好的性能,但當你的task有內存限制時就會有相當大的區(qū)別。 第七點 使用簡易的剖析方式查看task的運行 這是我在查看MapReduce job性能問題時常用的一個小技巧。那些不希望這些做的人就會反對說這樣是行不通的,但是事實是擺在面前。 為了實現(xiàn)簡易的剖析,可以當job中一些task運行很慢時,用ssh工具連接上task所在的那臺task tracker機器。執(zhí)行5到10次這個簡單的命令 sudo killall -QUIT java(每次執(zhí)行間隔幾秒)。別擔心,不要被命令的名字嚇著,它不會導致任何東西退出。然后使用JobTracker的界面跳轉到那臺機器上某個task的stdout 文件上,或者查看正在運行的機器上/var/log/hadoop/userlogs/目錄中那個task的stdout文件。你就可以看到當你執(zhí)行那段命令時,命令發(fā)送到JVM的SIGQUIT信號而產生的棧追蹤信息的dump文件。([譯]在JobTracker的界面上有Cluster Summary的表格,進入Nodes鏈接,選中你執(zhí)行上面命令的server,在界面的最下方有Local Logs,點擊LOG進入,然后選擇userlogs目錄,這里可以看到以server執(zhí)行過的jobID命名的幾個目錄,不管進入哪個目錄都可以看到很多task的列表,每個task的log中有個stdout文件,如果這個文件不為空,那么這個文件就是作者所說的棧信息文件) 解析處理這個輸出文件需要一點點以經(jīng)驗,這里我介紹下平時是怎樣處理的: 對于棧信息中的每個線程,很快地查找你的java包的名字(假如是com.mycompany.mrjobs)。如果你當前線程的棧信息中沒有找到任何與你的代碼有關的信息,那么跳到另外的線程再看。 如果你在某些棧信息中看到你查找的代碼,很快地查閱并大概記下它在做什么事。假如你看到一些與NumberFormat相關的信息,那么此時你需要記下它,暫時不需要關注它是代碼的哪些行。 轉到日志中的下一個dump,然后也花一些時間做類似的事情然后記下些你關注的內容。 在查閱了4到5個棧信息后,你可能會意識到在每次查閱時都會有一些似曾相識的東西。如果這些你意識到的問題是阻礙你的程序變快的原因,那么你可能就找到了程序真正的問題。假如你取到10個線程的棧信息,然后從5個里面看到過NumberFormat類似的信息,那么可能意味著你將50%的CPU浪費在數(shù)據(jù)格式轉換的事情上了。 當然,這沒有你使用真正的分析程序那么科學。但我發(fā)現(xiàn)這是一種有效的方法,可以在不需要引入其它工具的時候發(fā)現(xiàn)那些明顯的CPU瓶頸。更重要的是,這是一種讓你會變的更強的技術,你會在實踐中知道一個正常的和有問題的dump是啥樣子。 通過這項技術我發(fā)現(xiàn)了一些通常出現(xiàn)在性能調優(yōu)方面的誤解,列出在下面。 1. NumberFormat 相當慢,盡量避免使用它。 2. String.split—不管是編碼或是解碼UTF8的字符串都是慢的超出你的想像— 參照上面提到的建議,使用合適的Writable類型。 3. 使用StringBuffer.append來連接字符串 上面只是一些提高MapReduce性能的建議。做基準測試的那些代碼我放在了這里:performance blog code 原文: 7 Tips for Improving MapReduce Performance |
|