閱讀779 返回首頁    go 阿裏雲 go 技術社區[雲棲]


大數據處理之如何確保斷電不丟數據

 

作者:傑菊

在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中的數據包傳遞的流程。

31

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中。

32

最開始筆者在測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中可能會產生三個事務。那麼創建並寫一個文件的執行流程如下:

33

 

在磁盤沒有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的錯誤。

34

 

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

  上一篇:go Java中的匿名內部類
  下一篇:go jvm開發筆記4&#8212;jvm crash信息處理