大數據處理之如何確保斷電不丟數據
作者:傑菊
在Hadoop 2.0.2-alpha之前,HDFS在機器斷電或意外崩潰的情況下,有可能出現正在寫的數據丟失的問題。而最近剛發布的CDH4中HDFS在Client端提供了hsync()的方法調用(HDFS-744),從而保證在機器崩潰或意外斷電的情況下,數據不會丟失。這篇文件將圍繞這個新的接口對其實現細節進行簡單的分析,從而希望找出一種合理使用hsync()的策略,避免重要數據丟失。
HDFS中sync(),hflush()和hsync()的差別
在hsync()之前,HDFS就已經提供了sync()和hflush()的調用,單從方法的名稱上看,很難分辨這三個方法之間的區別。咱們先從這幾個方法之間的差別介紹起。
在HDFS中,調用hflush()會將Client端buffer中的存放數據更新到Datanode端,直到收到所有Datanode的ack響應時結束調用。這樣可保證在hflush()調用結束時,所有的Client端都可以讀到一致的數據。HDFS中的sync()本質也是調用hflush()。
hsync()則是除了確保會將Client端buffer中的存放數據更新到Datanode端外,還會確保Datanode端的數據更新到物理磁盤上,這樣在hsync()調用結束後,即使Datanode所在的機器意外斷電,數據並不會因此丟失。而hflush()在機器意外斷電的情況下卻有可能丟失數據,因為Client端傳給Datanode的數據可能存在於Datanode的cache中,並未持久化到磁盤上。下圖描述了從Client發起一次寫請求後,在HDFS中的數據包傳遞的流程。
hsync()的實現本質
hsync()執行時,實際上會在對應Datanode的機器上產生一個fsync的係統調用,從而將內存中的相關文件的數據更新到磁盤。
Client端執行hsync時,Datanode端會識別到Client發送過來的數據包中的syncBlock_字段為true,從而判定需要將內存中的數據更新到磁盤。此時會在BlockReceiver.java的flushOrSync()中執行如下語句:
((FileOutputStream)cout).getChannel().force(true);
而FileChannel的force(boolean metadata)方法在JDK中,底層為於FileDispatcherImpl.c中調用fsync或fdatasync。metadata為true時執行fsync,為false時執行fdatasync。
Java_sun_nio_ch_FileDispatcherImpl_force0(JNIEnv *env, jobject this, jobject fdo, jboolean md) { jint fd = fdval(env, fdo); int result = 0; if (md == JNI_FALSE) { result = fdatasync(fd); } else { result = fsync(fd); } return handle(env, result, "Force failed"); }
當Datanode將數據持久化到磁盤上後,會發ack響應給Client端。當收到所有Datanode的ack響應時,hsync()的調用結束。
值得注意的是,fsync或fdatasync本身是一個非常耗時的調用,因為磁盤的讀寫速度遠低於內存的讀寫速度。在不調用fsync或fdatasync的情況下,數據可能保存在各級cache中。
最開始筆者在測hsync()的讀寫性能時,發現不同機器上測試結果hsync()耗時差別巨大,有的集群平均調用耗時為4ms,而有的集群平均調用耗時則需25ms。後來在公司各位大神的點撥下才意識到是跟Linux文件係統的機製有關。在這種情況下,隻有一探Linux相關部分的源碼才能解開心中的疑惑,下麵這節就將從更底層的角度來解析與hsync()密切相關的係統調用fsync及fdatasync方法。
fsync和fdatasync的大致實現過程
對ext4格式的文件係統來說,fsync和fdatasync方法的實現代碼位於fs/ext4/fsync.c這個文件中。在追加寫文件的情況下,fsync和fdatasync的流程幾乎一致,因為對HDFS的寫操作基本都是追加寫,下麵我們隻討論追加寫文件下的情景。ext4格式的文件係統中布局大致如下:
Group 0 Padding | Super Block | Group Descriptors | Reserved GDT Blocks Data | Data Block Bitmap | inode Bitmap | inode Table | Data Blocks |
1024 bytes | 1 block | many blocks | many blocks | 1 block | 1 block | many block | many more blocks |
在我們追加寫文件時,涉及到修改的有DataBlock BitMap、inode BitMap、inode Table、Data Blocks。但從代碼中來看,實際上對文件的追加會被合並成兩次寫(這裏是指邏輯意義上的兩次寫,實際在從係統Cache刷新到磁盤時,讀寫操作會被再次合並),第一次為寫DataBlock和DataBlock Bitmap,第二次為寫inode BitMap和更新inode BitMap中的inode。ext4為了支持更大的容量,使用了extend tree來實現塊映射。在追加文件的情況下,fsync和fdatasync除了更新inode中的extend tree外,還會更新inode中文件大小,塊計數這些metadata。對fsync來說,還會修改inode中的文件修改時間、文件訪問時間(在mount選項不含noatime的情況下)和inode修改時間。
寫障礙和Disk Cache的影響
在了解了fsync()和fdatasync()方法會對文件係統進行的改動後,離找出之前為什麼在不同集群上hsync()的調用平均耗時的原因仍還有一段距離。這時我發現了不同的磁盤掛載選項會影響到fsync()和fdatasync()的執行時間,進而確定是寫障礙和Disk Cache在搞怪。下麵這節就將分析寫障礙和Disk Cache對hsync()方法調用耗時的影響。
由於市麵上大部分的磁盤都是帶Disk Cache的,這導致在不開啟寫障礙的情況下,機器意外斷電可能會對其造成metadata的不一致。對ext4這種journal文件係統來說,journal寫入一個事務後,會對metadata進行更新,更新完成後會將該事務標記從未執行修改為完成。舉個例子,加入我們要創建並寫一個文件,那麼在journal中可能會產生三個事務。那麼創建並寫一個文件的執行流程如下:
在磁盤沒有Disk Cache的情況下,即時機器意外斷電,那麼重啟自檢時,可通過journal中最後事務的狀態來對metadata進行重新執行修複或者廢棄該事務。從而保證了metadata的一致性。但在磁盤有Disk Cache的情況下,IO事件會當數據寫到Disk Cache中就響應完成。雖然journal按上圖的流程進行執行,但是執行完成後這些數據仍可能有部分並未持久化到磁盤上。假如在執行第6個步驟的時候機器意外斷電,同時第4個步驟中的數據暫未更新到磁盤,而第1,2,3,5個步驟的數據已經同步到磁盤的話。這時機器重啟自檢時,由於第5個步驟中journal的執行狀態為未完成,會重新執行第6個步驟一次。但第6個步驟對metadata的修改是建立在第4個步驟已經完成的基礎之上的,由於第4個步驟並未持久化到磁盤,所以重新執行第6個步驟時會發生異常,造成metadata的錯誤。
Linux中為了避免這一情況,可以在ext4的mount選項中加barrier=1,data=ordered開啟寫障礙,來確保數據持久化到磁盤的順序。在寫障礙前的數據會先於寫障礙後的數據刷新到磁盤,Linux會在journal的事務寫到Disk Cache中後放置一個寫障礙。這樣journal的事務位於寫障礙之前,而對應的metadata的修改數據位於寫障礙之後。避免了Disk Cache中合並IO時,對讀寫操作進行重排序後,由於讀寫操作執行順序的改變而造成意外斷電後metadata無法修複的情況。
關閉寫障礙,即ext4的mount選項為barrier=0時,除了有可能造成在機器斷電或異常崩潰重啟後metadata錯誤外,fsync和fdatasync的調用還會在數據更新到Disk Cache時就返回,而非等到數據刷新到磁盤上後才結束調用。因為在不開寫障礙的情況下,Linux會將此時的磁盤當做沒有Disk Cache的磁盤來處理,當數據隻是更新到Disk Cache,就會認為該IO操作已完成,這也正是前文中提到的不同集群上hsync()的平均調用時長差別巨大的原因。所以關閉寫障礙的情況下,調用fsync或fdatasync並不能確保數據在機器斷電或異常崩潰時不丟失。
Disk Cache的存在可以提高磁盤每秒的吞吐量,通過重排序IO,盡量將IO讀寫變成順序讀寫提高速率,同時減少文件係統碎片。而通過開啟寫障礙,可避免意外斷電情形下metadata異常,同時確保調用fsync或fdatasync時Disk Cache中的數據持久到磁盤。
開啟journal的影響
除了寫障礙和Disk Cache會影響到hsync()的調用時長外,Datanode上文件係統有沒有打開journal也是影響因素之一。關閉journal的情況下可以減少hsync()的調用時長。
在不開啟journal的情況下,調用fsync或fdatasync主要是由generic_file_fsync這個方法來實現將數據刷新到磁盤。在追加寫文件的情況下,不論是fsync還是fdatasync,在generic_file_fsync這個方法中都會先更新Data Block數據,再更新inode數據。如果執行fsync或fdatasync的文件為新創建的文件,在不開啟journal的情況下,還會在更新完文件的inode後,更新該文件的父結點的Data Block和inode。
而開啟journal的情況下,調用fsync或fdatasync會先寫Data Block,然後提交journal的事務。雖然調用fsync或fdatasync是指定對某個文件進行操作,但在ext4中,整個文件係統隻有一個journal文件,提交journal的修改事務時會將整個文件係統的metadata的修改事務一並提交。在文件係統寫入操作頻繁時,這一步操作會比較耗時。
fsync及fdatasync耗時測試
測試使用的代碼如下:
代碼中以追加的方式向一個已存在的文件寫入4k數據,4k剛好為內存頁和磁盤塊的大小。下麵分別以幾種模式來測試fsync和fdatasync的耗時。
#define BLOCK_LEN 1024 static long long microseconds(void) { struct timeval tv; long long mst; gettimeofday(&tv, NULL); mst = ((long long)tv.tv_sec) * 1000000; mst += tv.tv_usec; return mst; } int main(void) { int block = open("./block", O_WRONLY|O_APPEND, 0644); long long block_start, block_end, fdatasync_time, fsync_time; char block_buf[BLOCK_LEN]; int i = 0; for(i = 0; i < BLOCK_LEN; i++){ block_buf[i] = i % 50; } if (write(block, block_buf, BLOCK_LEN) == -1) { perror("write"); exit(1); } block_start = microseconds(); fdatasync(block); block_end = microseconds(); fdatasync_time = block_end - block_start; if (write(block, block_buf, BLOCK_LEN) == -1) { perror("write"); exit(1); } block_start = microseconds(); fsync(block); block_end = microseconds(); fsync_time = block_end - block_start; printf("fdatasync spent: %lld, fsync spent: %lld\n", fdatasync_time, fsync_time); close(block); exit(0); }
測試準備
- 文件係統:ext4
- 操作係統內核:Linux 2.6.18-164.el5
- 硬盤型號:WDC WD1003FBYX-1 1V02,SCSI接口
- 通過sdparm–set=WCE /dev/sdx開啟Disk Write Cache,sdparm–clear=WCE /dev/sdx關閉Disk Write Cache
- 通過barrier=1,data=ordered開啟寫障礙,barrier=0關閉寫障礙
- 通過tune4fs-O has_journal /dev/sdxx開啟Journal,tune4fs-O ^has_journal /dev/sdxx關閉Journal
關閉Disk Cache,關閉Journal
類型 | 耗時(微秒) |
fdatasync | 8368 |
fsync | 8320 |
Device | wrqm/s | w/s | wkB/s | avgrq-sz | avgqu-sz | await | svctm | %util |
sdi | 0.00 | 120.00 | 480.00 | 8.00 | 1.00 | 8.33 | 8.33 | 100.00 |
可以看到,iostat為8ms,對inode、Data Block、inode Bitmap、DataBlock Bitmap的數據更新合並為了一次寫操作。
關閉Disk Cache,開啟Journal
類型 | 耗時(微秒) |
fdatasync | 33534 |
fsync | 33408 |
Device | wrqm/s | w/s | wkB/s | avgrq-sz | avgqu-sz | await | svctm | %util |
sdi | 37.00 | 74.00 | 444.00 | 11.95 | 1.22 | 16.15 | 13.32 | 99.90 |
通過使用blktrace跟蹤對磁盤塊的讀寫,發現此處寫journal會比較耗時,下麵的記錄為fsync過程中對磁盤發送的寫操作,已預處理掉了大部分不重要的信息,可以看到,後麵三條記錄都是journal的寫操作(通過此處kjournald的進程id為3001來識別)。
0,0 | 13 | 1 | 0.000000000 | 8835 | A | W | 2855185 + 8 <- (8,129) 2855184 |
0,0 | 4 | 5 | 0.000313001 | 3001 | A | W | 973352281 + 8 <- (8,129) 973352280 |
0,0 | 4 | 1 | 0.000305325 | 3001 | A | W | 973352273 + 8 <- (8,129) 973352272 |
0,0 | 4 | 12 | 0.014780357 | 3001 | A | WS | 973352289 + 8 <- (8,129) 973352288 |
開啟Disk Cache,開啟寫障礙,開啟Journal
類型 | 耗時(微秒) |
fdatasync | 23759 |
fsync | 25006 |
從結果可以看到,Disk Cache的開啟可以合並更多IO,從而減少耗時。
值得注意的是,在開啟Disk Cache時,iostat的await是按照從內存寫完到Disk Cache中來統計耗時,並非是按照寫到磁盤上來計時,所以此種情況下iostat的await參數會比較小,並無參考意義。
小結
從這次測試結果可以看到,雖然CDH4提供了hsync()方法,但是若我們對每次寫操作都執行hsync(),會嚴重加劇磁盤的寫延遲。通過一些策略,比方說定期執行hsync()或當存在於Cache中的數據達到一定數目時,執行hsync()會是更可行的方案,從而盡量減少機器意外斷電所帶來的影響。
附:術語解釋
- Hadoop: Apache基金會的開源項目,用於海量數據存儲與計算。
- CDH4: Cloudera公司在Apache社區發行版基礎之上進行改進後的發行版,更穩定更適用於生產環境。
- Namenode: Hadoop的HDFS模塊中管理所有文件元數據的組件。
- Datanode: Hadoop的HDFS模塊中存儲文件實際數據的組件。
- HDFS Client: 這裏指連接HDFS對其中文件進行讀寫操作的客戶端。
最後更新:2017-04-03 07:57:05