[料理佳餚] C# Microsoft.Hadoop.WebClient 讀取 Hadoop Archives(HAR Files)

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 個方法:

  1. HAR Files
  2. Sequence Files
  3. 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 個欄位的資訊說明如下:

  1. 項目的相對路徑
  2. 項目的類型(file or dir)
  3. 包含著項目內容的 part 檔名
  4. 項目在 part 檔內的 offset 位置
  5. 項目的內容長度

因此我自己建了一個類別來存放這樣的資訊:

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 >

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學