關於HDFS的資料可見性

peterwell發表於2019-08-22

原文連結:

以前一直知道,寫入hdfs的資料不會馬上可見。

稍微看了些程式碼,總結下。

單一寫,併發讀

傳統的文件系統是允許對一個文件併發寫入的,只是如果不同步的話,文件內容會亂掉。 http://blog.chinaunix.net/uid-11452714-id-3771084.html
HDFS不允許併發寫,但可以併發讀: http://www.cnblogs.com/ZisZ/p/3253570.html
大多數分散式文件系統都不允許併發寫,代價太大。

如果多執行緒試圖同時寫一個文件,只有一個執行緒可以正常寫,其他執行緒會丟擲AlreadyBeingCreatedException異常:

123456
org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.protocol.AlreadyBeingCreatedException): failed to create file /tmp/appendTest for DFSClient_NONMAPREDUCE_-427798443_10 on client 172.31.132.146 because current leaseholder is trying to recreate file.at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.recoverLeaseInternal(FSNamesystem.java:2275)at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.appendFileInternal(FSNamesystem.java:2153)at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.appendFileInt(FSNamesystem.java:2386)at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.appendFile(FSNamesystem.java:2347)at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.append(NameNodeRpcServer.java:508)

如果一個客戶端A獲取了lease,但寫資料時意外退出,文件沒有close,lease不會自己釋放(正常close的話lease是會釋放的)。
只能等時間超過soft limit後,另一個客戶端B嘗試寫同一個文件,NN回收lease;或者時間超過hard limit後lease被NN的一個後臺執行緒回收。

所以如果客戶端B嘗試寫同一個文件,如果還沒超出hard limit,第一次嘗試必定會失敗的,因為同一個文件的lease還被佔用著:

12345
org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.protocol.RecoveryInProgressException): Failed to close file /tmp/appendTest. Lease recovery is in progress. Try again later.at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.recoverLeaseInternal(FSNamesystem.java:2310)at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.appendFileInternal(FSNamesystem.java:2153)at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.appendFileInt(FSNamesystem.java:2386)at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.appendFile(FSNamesystem.java:2347)

再嘗試時,如果時間已經超過soft limit,才能成功獲得lease;否則必定繼續失敗,只有等超過soft limit後,NN才會把lease分給新的客戶端。
所以寫資料時一般要加上重試機制。
可以自己寫個程式驗證下,用以下命令可以看到正在寫還沒有close的文件:

1
fsck -openforwrite /tmp

寫資料機制

其實hadoop權威指南已經講得比較清楚了,這裡結合程式碼複述下。相關的類主要是DFSClient和DFSOutputStream。這塊還有點複雜,我也沒完全看懂。

我們一般透過FileSystem.create或FileSystem.append方法獲得output stream(其實是DFSOutputStream),然後write(byte[])寫資料。
注意客戶端寫資料是直接和datanode互動的,只有申請新block時才需要和namenode互動。
如果是create,客戶端透過RPC協議(ClientProtocol.addBlock方法,這個類名字和mapreduce的RPC重複了。。。)向NN申請一個新block,開始寫資料。
如果是append,客戶端會先判斷目標文件的最後一個block是否寫滿,如果已滿就申請新的block,否則就在最後一個block上追加。
無論如何,客戶端會得到一個目標block用於寫入。

NN在分配一個block時還會返回對應的pipeline。如果副本數設定為3,那麼pipeline就是3個節點。如果是create,NN挑3個節點組成pipeline(這裡有規則的,但對我們這種單機房單機架的來說,就是隨機)。如果客戶端同時也是DN,那麼必定有一個副本同時在當前節點上(這個沒從程式碼上求證過)。如果是append,並且在原來的block上追加資料,那返回的pipeline就是原來的3個節點。

我們呼叫write(byte[])方法時,資料並沒有馬上寫入pipeline。DFSOutputStream會暫時快取資料。
資料的傳送是以packet為單位的,一個packet大小預設64K(dfs.client-write-packet-size,預設65536)。DFSOutputStream內部有兩個queue:dataQueue和ackQueue。待寫入的資料達到64K時,DFSOutputStream將資料包裝成一個packet並放入dataQueue,等待一個守護執行緒DataStreamer去消費。
DataStreamer從dataQueue中取出packet,發到pipeline,將packet加入ackQueue。pipiline中的所有節點都將資料寫入後,DataStreamer會收到ack訊息,並將packet從ackQueue中移除。這樣才算是資料真正寫入完畢。

其實一個packet不全是資料。DFSOutputStream會將資料組合成一個個chunk(dfs.bytes-per-checksum,預設512),每一個chunk加一個校驗值。預設的校驗(dfs.checksum.type,預設CRC32C)需要佔用4個位元組(見DataChecksum類),也就是說每個chunk實際佔用516個位元組。一個packet最多儲存65536/516=127個chunk。所以,一個packet的實際大小隻有127*516=65532位元組,其中只有65024個位元組是真正的資料。這個計算邏輯見DFSOutputStream:

1234567891011
private void computePacketChunkSize(int psize, int csize) {int chunkSize = csize + checksum.getChecksumSize();chunksPerPacket = Math.max(psize/chunkSize, 1);packetSize = chunkSize*chunksPerPacket;      // 127*516=65532if (DFSClient.LOG.isDebugEnabled()) {DFSClient.LOG.debug("computePacketChunkSize: src=" + src +", chunkSize=" + chunkSize +", chunksPerPacket=" + chunksPerPacket +", packetSize=" + packetSize);}}

DataStreamer每次寫到pipeline的資料也不一定是64K。上面說過,一個滿的packet只有65024個位元組,而且write時還會生成一個header資訊,先寫出header,再寫出packet本身。如果是最後一個packet,還可能湊不滿127個chunk,就更小了。資料也不一定能被512位元組整除。還要考慮到寫入的資料不能超過block size(block size必須是chunk的整數倍,否則會報錯),也會對packet大小做一些調整。

Packet的結構見DFSOutputStream.Packet類。

寫資料時,其實是先寫到一個512位元組的buffer裡,寫滿了就呼叫flushBuffer()方法計算checksum,將checksum和資料寫入currentPacket。如果currentPacket已經寫滿了,就放入dataQueue,這裡會阻塞,因為快取的packet有個最大值,預設80個(這個80是寫死在程式裡的,不知為何)。如果已經寫到block的最後,還會傳送一個空的packet物件,要求DN將資料持久化(這個機制見下面的分析)。之後透過和NN的RPC協議申請一個新的block繼續寫。

資料可見性

http://www.cnblogs.com/ZisZ/p/3253354.html(只能參考。原作者的hadoop版本比較老。)

客戶端寫入hdfs的資料不是立即可見的。

以前一直以為正在寫的整個block都不可見,其實不是。 只要寫入pipeline並且ack的資料,都是可見的。

只有快取在寫客戶端的資料,對其他讀客戶端才是不可見的。根據上面的描述,客戶端最多快取80個packet(dataQueue和ackQueue的size之和,這個80是寫死在程式裡的),每個packet大概64K,所以總共有大概5M的資料不可見。之所以是“大概”,因為packet中有校驗資料,而且有一個currentPacket不在queue裡。準確的值是65024*81=5.023M,差不多。
如果客戶端意外掛掉,快取的資料會完全丟失,也就是說最多丟5M的資料。

但是,寫到pipeline的資料雖然能看到,但不能保證不丟失。因為DN端也會將資料快取(這個快取機制還不太明白,沒看過程式碼),而不是立即寫到磁碟。極端情況下,pipeline裡的3個節點都掛掉,寫入pipeline的資料也會丟。
只有寫滿一個block時,客戶端才會傳送一個空的packet,這個packet的header有個特殊的標誌位,要求DN將當前block的資料刷到磁碟。
所以極端情況下,可能會丟一個block的資料(這是某些資料的說法,我沒看DN的程式碼求證過。感覺上有點問題,難道整個block都快取在記憶體裡?只有DN記憶體裡的資料會丟吧,如果blocksize設的很大,豈不是很耗記憶體。所以感覺不太可能丟整個block,應該也是有一個buffer之類的)。不過3個節點一起掛掉的機率很小吧。

雖然寫入pipeline的資料對客戶端可見了(去讀這個文件的話可以讀到)。但如果看hadoop fs -ls看這個文件,會發現這個文件的大小沒有變化,可能還是0位元組。

因為客戶端寫入資料時只需要和DN互動,NN只知道這個文件有哪個block在寫,但寫入的資料量是不知道的。客戶端讀的時候也是直接讀DN上的block,所以可以讀到pipeline中的資料。
只有等一個block寫完或者客戶端主動close,NN那邊才能看到大小的變化(只有這時才會與NN互動)。
如果客戶端意外掛掉,等超過1個小時(hard limit)文件大小也會變化。
如果客戶端意外掛掉,另一個客戶端1分鐘(soft limit)後重新獲取lease並且append,上一個客戶端寫入pipeline的資料也還在的。

hflush和hsync

hflush要求客戶端將所有buffer裡的資料寫入pipeline。之後資料對所有客戶端可見。本質就是阻塞所有寫入,將currentPacket加入dataQueue(即使currentPacket還沒滿),然後等待queue中的所有資料都ack。
hsync在hflush的基礎上,會將currentPacket的isSync標識設為true,DN收到這樣一個packet後,會將資料刷入磁碟。即使沒有資料要flush,也會新建一個空的packet物件,設定isSync併傳送。

這個兩個方法都是為了防止資料丟失的。hflush防止客戶端快取的資料丟失,hsync防止客戶端和pipeline快取的資料丟失。
即使是hsync,也只是保證資料刷到磁碟,但可能在磁碟的快取裡。所以沒有絕對的安全的。
而且這兩個方法會影響寫入的效率。

感覺上,如果對資料可見性有要求,可以定期hflush;客戶端掛掉最多丟5M資料,不能接受這種情況,也要定期hflush;其他情況都沒必要hflush。
hsync完全沒必要,寫到pipeline的資料已經很安全了。

上面說過寫入pipeline的資料不會立即讓NN端的文件大小改變。其實hsync時可以強制更新文件大小。

1234
FileSystem fs = FileSystem.get(conf);// 這裡要強制轉換下。HdfsDataOutputStream才有對應的hsync(EnumSet)方法,普通的DFSOutputStream沒有HdfsDataOutputStream out = (HdfsDataOutputStream) fs.create(new Path("/tmp/bigBlock2"));out.hsync(EnumSet.of(SyncFlag.UPDATE_LENGTH));

不用想就知道,肯定對效能影響非常大。

關於HDFS讀

其實和本文關係不大,只是順便整理下。

簡單整理下HDFS讀資料的機制。

  • 網路讀。很好理解。客戶端直接連DataNode,透過網路傳輸資料。最常見,適用於各種情況。
  • 本地socket讀。如果客戶端同時是DataNode,並且要讀的資料就在本地,可以省掉網路傳輸的過程。這也是MapReduce計算本地性的基本原理。“頻寬是最寶貴的資源”
  • 本地磁碟讀。即Short-Circuit Local Reads。當要讀的資料在本地時,可以不走socket,直接用系統呼叫讀磁碟上的文件。效率更高,但需要編譯對應系統的native lib。
  • 記憶體讀。即快取機制。hadoop 2.3.0新增了DataNode端的快取機制,可以將一些block快取到記憶體中。是否適用看應用場景吧。好處是效率高,壞處是額外佔用記憶體,而且這些記憶體是off-heap的,不受GC管理。

關於Short-Circuit Local Reads

關於這個配置還有些問題。

網上有很多文件給出的配置是要dfs.block.local-path-access.user屬性的,只有特定的使用者才能使用local read。實際上那是老的實現方法(legacy),基於 。
這種方法配置麻煩,配置項很多,並且有安全隱患。
在hadoop 2.1.0以後的版本已經有了新的實現,基於 。

新的實現需要libhadoop.so,要在不同系統上分別編譯。只需要如下兩個配置即可:

12345678
<property><name>dfs.client.read.shortcircuit</name><value>true</value></property><property><name>dfs.domain.socket.path</name><value>/var/lib/hadoop-hdfs/dn_socket</value></property>

hadoop 2.5.2的文件中已經給出了2種實現的配置(2.2.0的文件中只有新的實現)。

org.apache.hadoop.hdfs.BlockReaderLocalLegacy類的註釋:

1234
* This is the legacy implementation based on HDFS-2246, which requires* permissions on the datanode to be set so that clients can directly access the* blocks. The new implementation based on HDFS-347 should be preferred on UNIX* systems where the required native code has been implemented.<br>


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946120/viewspace-2654574/,如需轉載,請註明出處,否則將追究法律責任。

相關文章