Hadoop HDFS 是透過 Block Size 的設定來決定對一個檔案切割的大小,HDFS 預設的 Block Size 是 128mb,意思就是說當一個檔案超過 128mb 時,就會被切成至少 2 個 Block 以上存放。
但是在海量圖檔的情況之下,一個圖檔通常不會超過 128mb,一個檔案還是佔用了一個 Block 個數,實際佔用的磁碟空間是依照檔案的實際大小沒錯,可是檔案的實際大小未達到 Block Size 的設定值時,還是硬生生地佔用了 1 個 Block 個數,當 Block 的數量太多的時候,對於 Hadoop Name Node 的記憶體空間就會有相當大程度的耗損。
Name Node 在記憶體中儲存 HDFS 中的檔案資訊,每個檔案、目錄或區塊(Block)需要大約 150 Bytes,看起來好像沒有什麼,但是如果有 2 億個 Block 呢?算下來就很恐怖了,而記憶體耗損倒是還好,真正影響速度的是在執行 Map-Reduce 工作時,需要不斷地去參照各個檔案,造成 Java Map-Reduce 的時候,Name Node 與 Data Node 交換的檔案資訊變多,大大地拖慢了處理的速度。
要解決這種海量小檔案拖慢 Hadoop 效能的問題,有 3 個方法:
- HAR Files
- Sequence Files
- HBase
相關說明我不多說,請參考 The Small Files Problem,而 HAR Files 是較多人推薦的解決方案。
執行 hadoop archive 指令
要將一堆小檔做成 HAR 檔最快的方法就是下指令:
sudo -u <hadoop account> hadoop archive -archiveName name -p <parent> <src>* <dest>
指令碼的相關說明在官網的 Hadoop Archives Guide 都有,我就不介紹了,至於 sudo -u <hadoop account>
則是在執行 hadoop archive
指令的時候需要是 Hadoop 允許的 Super User,所以這邊要切換指令的執行身分。
執行成功就會收到下面的訊息
HAR 檔就會以目錄的樣子呈現
列出 HAR 檔內的檔案清單
HAR 檔建立成功後,我們透過 Microsoft.Hadoop.WebClient 去瀏覽底下有哪些檔案時,會看到只有下面這個樣子,看不到我們原先的檔案,是因為我們的檔案已經被打包成一個一個的 part。
那這樣我們就無法很直接地列出我們的檔案清單,也無法很直接地下載檔案,這時候我們就必須要透過 HAR 檔底下的 _index 這個檔案,把它打開來看會發現裡面的內容大致上會長這樣:
其實它就是我們被 archive 的那些檔案的索引檔,裡面的資訊是 UrlEncode 過的,所以我們要使用的話必須就把它 UrlDecode 回來,UrlDecode 後就會長這樣:
第 1 列是一個 Summary 的資訊,真正有用的資料是從第 2 列開始,每列用空格隔開之後,我們只要知道每列的前 5 個欄位的資訊就可以執行檔案操作了,前 5 個欄位的資訊說明如下:
- 項目的相對路徑
- 項目的類型(file or dir)
- 包含著項目內容的 part 檔名
- 項目在 part 檔內的 offset 位置
- 項目的內容長度
因此我自己建了一個類別來存放這樣的資訊:
class HARIndexEntry
{
public string PathSuffix { get; set; }
public string Type { get; set; }
public string PartFileName { get; set; }
public int Offset { get; set; }
public int Length { get; set; }
}
有了這些資訊之後,列出 HAR 檔內的檔案清單就輕而易舉了。
private IEnumerable<string> ListFilesInHAR()
{
string indexFilePath = "擺放在 HAR 檔底下 _index 檔案的相對路徑";
// 讀取 _index 檔案,並將檔案內容做 UrlDecode。
var response = this.webHDFSClient.OpenFile(indexFilePath).Result;
var indexFileStream = response.Content.ReadAsStreamAsync().Result;
string indexData = Encoding.UTF8.GetString(((MemoryStream)indexFileStream).ToArray());
string decodedIndexData = HttpUtility.UrlDecode(indexData);
// 轉換 _index 檔案內容為 HARIndexEntry
var indexEntries = new List<HARIndexEntry>();
var decodedIndexDataLines = decodedIndexData.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
// 從第 2 列開始,跳過第 1 列。
for (int lineIndex = 1; lineIndex < decodedIndexDataLines.Length; lineIndex++)
{
var indexEntryDataArray = decodedIndexDataLines[lineIndex].Split(' ');
indexEntries.Add(
new HARIndexEntry()
{
PathSuffix = indexEntryDataArray[0],
Type = indexEntryDataArray[1],
PartFileName = indexEntryDataArray[2],
Offset = int.Parse(indexEntryDataArray[3]),
Length = int.Parse(indexEntryDataArray[4])
});
}
return indexEntries.Select(t => t.PathSuffix.Substring(1));
}
得到檔案清單一份
下載 HAR 檔內的檔案
當我們知道我們的 HAR 內有哪些檔案,要下載它就不是什麼難事了。
private void DownloadFileInHAR()
{
// 這邊要注意的是我們要 Open 的 File 已經不是我們熟悉的檔案名稱,而是我們欲下載的檔案所屬的 part 檔名。
string fileOpened = "擺放在 HAR 檔底下,我們欲下載的檔案所屬的 part 檔名。";
int fileOffset = 280060;
int fileLength = 144187;
// Open File 的時候我們要多丟 2 個參數 - offset、length,而這 2 個參數在 _index 檔都寫得清清楚楚。
var response = this.webHDFSClient.OpenFile(fileOpened, fileOffset, fileLength).Result;
var stream = response.Content.ReadAsStreamAsync().Result;
using (FileStream fs = File.Create("欲儲存的絕對路徑"))
{
stream.CopyTo(fs);
}
}
參考資料
< Source Code >