在前面的博文:TaskTracker節點的內部設計與實現 中,我曾詳細的概述了TaskTracker節點中的各個工作組件,而在本文,我將對其內部的Http服務組件展開詳細的討論。
TaskTracker節點的內部Http服務組件主要提供兩個功能:1)./logtask,獲取某一個Task的執行日誌;2)./mapOutput,獲取某一個Task的map輸出數據。對於用戶來說,Http服務組件的/logtask功能不是必須的,但是它的/mapOutput功能對於整個Map-Reduce框架實現來說則是至關重要的,因爲每一個Job的每一個Reduce任務就是通過該服務來獲取它所需要的處理數據(也就是同屬一個Job的Map任務的輸出數據)的。如果不是Http服務組件負責提供該功能的話,我們完全可以取消該組件來優化TaskTracker節點的性能。所以,下面我將主要圍繞Http服務組件的/mapOutput功能來展開。
作業的Reduce任務在shuffle階段主要負責從執行該作業的Map任務的TaskTracker節點上抓取屬於自己的Map輸出數據,當然前提是這些Map任務已經被成功執行了。至於Reduce任務是如何知道作業的那些Map任務完成了,這一點在前面的博文中有詳細的談到。當Reduce任務發現一個完成的Map任務時,它會向負責執行該Map任務的TaskTracker節點發送一個Http請求來獲取這個Map輸出中屬於自己的數據,也就是說作業的Map/Reduce任務之間的數據是通過Http協議來傳輸的。這個請求連接的URL請求格式是:http://*:*/mapOutput?job=jobId&map=mapId&reduce=partition。
TaskTracker節點的Http服務組件來接受到/mapOutput的http請求之後,就會交給它的一個後臺線程來處理。這個後臺線程最終會調用對應的MapOutputServlet來處理,該處理的詳細操作如下:
private static final int MAX_BYTES_TO_READ = 64 * 1024;
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String mapId = request.getParameter("map");
String reduceId = request.getParameter("reduce");
String jobId = request.getParameter("job");
if (jobId == null) {
throw new IOException("job parameter is required");
}
if (mapId == null || reduceId == null) {
throw new IOException("map and reduce parameters are required");
}
ServletContext context = getServletContext();
int reduce = Integer.parseInt(reduceId);
byte[] buffer = new byte[MAX_BYTES_TO_READ];
// true iff IOException was caused by attempt to access input
boolean isInputException = true;
OutputStream outStream = null;
FSDataInputStream mapOutputIn = null;
long totalRead = 0;
ShuffleServerMetrics shuffleMetrics = (ShuffleServerMetrics) context.getAttribute("shuffleServerMetrics");
TaskTracker tracker = (TaskTracker) context.getAttribute("task.tracker");
try {
shuffleMetrics.serverHandlerBusy();
//創建輸出響應流
outStream = response.getOutputStream();
JobConf conf = (JobConf) context.getAttribute("conf");
//TaskTracker節點的本地目錄,用來存儲Map/Reduce任務的中間結果
LocalDirAllocator lDirAlloc = (LocalDirAllocator)context.getAttribute("localDirAllocator");
FileSystem rfs = ((LocalFileSystem) context.getAttribute("local.file.system")).getRaw();
//通過JobId和TaskId就可以找到Map任務的map輸出文件及索引文件
Path indexFileName = lDirAlloc.getLocalPathToRead(TaskTracker.getIntermediateOutputDir(jobId, mapId) + "/file.out.index", conf);
Path mapOutputFileName = lDirAlloc.getLocalPathToRead(TaskTracker.getIntermediateOutputDir(jobId, mapId) + "/file.out", conf);
/**
* Read the index file to get the information about where
* the map-output for the given reducer is available.
*/
IndexRecord info = tracker.indexCache.getIndexInformation(mapId, reduce,indexFileName);
//set the custom "from-map-task" http header to the map task from which
//the map output data is being transferred
response.setHeader(FROM_MAP_TASK, mapId);
//set the custom "Raw-Map-Output-Length" http header to
//the raw (decompressed) length
response.setHeader(RAW_MAP_OUTPUT_LENGTH,Long.toString(info.rawLength));
//set the custom "Map-Output-Length" http header to
//the actual number of bytes being transferred
response.setHeader(MAP_OUTPUT_LENGTH, Long.toString(info.partLength));
//set the custom "for-reduce-task" http header to the reduce task number
//for which this map output is being transferred
response.setHeader(FOR_REDUCE_TASK, Integer.toString(reduce));
//use the same buffersize as used for reading the data from disk
response.setBufferSize(MAX_BYTES_TO_READ);
/**
* Read the data from the sigle map-output file and
* send it to the reducer.
*/
//open the map-output file
LOG.debug("open MapTask["+mapId+"]'s output file: "+mapOutputFileName);
mapOutputIn = rfs.open(mapOutputFileName);
//seek to the correct offset for the reduce
mapOutputIn.seek(info.startOffset);
long rem = info.partLength;
int len = mapOutputIn.read(buffer, 0, (int)Math.min(rem, MAX_BYTES_TO_READ));
while (rem > 0 && len >= 0) {
rem -= len;
try {
shuffleMetrics.outputBytes(len);
outStream.write(buffer, 0, len);
outStream.flush();
} catch (IOException ie) {
isInputException = false;
throw ie;
}
totalRead += len;
len = mapOutputIn.read(buffer, 0, (int)Math.min(rem, MAX_BYTES_TO_READ));
}
LOG.info("Sent out " + totalRead + " bytes for reduce: " + reduce + " from map: " + mapId + " given " + info.partLength + "/" + info.rawLength);
} catch (IOException ie) {
Log log = (Log) context.getAttribute("log");
String errorMsg = ("getMapOutput(" + mapId + "," + reduceId + ") failed :\n"+ StringUtils.stringifyException(ie));
log.warn(errorMsg);
//異常是由於map輸出造成的,所以通知TaskTracker該Map任務的輸出發生了錯誤
if (isInputException) {
tracker.mapOutputLost(TaskAttemptID.forName(mapId), errorMsg);
}
response.sendError(HttpServletResponse.SC_GONE, errorMsg);
shuffleMetrics.failedOutput();
throw ie;
} finally {
if (null != mapOutputIn) {
mapOutputIn.close();
}
shuffleMetrics.serverHandlerFree();
if (ClientTraceLog.isInfoEnabled()) {
ClientTraceLog.info(String.format(MR_CLIENTTRACE_FORMAT, request.getLocalAddr() + ":" + request.getLocalPort(), request.getRemoteAddr() + ":" + request.getRemotePort(), totalRead, "MAPRED_SHUFFLE", mapId));
}
}
outStream.close();
shuffleMetrics.successOutput();
}
這個處理過程可表示如下圖:
從上面的代碼可以看出,MapOutputServlet爲了提高響應時間,對Map任務的輸出索引文件信息做了緩存,這裏想要解釋一下的就是Map任務的輸出索引文件file.out.index到底存儲了什麼重要信息。對於map操作的輸出key-value,用戶通常會根據應用的需要來爲作業的Map輸出設置一個partitioner,這個絕對了map操作的每一個key-value輸出將要交給哪一個Reduce任務來處理。交給相同的Reduce處理的key-value會存儲在Map任務輸出文件file.out中的一塊連續的的位置,那麼這個數據塊在file.out中的起始位置、原始長度(map的輸出由於用戶的設置可能被壓縮了)、實際長度就會存儲在對應的file.out.index文件中。這樣設計的合理性在於,1).file.out.index文件是小文件,緩存的它的信息不回消耗多少內存;2).當作業的一個Map任務完成時,該作業的所有Reduce任務都會馬上來獲取屬於他們的map輸出數據,這非常符合局部性原理。同時這個緩存空間又被限制了大小,這樣設計的主要目的是通過先進先出的緩存策略來自動的刪除那些已經無用的Map輸出索引信息,這是因爲絕大部分作業的生命週期是很短暫的,當一個作業被完成時,它的所有Map任務輸出(即中間數據)就沒有任何存在的意義了而被TaskTracker節點給刪除了。這個緩存的大小可以由TaskTracker的配置文件來設置,對應的配置項爲:mapred.tasktracker.indexcache.mb。
當MapOutputServlet在給Reduce任務發送屬於它的Map輸出數據時,如果發生了讀取該Map輸出數據異常,則會通知給TaskTracker節點,而TaskTracker節點會認爲這個Map任務執行失敗了稍後把這個信息報告給JobTracker節點。至於JobTracker節點再是如何處理的,前面的博文有詳細的闡述。