大資料系列2:Hdfs的讀寫操作

lillcol發表於2021-01-26

在前文大資料系列1:一文初識Hdfs中,我們對Hdfs有了簡單的認識。

在本文中,我們將會簡單的介紹一下Hdfs檔案的讀寫流程,為後續追蹤讀寫流程的原始碼做準備。


Hdfs 架構

首先來個Hdfs的架構圖,圖中中包含了Hdfs 的組成與一些操作。

對於一個客戶端而言,對於Hdfs的操作不外乎也就讀寫兩個操作,接下來就去看看整個流程是怎麼走的。

下面我們由淺及深,分為簡單流程,詳細流程分別介紹讀寫過程


簡單流程

讀請求流程

客戶端需要讀取資料的時候,流程大致如下:

  1. ClientNameNode發起讀請求
  2. NameNode收到讀請求後,會返回後設資料,包括請求檔案的資料塊在DataNode的具體位置。
  3. Client根據返回的後設資料資訊,找到對應的DataNode發起讀請求
  4. DataNode收到讀請求後,會返回對應的Block資料給Client

寫請求流程

客戶端需要寫入資料的時候,流程大致如下:

  1. ClientNameNode發起寫請求,其中包含寫入的檔名,大小等。
  2. NameNode接收到資訊,NameNode會將檔案的資訊儲存到本地,同時判斷客戶端的許可權、以及檔案是否存在等資訊,驗證通過後NameNode返回資料塊可以儲存的DataNode資訊。
  3. 客戶端會切割檔案為多個Block,將每個Block寫入DataNode,在DataNode之間通過管道,對Block做資料備份。

詳細流程

讀請求流程

客戶端需要讀取資料的時候,流程大致如下:

  1. 客戶端通過呼叫FileSystemopen()方法來讀取檔案。、
  2. 這個物件是DistributedFileSystem的一個例項,通過遠端呼叫(RPC)與NameNode溝通,會向NameNode請求需要讀寫檔案檔案的Block位置資訊。
  3. NameNode會進行合法性校驗,然後返回Block位置資訊,每一個Block都會返回存有該副本的DataNode地址,並且會根據DtaNode與Client的距離進行排序(這裡的距離是指叢集網路拓撲的距離,也是儘可能滿足資料本地性的要求)
  4. DistributedFileSystem會返回一個支援檔案定位的輸入流FSDataInputStream給客戶端,其中封裝著DFSInputStream物件,該物件管理者DataNodeNameNode之間的I/O
  5. Client對這個輸入流呼叫read()方法
  6. DFSInputStream儲存了檔案中前幾個塊的DataNode地址,然後在檔案第一個Block所在的DataNode中連線最近的一個DtaNode。通過對資料流反覆呼叫read(),可以將資料傳輸到客戶端。
  7. 當到到Block的終點的時候,DFSInputStream會關閉與DataNode的連結。然後搜尋下一個BlockDataNode重複6、7步驟。在Client看來,整個過程就是一個連續讀取過程。
  8. 當完成所有Block的讀取後,Client會對FSDataInputStream呼叫close()

Client 讀取資料流的時候,Block是按照DFSInputStreamDataNode開啟新的連線的順序讀取的。
並且在有需要的時候,還會請求NameNode返回下一個批次BlocksDataNode資訊

DFSInputStreamDataNode互動的時候出現錯誤,它會嘗試選擇這個Block另一個最近的DataNode,並且標記之前的DataNode避免後續的Block繼續在該DataNode上面出錯。

DFSInputStream也會對來自DataNode資料進行校驗,一旦發現校驗錯誤,也會從其他DataNode讀取該Bclock的副本,並且向NamaNode上報Block錯誤資訊。

整個流程下來,我們可以發現Client直接連線到DataNode檢索資料並且通過NameNode知道每個Block的最佳DataNode。

這樣設計有一個好處就是:

因為資料流量分佈在叢集中的所有DataNode上,所以允許Hdfs擴充套件到大量併發Client.

與此同時,NamaNode只需要響應Block的位置請求(這些請求儲存在記憶體中,非常高效),
而不需要提供資料。

否則隨著客戶端數量的快速增加,NameNode會成為成為效能的瓶頸。


讀請求流程

客戶端需要寫入資料的時候,流程大致如下:

  1. Client通過create()方法呼叫DistributedFileSystemcreate()
  2. DistributedFileSystem通過RPCNameNode請求建立在檔案系統的明明空間中新建一個檔案,此時只是建立了一個空的檔案加,並沒有Block
  3. NameNode接收到crete請求後,會進行合法性校驗,比如是否已存在相同檔案,Client是否有相關許可權。如果校驗通過,NameNode會為新檔案建立一個記錄,並返回一些可用的DataNode。否則客戶端丟擲一個IOException
  4. DistributedFileSystem 會返回一個FSDataOutputStream個Client,與讀取資料類似,FSDataOutputStream封裝了一個DFSOutputStream,負責NameNodeDataNode之間互動。
  5. Client呼叫write()
  6. DFSOutputStream會將資料切分為一個一個packets,並且將之放入一個內部佇列(data queue),這個佇列會被DataStreamer消費,DataStreamer通過選擇一組合合適DataNodes來寫入副本,並請求NameNode分配新的資料塊。與此同時,DFSOutputStream還維護一個等待DataNode確認的內部包佇列(ack queue)
  7. 這些DataNodes會被組成一個管道(假裝置份數量為3)
  8. 一旦pipeline建立,DataStreamerdata queue中儲存的packet流式傳入管道的第一個DataNode,第一個DataNode儲存Packet並將之轉發到管道中的第二個DataNode,同理,從第二個DataNode轉發到管道中的第三個DataNode
  9. 當所一個packet已經被管道中所有的DataNode確認後,該packet會從ack queue移除。
  10. Client完成資料寫入,呼叫close(),此操作將所有剩餘的資料包重新整理到DataNode管道,等待NameNode返回檔案寫入完成的確認資訊。
  11. NameNode已經知道檔案是由哪個塊組成的(因為是DataStreamer請求NameNode分配Block的),因此,它只需要等待Block被最小限度地複製,最後返回成功。

如果在寫入的過程中發生了錯誤,會採取以下的操作:

  1. 關閉管道,並將所有在ack queue中的packets加到 data queue的前面,避免故障節點下游的DataNode發生資料丟失。
  2. 給該Block正常DataNode一個新的標記,將之告知NameNode,以便後續故障節點在恢復後能刪除已寫入的部分資料。
  3. 將故障節點從管道中移除,剩下的兩個正常DataNodes重新組成管道,剩餘的資料寫入正常的DataNodes
  4. NameNode發現備份不夠的時候,它會在另一個DataNode上建立一個副本補全,隨後該Blcok將被視為正常

針對多個DataNode出現故障的情況,我們只要設定 dfs.NameNode.replication.min的副本數(預設為1),Block將跨叢集非同步複製,直到達到其目標複製因子( dfs.replication,預設為3)為止.


通俗易懂的理解

上面的讀寫過程可以做一個類比,

NameNode 可以看做是一個倉庫管理員;
DataNode 可以看作是倉庫;

管理員負責管理商品,記錄每個商品所在的倉庫;
倉庫負責儲存商品,同時定期向管理員上報自己倉庫中儲存的商品;
此處的商品可以粗略的理解為我們的資料。

管理員只能有一個,而倉庫可以有多個。

當我們需要出庫的時候,得先去找管理員,在管理員處取得商品所在倉庫的資訊;
我們拿著這個資訊到對應倉庫提取我們需要的貨物。

當我們需要入庫的時候,也需要找管理員,核對許可權後告訴我們那些倉庫可以儲存;
我們根據管理員提供的倉庫資訊,將商品入庫到對應的倉庫。


存在的問題

上面是關於Hdfs讀寫流程介紹,雖然我分了簡單和詳細,但是實際的讀寫比這個過程複雜得多。

比如如何切塊?
為何小於塊大小的檔案按照實際大小儲存?
備份是如何實現的?
Block的結構等等。

這些內容會在後續的原始碼部分詳細解答。

此外,有人也許發現了,前文大資料系列1:一文初識Hdfs中Hdfs架構的介紹和本文讀寫的流程的介紹中,存在一個問題。

就是NameNode的單點故障問題。雖然之前有SecondaryNameNode 輔助NameNode合併fsiamgeedits,但是這個還是無法解決NameNode單點故障的問題。
很多人聽過HA(High Availability) 即高可用,誤以為高可用就是SecondaryNameNode,其實並不是。

在下一篇文章中會介紹Hdfs高可用的實現方式。

想了解更多內容觀影關注:【兔八哥雜談】

相關文章