http://www.cnblogs.com/hustcat/p/3283955.html
http://www.cnblogs.com/zengkefu/p/5639200.html
http://www.cnblogs.com/zengkefu/p/4943836.html
http://www.cnblogs.com/zengkefu/p/5639200.html
http://blog.sina.com.cn/s/blog_8308bc810102uxhz.html
深入理解Fsync
1 介紹
數據庫系統從誕生那天開始,就面對一個很棘手的問題,fsync的性能問題。組提交(group commit)就是為了解決fsync的問題。最近,遇到一個業務反映MySQL創建分區表很慢,仔細分析了一下,發現InnoDB在創建表的時候有很多fsync——每個文件會有4個fsync的調用。當然,並不每個fsync的開銷都很大。
這里引出幾個問題:
(1)問題1:為什么fsync開銷相對都比較大?它到底做了什么?
(2)問題2:細心的人可以發現,第一次open數據文件后,第二次fsync的時間遠遠小於第1次調用fsync的時間,為什么?
(3)問題3:能否優化fsync?
來着這些疑問,一起來了解一下fsync。
2 原因分析
我們先通過一個測試程序來學習一下fsync在塊層的基本流程。
2.1 測試程序1
Write page 0 Sleep 5 Fsync |
用blktrace跟蹤結果如下:
上半部紅色框內為pwrite在塊層的流程,下半部黃色框內為fsync在塊層流程,中間剛好相差5秒。
4722712為測試文件的第1個block對應的扇區號,590339(block號) * 8=4722712(扇區號)。
無論是pwrite,還是fsync,主要的開銷都發生IO請求提交給驅動和IO完成之間,也就是說開自設備驅動。差不多占了整個系統調用的1/2的開銷。
另外,可以看到調用fsync時,發生了3次塊層IO,起始扇區分別是19240、19248和19256,物理上3個連續的塊。實際上這3個塊為內核線程kjournald寫的日志,分別描述塊(2405)、數據塊(2406)和提交塊(2407)。為了驗證,不妨看一下這三個塊的實際數據。
19240/8=2405
19248/8=2406
19256/8=2407
塊2405:
#define JFS_MAGIC_NUMBER 0xc03b3998U #define JFS_DESCRIPTOR_BLOCK 1 #define JFS_COMMIT_BLOCK 2 |
開始的4個字節為JFS_MAGIC_NUMBER,然后是block type:JFS_DESCRIPTOR_BLOCK。
塊2407:
的確是提交塊。
2.2 fsync的實現
既然fsync的開銷很大,就來看看代碼吧。
函數ext3_sync_file:
函數log_start_commit負責喚醒kjounald內核線程,log_wait_commit等待jbd事務提交完成。
從代碼來看,fsync的主要開銷在於調用log_wait_commit后的等待。也就是說fsync要等待kjournald把事務提交完成,才會返回。
到這里,我們已經知道了fsync開銷的主要來源:(1)硬件驅動層的開銷;(2)ext3寫日志。
另外,當log_start_commit返回0時,fsync就不會等待事務提交完成。到這里已經基本可以確認第2次fsync的開銷為什么那么小了——沒有wait事務提交。
下面驗證這一想法。為了方便調試,打開了內核jbd debug日志。
2.3 測試程序2
Write page 0 Fsync Write page 0 Fsync Write page 1 Fsync Write page 2 Fsync |
從第2個紅框的日志來看,第2次fsync時,的確是沒有wait的,所以開銷這么小,而其它3次fsync都調用了log_wait_commit函數。
問題4:第2次fsync為什么不會調用log_wait_commit?
因為掛載文件系統的時候,data=writeback,即寫數據本身不會寫jbd日志。第2次pwrite沒有引起文件擴展,只會修改ext3 inode的i_mtime,而i_mtime只精確到second,也就是說第2次pwrite不會引起inode信息改變,所以,不會生成jbd日志,也就不需要等待事務提交完成。
下面驗證一下該想法。
2.4 測試程序3
Write page 0 Fsync Sleep 1 second Write page 0 Fsync Write page 1 Fsync Write page 2 Fsync |
在第2次pwrite之前,sleep 1秒鍾,保證ext3 inode的i_mtime修改。
想法被證實了,第2次fsync的時間回到正常水平。
可以看到,第2次fsync調用提交了新的事務,並調用了log_wait_commit等待事務完成。
3 優化
如何優化fsync?是個難題。
(1)系統減少對fsync的調用。
(2)ext3日志放在更快的存儲介質,參考http://insights.oetiker.ch/linux/external-journal-on-ssd/
作者:YY哥
出處:http://www.cnblogs.com/hustcat/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
[root@localhost ~]# debugfs -R "stat ./test" /dev/sda2 debugfs 1.39 (29-May-2006) Inode: 3604481 Type: directory Mode: 0755 Flags: 0x0 Generation: 46195286 User: 502 Group: 503 Size: 4096 File ACL: 0 Directory ACL: 0 Links: 3 Blockcount: 8 Fragment: Address: 0 Number: 0 Size: 0 ctime: 0x5768c427 -- Mon Jun 20 21:35:51 2016 atime: 0x57725a43 -- Tue Jun 28 04:06:43 2016 mtime: 0x5768c427 -- Mon Jun 20 21:35:51 2016 BLOCKS: (0):3631328 TOTAL: 1
[root@localhost fs]# find / -name "*.c" | xargs grep "void file_update_time" -rn /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/fs/inode.c:1225:void file_update_time(struct file *file) /usr/src/kernels/linux-2.6.32/fs/inode.c:1460:void file_update_time(struct file *file)
void file_update_time(struct file *file) { struct inode *inode = file->f_path.dentry->d_inode; struct timespec now; enum { S_MTIME = 1, S_CTIME = 2, S_VERSION = 4 } sync_it = 0; /* First try to exhaust all avenues to not sync */ if (IS_NOCMTIME(inode)) return; now = current_fs_time(inode->i_sb); if (!timespec_equal(&inode->i_mtime, &now)) sync_it = S_MTIME; if (!timespec_equal(&inode->i_ctime, &now)) sync_it |= S_CTIME; if (IS_I_VERSION(inode)) sync_it |= S_VERSION; if (!sync_it) return; /* Finally allowed to write? Takes lock. */ if (mnt_want_write_file(file)) return; /* Only change inode inside the lock region */ if (sync_it & S_VERSION) inode_inc_iversion(inode); if (sync_it & S_CTIME) inode->i_ctime = now; if (sync_it & S_MTIME) inode->i_mtime = now; mark_inode_dirty_sync(inode); mnt_drop_write(file->f_path.mnt); } EXPORT_SYMBOL(file_update_time);
[root@localhost jbd]# find / -name "*.c" | xargs grep "int __log_start_commit" -rn /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/fs/jbd/journal.c:427:int __log_start_commit(journal_t *journal, tid_t target) /usr/src/kernels/linux-2.6.32/fs/jbd/journal.c:435:int __log_start_commit(journal_t *journal, tid_t target)
int __log_start_commit(journal_t *journal, tid_t target) { /* * Are we already doing a recent enough commit? */ if (!tid_geq(journal->j_commit_request, target)) { /* * We want a new commit: OK, mark the request and wakup the * commit thread. We do _not_ do the commit ourselves. */ journal->j_commit_request = target; jbd_debug(1, "JBD: requesting commit %d/%d\n", journal->j_commit_request, journal->j_commit_sequence); wake_up(&journal->j_wait_commit); return 1; } return 0; }
[root@localhost ~]# strace -f -F -T -r -p 5109 -e trace=write,open,read,fsync Process 5160 attached with 22 threads - interrupt to quit [pid 5160] 0.000000 open("./test/h.frm", O_RDONLY) = 18 <0.000049> [pid 5160] 0.000492 read(18, "\376\1\t\f\3\0\0\20\1\0\0000\0\0\20\0\5\0\0\0\0\0\0\0\0\0\0\2\10\0\10\0"..., 64) = 64 <0.000062> [pid 5160] 0.000312 read(18, "//\0\0 \0\0", 7) = 7 <0.000019> [pid 5160] 0.000120 read(18, "j\1\0\20\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 288) = 288 <0.000019> [pid 5160] 0.000154 read(18, "\0\0\0\0\2\0\377\0", 8) = 8 <0.000020> [pid 5160] 0.000299 read(18, "5\0\2\1\2\24) "..., 74) = 74 <0.000018> [pid 5160] 0.202203 fsync(9) = 0 <0.003163> [pid 5160] 0.003619 write(34, "P\262yW\2\1\0\0\0O\0\0\0'\1\0\0\10\0\1\0\0\0\0\0\0\0\4\0\0!\0"..., 206) = 206 <0.000050> [pid 5124] 0.060908 fsync(9) = 0 <0.003123> [pid 5126] 1.117089 fsync(4) = 0 <0.005403> [pid 5116] 0.008428 fsync(4) = 0 <0.000019> [pid 5126] 0.001892 fsync(4) = 0 <0.000019> [pid 5116] 0.001509 fsync(17) = 0 <0.002887> [pid 5126] 0.004133 fsync(4) = 0 <0.000020> [pid 5117] 0.002916 fsync(4) = 0 <0.000021> [pid 5111] 0.872229 fsync(9) = 0 <0.005205>
[] EXT4 debugging support [ ] JBD (ext3) debugging support JDB調試支持 如果你正在使用Ext3日志文件系統(或者其他文件系統/設備可能會潛在使用JBD),這個選項可以讓你在系統運行時開啟調試輸出,以便追蹤任何錯誤。默認地這些調試輸出是關閉的。 如果選Y,將可打開調試,使用echo N > /sys/kernel/debug/bd/jbd-debug,其中N是從1-5的數字,越高產生的調試輸出越多。要再次關閉,使用echo 0 > /sys/kernel/debug/jbd/jbd-debug [ ] JBD2 (ext4) debugging support JDB2調試支持 如果你正在使用Ext4日志文件系統(或者其他文件系統/設備可能會潛在使用JBD2),這個選項可以讓你在系統運行時開啟調試輸出,以便追蹤任何錯誤。默認地這些調試輸出是關閉的。 如果選Y,將可打開調試,使用echo N > /sys/kernel/debug/bd2/jbd2-debug,其中N是從1-5的數字,越高產生的調試輸出越多。要再次關閉,使用echo 0 > /sys/kernel/debug/jbd2/jbd2-debug
journal block device代碼分析
進入此門的肯定都對journal block device有一定了解,需要對ext3文件系統有了解,多余的就不贅述。
為什么要設計JBD?
普通數據是存在硬盤上的,文件系統也是作為普通數據存在硬盤上,類似如果碰到突然斷電的情況,硬盤就可能損壞,硬件損壞,還是要硬件設計保證,軟件設計(JBD)就是解決軟件錯誤,斷電可能會導致軟件錯誤,舉個例子,文件系統相當於常用的壓縮文件,普通數據則是其中一個txt中的文字,如果壓縮到一半被殺掉,如果txt中的文字損壞,壓縮文件仍能解壓,只是txt內容不同而已,但如果壓縮文件的結構被損壞,很可能解壓不來任何文件。而JBD就是防止文件系統的結構數據(元數據)被損壞,它作為一個緩存塊先緩存所有的元數據,如果磁盤數據異常后,就從緩存塊中恢復。
JBD的具體工作流程:
如上圖示,kernel正常讀寫磁盤,讀磁盤直接獲取,寫磁盤則走兩條路,每個IO群(即事務),先寫到jbd里面,然后在寫磁盤,如果寫磁盤被中斷,則從jbd恢復,如果jbd被中斷,OK,沒影響。jbd本身數據存儲到磁盤的一個用戶態不可見位置,即日志空間,日志空間本身是一個文件系統結構的存儲空間,有超級塊,組描述符,位圖等,估計所有數據系統都是類似結構。
基本原理就不說了,下面就以ext3_mkdir為例,描述jbd工作機制。
首先通過ext3_journal_start獲取原子操作handle,(原子操作即操作不可分割的,只有完成態和未開始狀態,不會停留在中間態,和atomic_inc不同,atomic加減是限制多線程沖突,handle則是保證完整性),具體細節可以參考ext3_journal_start函數,我對此的理解是,ext3_journal_start對handle進行了初始化,獲取當前journal空間的數據,比如,空閑字節的開始位置。
1
2
3
|
handle = ext3_journal_start(dir, EXT3_DATA_TRANS_BLOCKS(dir->i_sb) +
EXT3_INDEX_EXTRA_TRANS_BLOCKS + 3 +
EXT3_MAXQUOTAS_INIT_BLOCKS(dir->i_sb));
|
在后面ext3_new_inode函數中見handle傳遞進入,在ext3_new_inode中申請新inode,需要修改位圖,當然還有超級塊和組描述符等,下面截取位圖的寫入作為一個描述:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
bitmap_bh = read_inode_bitmap(sb, group);
if (!bitmap_bh)
goto fail;
ino = 0;
repeat_in_this_group:
ino = ext3_find_next_zero_bit((unsigned long *)
bitmap_bh->b_data, EXT3_INODES_PER_GROUP(sb), ino);
if (ino < EXT3_INODES_PER_GROUP(sb)) {
BUFFER_TRACE(bitmap_bh, "get_write_access");
err = ext3_journal_get_write_access(handle, bitmap_bh);
if (err)
goto fail;
if (!ext3_set_bit_atomic(sb_bgl_lock(sbi, group),
ino, bitmap_bh->b_data)) {
/* we won it */
BUFFER_TRACE(bitmap_bh,
"call ext3_journal_dirty_metadata");
err = ext3_journal_dirty_metadata(handle,
bitmap_bh);
if (err)
goto fail;
goto got;
}
|
通過read_inode_bitmap獲取位圖數據bitmap_bh,用ext3_find_next_zero_bit算出空閑ino位置,用ext3_journal_get_write_access獲取日志的寫權限,更多的是將handle加入事務transaction管理,或者說將bitmap_bh加入到journal管理中,然后才開始進行具體的數據修改,也就是ext3_set_bit_atomic修改位圖,修改完成使用ext3_journal_dirty_metadata標記為臟,即告訴journal本次handle操作結束,可以進行提交了。
ext3_new_inode下的組描述符也是類似,包括后面的目錄項修改都是如此,也不贅述了。
需要提到的是,此處標記為臟的是元數據,非元數據使用ext3_journal_dirty_data函數,在ext3里面,如果發現當前數據是臟頁,則直接進行刷新到磁盤,原因在注釋中有描述。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/*
* This buffer may be undergoing writeout in commit. We
* can't return from here and let the caller dirty it
* again because that can cause the write-out loop in
* commit to never terminate.
*/
if (buffer_dirty(bh)) {
get_bh(bh);
spin_unlock(&journal->j_list_lock);
jbd_unlock_bh_state(bh);
need_brelse = 1;
sync_dirty_buffer(bh);
jbd_lock_bh_state(bh);
spin_lock(&journal->j_list_lock);
/* Since we dropped the lock... */
if (!buffer_mapped(bh)) {
JBUFFER_TRACE(jh, "buffer got unmapped");
goto no_journal;
}
/* The buffer may become locked again at any
time if it is redirtied */
}
|
至此,一個使用journal的標准寫入過程結束,后續的就是提交了。
jbd有常駐線程kjournald負責提交transaction,kjournald線程每個ext系列的分區分一個,主要部分通過調用journal_commit_transaction完成。需要插播一下,如果編譯內核的時候打開CONFIG_JBD_DEBUG或者CONFIG_JBD2_DEBUG開關,就可以根據jbd-debug跟蹤jbd的執行過程,有更直接的感覺,在代碼實現上就是jbd_debug函數。
具體流程我建議打開debug開關后,對比着看,具體代碼不梳理了,直接上圖:
jbd前面所有設計都是為了此時的提交,需要留意的是此時設計的普通數據在元數據前進行提交,來保證ordered執行順序。另外在之前寫文件流程中提到ext3_ordered_write_end,中調用walk_page_buffers中journal_dirty_data_fn標記普通數據為臟,會將已臟的數據先用sync_dirty_buffer刷磁盤一下,可以對比參看。
最后則是出問題之后日志進行恢復:
journal恢復是在mount掛載磁盤的時候,ext3_fill_super()一直調用到journal_recover,判斷是否進行日志恢復也是如下判斷。
1
2
3
4
5
6
|
if (!sb->s_start) {
jbd_debug(1, "No recovery required, last transaction %dn",
be32_to_cpu(sb->s_sequence));
journal->j_transaction_sequence = be32_to_cpu(sb->s_sequence) + 1;
return 0;
}
|
即根據日志的超級塊s_start參數是否為0判斷。
整個恢復過程有3部分組成,都是調用do_one_pass,只是傳參不同,第一步獲取recovery_info信息,journal的起點和終點,journal是一個循環利用的環狀存儲介質。第二步獲取REVOKE塊,第三步PASS_REPLAY則根據描述符塊將日志信息寫到磁盤上。
另外提一下在工作中碰到一個案例:內核在寫文件的時候發生了多次復位,根據內核黑匣子記錄的信息,看到journal_bmap獲取信息為0,日志被__journal_abort_soft中斷 了,再寫journal出現了panic。當時看以為bmap出現異常,中間讀取有問題,后來把journal日志塊倒出來看,對應的一個間接索引塊里面全為0,在普通文件中是正常的,稱為文件的洞,而日志則是格式化一開始就全分配了,而且順序讀取利用不應產生文件的洞。具體原因再也沒找到,但是發現fsck不支持修改journal出現洞的問題,導致重復復位,后來找到社區高版本fsck比對一下,改了一個補丁,勉強算解決了問題。
以上都是開胃小菜,更多的請讀代碼,文章描述不細致的地方請參考jdb代碼分析
—結束—