環境是 64bit Ubuntu 14.04 系統, jdk 1.7 以及 Eclipse Mars (4.5)
這里介紹兩種調試 Hadoop 源代碼的方法: 利用 Eclipse 遠程調試工具和打印調試日志. 這兩種方法均可以調試偽分布式工作模式和完全分布式工作模式下的 Hadoop. 最后介紹我自己的方法, 可以打印你想查看的信息( 針對單個文件內部 ).
(1) 利用 Eclipse 進行遠程調試
參考 http://andilyliao.iteye.com/blog/2151688 https://www.cnblogs.com/viviman/archive/2013/01/15/2861725.html http://www.sohu.com/a/216999944_820120
下面以調試 ResourceManager 為例, 介紹利用 Eclipse 遠程調試的基本方法, 這可分兩步進行.
步驟 1 調試模式下啟動 Hadoop.
在 Hadoop 安裝目錄下運行如下的 Shell 腳本:
$ export YARN_NODEMANAGER_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=8788,server=y,suspend=y" $ sbin/start-all.sh # 在 /usr/local/hadoop 目錄下
運行了腳本后會看到 Shell 命令行終端顯示如下信息:
$ Listening for transport dt_socket at address: 8788
此時表明 ResourceManager 處於監聽狀態, 直到收到 debug 確認信息.
步驟 2 設置斷點
在新建的 Java 工程 "hadoop-main" 中, 找到 ResourceManager 相關代碼, 並在感興趣的地方設置一些斷點.
步驟 3 在 Eclipse 中調試 Hadoop 程序.
在 Eclipse 的菜單中, 依次選擇 "Run" --> "Debug Configurations" --> "Remote Java Applications", 並按照要求填寫遠程調試器名稱(自己定義一個即可), ResourceManager 所在 host 以及監聽端口號等信息, 並選擇 Hadoop 源代碼工程, 便可進入調試模式.
調試過程中, ResourceManager 輸出的信息被存儲到日志文件夾下的 yarn-XXX-resourcemanager-localhost.log 文件 ( XXX 為當前用戶名 ) 中, 可通過以下命令查看調試過程中打印的日志:
$ tail -f logs/yarn-XXX-resourcemanager-localhost.log # 在hadoop源代碼目錄下
(2) 打印 Hadoop 調試日志 參見 Hadoop源碼編輯--日志修改篇
Hadoop 使用了 Apache log4j 作為基本日志庫, 該日志庫將日志分為5個級別, 分別是 DEBUG, INFO, WARN, ERROR 和 FATAL. 這5個級別是有順序的, 即 DEBUG < INFO < WARN < ERROR < FATAL, 分別用來指定日志信息的重要程度. 日志輸出規則為: 只輸出級別不低於設定級別的日志信息, 比如若級別設定為 INFO, 則 INFO, WARN, ERROR 和 FATAL 級別的日志信息都會輸出, 但級別比 INFO 低的 DEBUG 則不會輸出.
在 Hadoop 源代碼中, 大部分 Java 文件中存在調試日志 ( DEBUG 級別日志 ), 但默認情況下, 日志級別是 INFO, 為了查看更詳細的運行狀態, 可采用以下幾種方法打開 DEBUG 日志.
方法 1 使用 Hadoop Shell 命令.
可使用 Hadoop 腳本中的 daemonlog 命令查看和修改某個類的日志級別, 比如, 可通過以下命令查看 NodeManager 類的日志級別: ( 如果你的主機是 node1, ip是192.168.1.101, 而且已經綁定了, host 寫這兩個中的一個即可 )
$ bin/hadoop daemonlog -getlevel ${nodemanager-host}:8042 org.apache.hadoop.yarn.server.nodemanager.NodeManager
可通過以下命令將 NodeManager 類的日志級別:
$ bin/hadoop daemonlog -setlevel ${nodemanager-host}:8042 org.apache.hadoop.yarn.server.nodemanager.NodeManager DEBUG
其中, nodemanager-host 為 NodeManager 服務所在的 host, 8042 是 NodeManager 的 HTTP 端口號.
方法 2 通過 Web 界面.
用戶可以通過 Web 界面查看和修改某個類的日志級別, 比如, 可通過以下 URL 修改 NodeManager 類的日志級別:
http://${nodemanager-host}:8042/loglevel
方法 3 修改 log4j.properties 文件. (親測有效) 參見 Apache log4j 官網
以上兩種方式只能暫時修改日志級別, 當 Hadoop 重啟后會被重置, 如果要永久性改變日志級別, 可在目標節點配置目錄下的 log4j.properties 文件中添加以下配置選項:
$ log4j.logger.org.apache.hadoop.yarn.server.nodemanager.NodeManager=DEBUG
3.1 此外, 有時為了專門調試某個 Java 文件, 需要把該文件的相關日志輸出到一個單獨文件中, 可在 log4j.properties 中添加以下內容:
# 定義輸出方式為自定義的 TTOUT log4j.logger.org.apache.hadoop.yarn.server.nodemanager.NodeManager=DEBUG,TTOUT # 設置 TTOUT 的輸出方式為輸出到文件 log4j.appender.TTOUT=org.apache.log4j.FileAppender # 設置文件路徑 log4j.appender.TTOUT.File=${hadoop.log.dir}/NodeManager.log # 設置文件布局 log4j.appender.TTOUT.layout=org.apache.log4j.PatternLayout # 設置文件格式 log4j.appender.TTOUT.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n
這些配置選項會把 NodeManager.java 中的 DEBUG 日志寫到日志目錄下的 NodeManager.log 文件中。這些對應的是NodeManager的自定義的LOG,如下所示。即 org.apache.hadoop.yarn.server.nodemanager.NodeManager 對應 NodeManager.class 。輸出的日志也是它自定義的LOG的輸出。
// NodeManager.java private static final Log LOG = LogFactory.getLog(NodeManager.class);
在閱讀源代碼的過程中, 為了跟蹤某個變量值的變化, 讀者可能需要自己添加一些 DEBUG 日志. 在 Hadoop 源代碼中, 大部分類會定義一個日志打印對象, 通過該對象可打印各個級別的日志. 比如, 在 NodeManager 中用以下代碼定義對象 LOG:
public static final Log LOG = LogFactory.getLog(NodeManager.class);
用戶可使用 LOG 對象打印調試日志. 比如, 可在 NodeManager 的 main 函數首行添加以下代碼:
LOG.debug("Start to lauch NodeManager....");
然后重新編譯 Hadoop 源代碼, 並將 org.apache.hadoop.yarn.server.nodemanager.NodeManager 的調試級別修改為 DEBUG, 重新啟動 Hadoop 后便可以看到該調試信息.
3.2 而大部分時候我們想用log4j為自己所用,輸出一些自己比較關心的信息。
在閱讀源代碼的過程中, 為了跟蹤某個變量值的變化, 讀者可能需要自己添加一些 DEBUG 日志. 在 Hadoop 源代碼中, 大部分類會定義一個日志打印對象, 通過該對象可打印各個級別的日志. 比如, 在 NodeManager 中用以下代碼定義對象 LOG:
// NodeManager.java private static final Log LOG = LogFactory.getLog(NodeManager.class); // 自帶的 private static final Log LOG = LogFactory.getLog("MyNodeManager"); //自己定義的
用戶可使用 LOG 對象打印調試日志. 比如, 可在 NodeManager 的 main 函數首行添加以下代碼:
LOG.debug("Start to lauch NodeManager....");
然后重新編譯 Hadoop 源代碼,將編譯好的jar包替換部署好的Hadoop集群的相應jar包之后,具體參見我的博客Hadoop 修改源碼以及將修改后的源碼應用到部署好的Hadoop中。 再配置 {HADOOP_HOME}/etc/hadoop/log4j.properties ,如下所示:
// log4j.properties # 我的NodeManager Logs log4j.logger.MyNodeManager=DEBUG,mynodemanager #設置OUT的輸出方式為輸出到文件 log4j.appender.mynodemanager=org.apache.log4j.FileAppender #設置文件路徑 log4j.appender.mynodemanager.File=${hadoop.log.dir}/MyNodeManager.log #設置文件的布局 log4j.appender.mynodemanager.layout=org.apache.log4j.PatternLayout #設置文件的格式 log4j.appender.mynodemanager.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n #設置該日志操作不與父類日志操作重疊 log4j.additivity.MyNodeManager=false
重新啟動 Hadoop 后,便可以在我們指定的文件下看到該調試信息文件。
(3) 我自己的方法
這里需要先修改源代碼. 然后重新編譯 Hadoop 源代碼,將編譯好的jar包替換部署好的Hadoop集群的相應jar包之后,具體參見我的博客Hadoop 修改源碼以及將修改后的源碼應用到部署好的Hadoop中。最后重新啟動 Hadoop 后, 只要執行到該類,就會顯示你要查看的信息.
第一步: 先修改源代碼
比如我們想要查看 DFSUtil.java 中的 locatedBlocks2Locations(List<LocatedBlock> blocks) 方法, 該方法用來創建 BlockLocation .
首先, 這是我們之前說的類和方法. DFSUtil.java 在 hadoop-2.7.3-src/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs 文件里.
// 在 org.apache.hadoop.hdfs.DFSUtil public class DFSUtil { // ...... /** * Convert a List<LocatedBlock> to BlockLocation[] * @param blocks A List<LocatedBlock> to be converted * @return converted array of BlockLocation */ public static BlockLocation[] locatedBlocks2Locations(List<LocatedBlock> blocks) { if (blocks == null) { return new BlockLocation[0]; } int nrBlocks = blocks.size(); BlockLocation[] blkLocations = new BlockLocation[nrBlocks]; if (nrBlocks == 0) { return blkLocations; } int idx = 0; for (LocatedBlock blk : blocks) { assert idx < nrBlocks : "Incorrect index"; // 改為DatanodeInfoWithStorage[] 或者在調用時 ((DatanodeInfoWithStorage)location[hCnt]).getStorageType() DatanodeInfo[] locations = blk.getLocations(); String[] hosts = new String[locations.length]; String[] xferAddrs = new String[locations.length]; String[] racks = new String[locations.length]; for (int hCnt = 0; hCnt < locations.length; hCnt++) { hosts[hCnt] = locations[hCnt].getHostName(); xferAddrs[hCnt] = locations[hCnt].getXferAddr(); NodeBase node = new NodeBase(xferAddrs[hCnt], locations[hCnt].getNetworkLocation()); racks[hCnt] = node.toString(); } DatanodeInfo[] cachedLocations = blk.getCachedLocations(); String[] cachedHosts = new String[cachedLocations.length]; for (int i=0; i<cachedLocations.length; i++) { cachedHosts[i] = cachedLocations[i].getHostName(); } blkLocations[idx] = new BlockLocation(xferAddrs, hosts, cachedHosts, racks, blk.getStartOffset(), blk.getBlockSize(), blk.isCorrupt()); idx++; } return blkLocations; } // ...... }
我們要想知道該方法內部的一些具體信息, 就先添加我自己設計的方法, 注意, 最開始要加上包, 如下所示:
import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.Arrays; // 如果有數組 /** * 向指定的文件中寫入內容; 如果是靜態,則在前面添加關鍵字 static . * 這里是追加寫, 想重新寫,先刪除生成的文件, 或者把文件刪除代碼注釋取消 * @author zhangchao * @version 2018年1月9號 下午14:57 * @param filecontent, 要寫入文件的內容 String 或 Object */ void writeToFile(Object filecontent){ String path = "/home/hadoop/"; String filename = "MyTest.txt"; String filenameTemp = path + filename; String filein = filecontent + "\r\n"; //新寫入的行,換行 // 如果文件不存在,創建文件. File file=new File(filenameTemp); try { // 若文件存在,先刪除已經存在的文件. 如果不想每次手動刪除文件,則取消這一塊注釋 //if(file.exist()){ // file.delete(); //} // 若文件不存在, 創建文件. if (!file.exists()) { file.getParentFile().mkdirs(); file.createNewFile(); } } catch (IOException e) { e.printStackTrace(); } // 向指定文件中寫入文字 FileWriter fileWriter; try { // 打開一個寫文件器,構造函數中的第二個參數true表示以追加形式寫文件 fileWriter = new FileWriter(filenameTemp,true); //使用緩沖區比不使用緩沖區效果更好,因為每趟磁盤操作都比內存操作要花費更多時間。 //通過BufferedWriter和FileWriter的連接,BufferedWriter可以暫存一堆數據,然后到滿時再實際寫入磁盤 //這樣就可以減少對磁盤操作的次數。如果想要強制把緩沖區立即寫入,只要調用writer.flush();這個方法就可以要求緩沖區馬上把內容寫下去 BufferedWriter bufferedWriter=new BufferedWriter(fileWriter); bufferedWriter.write(filein); bufferedWriter.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
最后, 把該方法添加到 DFSUtil 類中, ( 注意, 包不要和原有的重復; 並且如果是靜態的方法, 需要添加 static 關鍵字 ), 如下所示:
// 在 org.apache.hadoop.hdfs.DFSUtil
import java.io.BufferedWriter;
import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.Arrays; // 如果有數組
public class DFSUtil { // ......
/**
* 向指定的文件中寫入內容; 如果是靜態,則在前面添加關鍵字 static .
* 這里是追加寫, 想重新寫,先刪除生成的文件, 或者把文件刪除代碼注釋取消
* @author zhangchao
* @version 2018年1月9號 下午14:57
* @param filecontent, 要寫入文件的內容 String 或 Object
*/
void writeToFile(Object filecontent){
String path = "/home/hadoop/hadooplogs/"; // 目錄 String filename = "MyTest.txt"; // 寫入數據的文件名 String filenameTemp = path + filename; String filein = filecontent + "\r\n"; //新寫入的行,換行 // 如果文件不存在,創建文件. 取消這一塊注釋 File file=new File(filenameTemp); try { // 若文件存在,先刪除已經存在的文件 //if(file.exist()){ // file.delete(); //} // 若文件不存在, 創建文件 if (!file.exists()) { file.getParentFile().mkdirs(); file.createNewFile(); } } catch (IOException e) { e.printStackTrace(); } // 向指定文件中寫入文字 FileWriter fileWriter; try { // 打開一個寫文件器,構造函數中的第二個參數true表示以追加形式寫文件 fileWriter = new FileWriter(filenameTemp,true); //使用緩沖區比不使用緩沖區效果更好,因為每趟磁盤操作都比內存操作要花費更多時間。 //通過BufferedWriter和FileWriter的連接,BufferedWriter可以暫存一堆數據,然后到滿時再實際寫入磁盤 //這樣就可以減少對磁盤操作的次數。如果想要強制把緩沖區立即寫入,只要調用writer.flush();這個方法就可以要求緩沖區馬上把內容寫下去 BufferedWriter bufferedWriter=new BufferedWriter(fileWriter); bufferedWriter.write(filein); bufferedWriter.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
/** * Convert a List<LocatedBlock> to BlockLocation[] * @param blocks A List<LocatedBlock> to be converted * @return converted array of BlockLocation */ public static BlockLocation[] locatedBlocks2Locations(List<LocatedBlock> blocks) { if (blocks == null) { return new BlockLocation[0]; } int nrBlocks = blocks.size();
writeToFile("blocks.size() = " + nrBlocks); // 這是我添加的, 我想知道有幾個塊. 最后運行集群的時候,只要調用 DFSUtil 類的該方法, 就會調用我自己設計的方法 writeToFile(), 從而完成創建文件並寫入相關數據的操作. BlockLocation[] blkLocations = new BlockLocation[nrBlocks]; if (nrBlocks == 0) { return blkLocations; } int idx = 0; for (LocatedBlock blk : blocks) { assert idx < nrBlocks : "Incorrect index"; // 改為DatanodeInfoWithStorage[] 或者在調用時 ((DatanodeInfoWithStorage)location[hCnt]).getStorageType() DatanodeInfo[] locations = blk.getLocations(); String[] hosts = new String[locations.length]; String[] xferAddrs = new String[locations.length]; String[] racks = new String[locations.length]; for (int hCnt = 0; hCnt < locations.length; hCnt++) { hosts[hCnt] = locations[hCnt].getHostName(); xferAddrs[hCnt] = locations[hCnt].getXferAddr(); NodeBase node = new NodeBase(xferAddrs[hCnt], locations[hCnt].getNetworkLocation()); racks[hCnt] = node.toString(); }
writeToFile(Arrays.asList(hosts)); // 要把數組寫進文件, 需要借助 Arrays, Arrays.asList(...), 把數組轉化為 List<T> , 這樣就可以寫入到文件. DatanodeInfo[] cachedLocations = blk.getCachedLocations(); String[] cachedHosts = new String[cachedLocations.length]; for (int i=0; i<cachedLocations.length; i++) { cachedHosts[i] = cachedLocations[i].getHostName(); } blkLocations[idx] = new BlockLocation(xferAddrs, hosts, cachedHosts, racks, blk.getStartOffset(), blk.getBlockSize(), blk.isCorrupt()); idx++; } return blkLocations; } // ...... }
第二步: 編譯 Hadoop 源代碼,將編譯好的jar包替換部署好的Hadoop集群的相應jar包
修改好代碼之后, 我們知道, DFSUtil.java 在 hadoop-2.7.3-src/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs 文件里, 最深的一層包含 pom.xml ( 即可Maven ) 是 hadoop-2.7.3-src/hadoop-hdfs-project/hadoop-hdfs , 所以
// 先切換到 root 用戶 su root cd hadoop-2.7.3-src/hadoop-hdfs-project/hadoop-hdfs // hadoop-2.7.3-src 放在哪,就從那進 mvn package -Pdist -DskipTests -Dtar
Maven 編譯成功的話, 會顯示:
BUILD SUCCESS
編譯成功之后, 就會在 hadoop-2.7.3-src/hadoop-hdfs-project/hadoop-hdfs 文件下生成 target 文件夾, 里面存放 Maven 好的 jar 包, 這里會生成 hadoop-2.7.3-src/hadoop-hdfs-project/hadoop-hdfs/target/hadoop-hdfs-2.7.3.jar ,( 在該 Jar 包內, 我們通過壓縮軟件查看會發現 org/apache/hadoop/hdfs/DFSUtil.class, ) . 最后就是將該 jar 包替換到部署好的 Hadoop 的相應 jar 包, 即替換 hadoop-2.7.3/share/hadoop/hdfs/hadoop-hdfs-2.7.3.jar .
// $CLUSTER_SRC_HOME 是 hadoop-src-2.7.3 所在位置 // $HADOOP_HOME 是 hadoop-2.7.3 所在的位置 // 這里是在自己機器上單機部署的情況 cp $CLUSTER_SRC_HOME/hadoop-hdfs-project/hadoop-hdfs/target/hadoop-hdfs-2.7.3.jar $HADOOP_HOME/share/hadoop/hdfs/hadoop-hdfs-2.7.3.jar // 如果是全分布式的,需要向集群的每台機器拷貝. scp $CLUSTER_SRC_HOME/hadoop-hdfs-project/hadoop-hdfs/target/hadoop-hdfs-2.7.3.jar username@IP:$HADOOP_HOME/share/hadoop/hdfs/hadoop-hdfs-2.7.3.jar
第三步: 重新啟動 Hadoop
只要集群調用 DFSUtil 類的 locatedBlocks2Locations(List<LocatedBlock> blocks) 方法, 就會調用該函數內部我自己設計的方法, 完成創建文件並寫入信息. 實際上,運行個wordcount, 該方法會被調用, 因為集群需要創建 BlockLocation .
最后會創建 /home/hadoop/MyTest.txt , 文件內部有寫入的信息.
