HDFS 協議詳解

本文側重 HDFS 消息定義, 讀寫流程, 默認讀者瞭解 HDFS 基本概念和操作, 如有不瞭解, 可以翻閱其他資料.

需求來源

之前 (大概 2023 年年中) 在使用 rust 操作 HDFS 文件時發現 rust 的 HDFS 客戶端基本都依賴 java 的客戶端, 在使用前需要先下好 HDFS java 依賴, 然後配置 JAVA_HOME 等環境變量; 使用還需要啓動 jvm 通過 jni 交互, 臃腫又不方便, 所以當時就萌生了使用 rust 從頭實現 HDFS 客戶端想法, 畢竟我之前從頭實現過 smtp, websocket, mysql binlog 協議, 對網絡編程和協議解析都比較熟悉, HDFS 應該不會很難.

可等我真正從頭實現卻發現前方困難重重, 社區沒原生 HDFS 客戶端也情有可原. 第一個困難來自混亂不清的文檔, 不管是官方還是社區, 英文還是中文; 基本都是對 HDFS 協議簡單解釋, 配上官方文檔的一兩個配圖, 到了具體消息格式, 官方文檔是過時的, 其他文檔也沒有, 只能參照 java 源碼和其他語言實現. 第二個困難來自 HDFS 奇葩的消息定義, 3.x 版本的 NameNode 消息使用 protocol buffer 定義和編解碼, 但又不使用標準的 grpc, 而是自己定義一套 header 規範, 導致讀寫消息時只能逐個實驗. 第三個困難來自 HDFS 文檔裏的讀寫流程與消息定義不能完全對應, 要實現只能靠看源碼和試驗. 第四個困難來自搭建 HDFS 測試環境, 即使要部署一套單機測試環境, 也不是一件易事.

協議概述

HDFS 裏要完成文件讀寫需要 NameNode 和 DataNode 兩種類型節點, NameNode 負責存放文件元數據, 因此在讀寫文件前客戶端首先需要與 NameNode 通信, 獲取文件位置, 獲取到文件位置 (即文件在哪些 DataNode) 後客戶端與 DataNode 進行通信開始傳輸文件. 客戶與兩種節點的通信協議各不相同, 爲了方便描述, 本文將客戶端與 NameNode 直接的通信稱爲 HRpc, 客戶端與 DataNode 的通信稱爲 DataTransfer.

在 HDFS 裏文件會被切分成一個個塊 (Block), 讀寫時客戶端也需要處理屬於一個文件的多個 Block, 詳細過程會在下面的讀寫章節描述.

HRpc

HRpc 流程圖如下, 客戶端創建好 tcp 連接後會先發送 Handshake 消息, 告訴服務端自己使用的協議版本認證協議等, 接着發送 RequestHeader 和 IpcContext 信息, 接着就可以按固定格式調用各個 rpc 方法和讀取調用結果.

握手階段

Handshake 定義如下, 編碼時需要依次寫入 hrpcversionserver_class 和 auth_protocol 即可, 其中 version 默認爲 9, 其他兩個字段填 0 即可.

#[derive(Debug, Clone)]
pub struct Handshake {
    pub version: u8,
    pub server_class: u8,
    pub auth_protocol: u8,
}

發送 IpcContext 時首先需要寫入大端序的 u32, 表示包總長度, 接着是 encode_length_delimited[1] 編碼的 RpcRequestHeaderProto[2] 和 IpcConnectionContextProto[3]

此消息不需要等待 NameNode 端響應, 可直接進行後面的 rpc 方法調用

rpc 請求

rpc 請求與 ipc 請求類似, 不同的是, rpc 請求需要額外傳輸方法名和方法參數, 然後需要讀取 NameNode 響應, NameNode 響應也由響應頭和與方法對應的響應組成, 響應對應的請求和響應可以在 HRpc[4] 文檔裏找到.

如果調用出現錯誤, NameNode 會將 RpcResponseHeaderProto 裏的 status 設置爲 Error, 具體錯誤信息則放在 error_msg 字段.

下面以 delete[5] 方法爲例說明 rpc 調用流程.

DataTransfer

在上文我們提到 HDFS 對文件的讀寫其實是按塊進行的, 這裏爲了方便, 我們將負責讀寫塊的結構體 / 流程分別稱爲 BlockReader 和 BlockWriter.

BlockReader

客戶端通過 HRpc 的 getFileInfo 和 getBlockLocations 方法獲得文件的元數據和所有塊位置信息, 接着可以按順序依次讀取每個塊. 如果 HDFS 設置了多副本, 每個塊下 locs 字段會有多個 DataNode 信息, 客戶端可以通過這些 DataNode 信息自動選擇最近或按其他邏輯選擇合適的 location 開始讀取.

客戶端創建好連接後需要先發送 OpReadBlockProto[6], 將客戶端 ID, 認證信息, 塊信息和是否進行 checksum 校驗等信息發送給 DataNode, 此外理論上 offset 可以指定讀哪裏開始讀, 但設置似乎不生效. 如果 DataNode 返回成功響應, 客戶端還需要再讀取 PacketHeaderProto[7] 數據長度, checksum 等消息, 接着 DataNode 會發送客戶端要讀取的數據.

如果要求發送 checksum(一個 u32 數值), DataNode 會在 BlockOpResponseProto 字段裏設置對應的 checksum 方法和每多少字節計算一次 checksum, 也就是 bytes_per_checksum, 客戶端需要根據公式 data_len.div_ceil(bytes_per_checksum) * 4, 計算 checksum 數據長度.

BlockWriter

相比於讀, 寫流程更復雜些, 寫還分爲 create 和 append 模式, 不過 append 僅在第一個 block 處理上和 create 有區別, append 需要將 offset 設置爲已有數據長度, 而不是 0; 寫入的時候可以不按 bytes_per_checksum 數量寫入, 但如果開啓 checksum, 每次寫入時不論數據多少, 必須同時發送 checksum, 此外如果當前塊 offset + 寫入字節數超過 bytes_per_checksum, 在 bytes_per_checksum 切分, 計算兩次 checksum 發送, 否則會導致 server error. 最後, Client 還需要向 NameNode 更新塊狀態.

讀取文件

爲了易讀性, 後面的流程圖省略了 HRpc 和 BlockReader 具體過程.

創建並寫入文件

追加寫文件

尾聲

上述讀寫流程也只是提供大致視角, 如果你想了解更多 HDFS 細節可以閱讀 hdfs-client[8] 源碼, 也歡迎使用 hdfs-client 提 pr 和 issue.

PS: 封面來自北京初雪晚上散步所拍

引用鏈接

[1] encode_length_delimited: https://docs.rs/prost/latest/prost/trait.Message.html#method.encode_length_delimited
[2] RpcRequestHeaderProto: https://docs.rs/hdfs-types/0.1.0/hdfs_types/common/struct.RpcRequestHeaderProto.html
[3] IpcConnectionContextProto: https://docs.rs/hdfs-types/0.1.0/hdfs_types/common/struct.IpcConnectionContextProto.html
[4] HRpc: https://docs.rs/hdfs-client/latest/hdfs_client/hrpc/struct.HRpc.html
[5] delete: https://docs.rs/hdfs-client/latest/hdfs_client/hrpc/struct.HRpc.html#method.delete
[6] OpReadBlockProto: https://docs.rs/hdfs-types/0.1.0/hdfs_types/hdfs/struct.OpReadBlockProto.html
[7] PacketHeaderProto: https://docs.rs/hdfs-types/0.1.0/hdfs_types/hdfs/struct.PacketHeaderProto.html
[8] hdfs-client: https://github.com/PrivateRookie/hdfs-client

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ieseR_3kJD8trkSRj-hBHw