1. 程式人生 > >大資料處理之如何確保斷電不丟資料

大資料處理之如何確保斷電不丟資料

今年7、8月份杭州實行拉閘限電時,導致阿里餘杭機房的機器意外斷電,造成HDFS叢集上的部分資料丟失。

在Hadoop 2.0.2-alpha之前,HDFS在機器斷電或意外崩潰的情況下,有可能出現正在寫的資料丟失的問題。而最近剛釋出的CDH4中HDFS在Client端提供了hsync()的方法呼叫(HDFS-744),從而保證在機器崩潰或意外斷電的情況下,資料不會丟失。這篇檔案將圍繞這個新的介面對其實現細節進行簡單的分析,從而希望找出一種合理使用hsync()的策略,避免重要資料丟失。

HDFS中sync(),hflush()和hsync()的差別

在hsync()之前,HDFS就已經提供了sync()和hflush()的呼叫,單從方法的名稱上看,很難分辨這三個方法之間的區別。咱們先從這幾個方法之間的差別介紹起。


hsync()則是除了確保會將Client端buffer中的存放資料更新到Datanode端外,還會確保Datanode端的資料更新到物理磁碟上,這樣在hsync()呼叫結束後,即使Datanode所在的機器意外斷電,資料並不會因此丟失。而hflush()在機器意外斷電的情況下卻有可能丟失資料,因為Client端傳給Datanode的資料可能存在於Datanode的cache中,並未持久化到磁碟上。下圖描述了從Client發起一次寫請求後,在HDFS中的資料包傳遞的流程。在HDFS中,呼叫hflush()會將Client端buffer中的存放資料更新到Datanode端,直到收到所有Datanode的ack響應時結束呼叫。這樣可保證在hflush()呼叫結束時,所有的Client端都可以讀到一致的資料。HDFS中的sync()本質也是呼叫hflush()。

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對其中檔案進行讀寫操作的客戶端。