網易視頻雲是網易傾力打造的一款基於雲計算的分佈式多媒體處理集羣和專業音視頻技術,提供穩定流暢、低時延、高併發的視頻直播、錄製、存儲、轉碼及點播等音視頻的PAAS服務,在線教育、遠程醫療、娛樂秀場、在線金融等各行業及企業用戶只需經過簡單的開發即可打造在線音視頻平臺。現在,網易視頻雲的技術專家給大家分享一則技術文:linux軟raid的bitmap分析。
在使用raid1,raid5等磁盤陣列的時候,對於數據的可靠性有很高的要求,raid5在寫的時候需要計算校驗並寫入,raid1則寫源和鏡像來保證數據的一致性,在寫的過程中,有可能存在不穩定的因素,比如磁盤損壞,系統故障等,這樣導致寫入失敗,在系統恢復後,raid也需要進行恢復,傳統的恢復方式就是全盤掃描計算校驗或者全量同步,如果磁盤比較大,那同步恢復的過程會很長,有可能再發生其他故障,這樣就會對業務有比較大的影響。以raid1來說,在發生故障時,其實兩塊盤的數據很多都是已經一致的了,可能只有少部分不一致,所以就沒必要進行全盤掃描,但是系統並不知道兩塊盤哪些數據是一致的,這就需要在某個地方記錄哪些是已同步的,爲此,就誕生了bitmap,簡單來說,bitmap就是記錄raid中哪些數據是一致的,哪些是不一致的,這樣在raid進行恢復的時候就不用全量同步,而是增量同步了,從而減少了恢復的時間。
1. bitmap的使用
bitmap的使用比較簡單,mdadm的幫助文檔裏有很詳細的說明。bitmap分兩種,一種是internal,一種是external。
internal bitmap是存放在raid設備的成員盤的superblock附近(可以在之前也可以在之後),而external是單獨指定一個文件用來存放bitmap。
這裏簡單的介紹一下bitmap的使用。
# mdadm –create /dev/md/test_md1 –run –force –metadata=0.9 –assume-clean –bitmap=/mnt/test/bitmap_md1 –level=1 –raid-devices=2 /dev/sdb /dev/sdc
mdadm: /dev/sdb appears to be part of a raid array:
level=raid1 devices=2 ctime=Tue Dec 17 21:00:58 2013
mdadm: array /dev/md/test_md1 started.
查看md的狀態
#cat /proc/mdstat
Personalities : [raid1]
md126 : active raid1 sdc[1] sdb[0]
2097216 blocks [2/2] [UU]
bitmap: 1/257 pages [4KB], 4KB chunk, file: /mnt/test/bitmap_md1
可以看到最後一行就是md126的bitmap信息,這裏的默認bitmap的chunksize是4KB,可以通過–bitmap-chunk來指定bitmap chunk大小,bitmap的chunk表示在bitmap中
1bit對應md設備的一個chunk(大小爲bitmap的chunksize)。
這裏對cat /proc/mdstat查看到的bitmap的信息進行說明。
其中的4KB chunk表示bitmap的chunk大小是4KB;
1/257 pages指的是bitmap所對應的內存位圖(作爲磁盤上的bitmap的緩存,提高對位圖的操作效率),257是內存bitmap佔的總page數,1表示已經分配的page數,內存bitmap是動態分配的,使用完後就可以回收。內存位圖使用16bit來表徵一個chunk,其中的14bit用來統計該chunk上正在進行的寫io數(後面會有詳細的介紹)。
[4KB]表示已經分配的內存位圖page總大小。
總的chunk數=md設備大小/bitmap的chunk大小
內存bitmap一個page可以表示的chunk數=PAGE_SIZE*8/16
內存bitmap佔的總page數=總的chunk數/內存bitmap一個page可以表示的chunk數
上面給出的例子中總的chunk數爲2097216KB/4KB=524304
取頁大小爲4096,一個page可以表示的chunk數爲4096*8/16=2048
總共需要524304/2048=256個page,看實際上是257,這是因爲有可能不能整除的情況,最後一個page可能不是全部都使用。
2. bitmap的內存結構
bitmap的結構體有比較多的字段,這裏關注幾個重要的字段,加以說明,便於後面的分析。
struct bitmap {
struct bitmap_page *bp; /* 指向內存位圖頁的結構*/
……
unsigned long chunks; /* 陣列總的chunk數 */
……
struct file *file; /* bitmap文件 */
……
struct page **filemap; /* 位圖文件的緩存頁 */
unsigned long *filemap_attr; /* 位圖文件緩存頁的屬性 */
……
};
其中struct bitmap_page結構如下:
struct bitmap_page {
char *map; /* 指向實際分配的內存頁*/
/*
* in emergencies (when map cannot be alloced), hijack the map
* pointer and use it as two counters itself
*/
unsigned int hijacked:1;
/*
* count of dirty bits on the page
*/
unsigned int count:31; /* 該頁上有多少髒的chunk,每16bit表示一個chunk*/
};
實際動態分配的每個內存頁,每16bit對應bitmap file的一個bit,即表示md的一個chunk
這16個bit的作用如下:
15 14 13 0
+————-+———-+————————————-+
| needed |resync | counter |
+————-+———-+————————————+
最高一位表示是否需要同步,後面一位表示是否正在同步,低14bit是counter,用來統計該chunk有多少正在進行的寫io。
這14bit表示的counter記爲bmc,方便後面描述。bmc的值0,1,2比較特殊,爲0時表示對應chunk還未進行寫操作,內存位圖還未置位,bmc爲1時表示內存位圖已經置位,bmc爲2表示所有寫操作剛結束,真正的寫io數是從2開始累加的。
bitmap結構中的filemap表示bitmap file的對應緩存,bitmap file有多大,對應的這個filemap緩存就有多大,在初始化的時候就分配好的。
filemap_attr 表示位圖文件緩存頁的屬性,使用4bit來表示一個緩存頁的屬性,
第0bit是BITMAP_PAGE_DIRTY,該bit爲1表示內存bitmap中爲髒,但是bitmap file中的對應位不爲髒,因此對於有這種標記的page需要同步刷到磁盤(實際上是異步調用write_page,但是等到寫完成)
第1bit是BITMAP_PAGE_PENDING,置位後表示內存bitmap中的髒位已經清0,但是此時外存bitmap file中的對應髒位沒有清0,需要進行清0的操作,這是一個過渡狀態,過渡到BITMAP_PAGE_NEEDWRITE。
第2bit是BITMAP_PAGE_NEEDWRITE,置位後表示需要進行同步,把內存位圖緩存中的數據刷到外部位圖文件中,所對於這種標記的page只需要異步寫,因爲即使寫失敗,最多帶來額外的同步,不會帶來數據的危害。
第3bit在代碼中沒有看到使用,猜測是預留的。
bp中的page和filemap對應的page以及bitmap file的關係如下:
1個位圖文件緩存頁可以表示4096個chunk,而內存位圖頁則需要16個page。bp數組所對應的內存位圖頁的作用其實是控制bitmap的置位與復位,並且也控制一個chunk上的io不能超過最大值(14bit表示的最大整數),達到最大值的時候會進行io schedule。
3.bitmap的可靠刷新機制
在進行寫操作的時候,是先把bitmap的對應位置爲髒,然後再進行寫操作,寫完成後再復位。那麼如何保證每次寫操作時,內存bitmap中的數據都被可靠刷到對應的磁盤bitmap file中?
一般的邏輯是在寫io到md設備前,先在bitmap中標記爲dirty(成功刷到磁盤中),然後執行寫io,io完成後需要清理dirty的標記這就需要在正常數據寫操作之前,完成bitmap的刷新操作。那麼bitmap如何實現呢?
以raid1爲例,md在make_request中,調用了bitmap_startwrite函數,但是這個函數並沒有直接調用write_page刷新數據到磁盤,而是調用了bitmap_file_set_bit將bitmap位標記爲BITMAP_PAGE_DIRTY。之所以不在bitmap_startwrite函數中調用write_page刷新,是因爲塊設備的io操作是通過queue隊列進行的,不能保證每次io操作都能及時完成,而且io調度的順序也可能調換,因此如果直接調用write_page進行寫操作的話,就有可能存在bitmap的刷新和正常的數據寫操作的順序發生顛倒。
真正處理BITMAP_PAGE_DIRTY是在bitmap_unplug中,對於raid1來說,bitmap_unplug是在raid1.c中的flush_pending_writes函數中調用的,而flush_pending_writes是由raid1的守護進程raid1d調用的。flush_pending_writes會調用bitmap_unplug刷新bitmap到磁盤,然後遍歷conf->pending_bio_list,取出bio來處理正常掛起的寫io。(在raid1的make_request中會把mbio加到conf->pending_bio_list中)
從上面的分析可知,raid1在收到寫io請求時,先把內存位圖置爲dirty,並把該寫io加到pending_list,然後raid1d守護進程會把標記爲dirty的內存位圖頁刷到外存的位圖文件中,然後從pendling_list中取出之前掛起的寫io進行處理。
在刷髒頁時,需要把位圖文件緩存頁的數據寫到位圖文件中,因爲md是內核態的程序,在實現時並沒有直接調用通常的寫函數往外存的文件寫數據,而是通過bmap機制,根據inode,把文件數據塊和物理磁盤塊映射起來,這樣就可以透過文件系統,調用submit_bh進行bitmap的刷新。
上面描述的可靠刷新機制也就是bitmap設置的過程,下面分析bitmap清理的邏輯。
4.bitmap位的清除
前面說到從pending_list中取出寫io進行處理,當io完成後需要清除dirty的標記,會把內存位圖頁的屬性設置爲BITMAP_PAGE_PENDING,表示正要去清除,BITMAP_PAGE_PENDING屬性頁並不會立即刷到外存的位圖文件中,而是異步清0的過程。真正的清理流程在bitmap_daemon_work中實現。這是有raid的守護進程定期執行時調用的(比如raid1d),守護進程會定期調用md_check_recovery,然後md_check_recovery會調用bitmap_daemon_work根據各種狀態進行清0的操作。
bitmap_daemon_work的實現比較複雜,裏面各種狀態判斷與轉換,很容易把人繞暈,bitmap的清0(內存位圖頁的bit清0及刷到外存的位圖文件)是需要經過3次調用bitmap_daemon_work。下面以1個bit的清理來闡述,在io完成後,在bitmap_endwrite中會把這邊bit的計數器bmc會置爲2(前提是這個bit對應的chunk上的寫io都完成),並標記位爲BITMAP_PAGE_PENDING。
1)第一次進入bitmap_daemon_work,bmc=2,頁屬性爲BITMAP_PAGE_PENDING。
這裏判斷位是否爲BITMAP_PAGE_PENDING,這時候bit所對應的確實是BITMAP_PAGE_PENDING,所以跳過這個判斷裏的處理邏輯
if (!test_page_attr(bitmap, page, BITMAP_PAGE_PENDING)) {
int need_write = test_page_attr(bitmap, page,
BITMAP_PAGE_NEEDWRITE);
if (need_write)
clear_page_attr(bitmap, page, BITMAP_PAGE_NEEDWRITE);spin_unlock_irqrestore(&bitmap->lock, flags);
if (need_write)
write_page(bitmap, page, 0);
spin_lock_irqsave(&bitmap->lock, flags);
j |= (PAGE_BITS – 1);
continue;
}
接着執行後續的,
這裏會判斷page是否爲BITMAP_PAGE_NEEDWRITE,但是這個時候的page不是BITMAP_PAGE_NEEDWRITE,所以進入else的處理,
把頁標記爲BITMAP_PAGE_NEEDWRITE
if (lastpage != NULL) {
if (test_page_attr(bitmap, lastpage,
BITMAP_PAGE_NEEDWRITE)) {
clear_page_attr(bitmap, lastpage,
BITMAP_PAGE_NEEDWRITE);
spin_unlock_irqrestore(&bitmap->lock, flags);
write_page(bitmap, lastpage, 0);
} else {
set_page_attr(bitmap, lastpage,
BITMAP_PAGE_NEEDWRITE);
bitmap->allclean = 0;
spin_unlock_irqrestore(&bitmap->lock, flags);
}
}
繼續執行,bmc爲2,會把bmc設置爲1,並且再設置一次BITMAP_PAGE_PENDING
if (*bmc) {
if (*bmc == 1 && !bitmap->need_sync) {
/* we can clear the bit */
*bmc = 0;
bitmap_count_page(bitmap,
(sector_t)j << CHUNK_BLOCK_SHIFT(bitmap),
-1);
/* clear the bit */
paddr = kmap_atomic(page, KM_USER0);
if (bitmap->flags & BITMAP_HOSTENDIAN)
clear_bit(file_page_offset(bitmap, j),
paddr);
else
__clear_bit_le(
file_page_offset(bitmap,
j),
paddr);
kunmap_atomic(paddr, KM_USER0);
} else if (*bmc <= 2) {
//進入這裏把bmc設置爲bmc=1
*bmc = 1; /* maybe clear the bit next time */
set_page_attr(bitmap, page, BITMAP_PAGE_PENDING);
bitmap->allclean = 0;
}
第一次調用結束。
2)第二次進入bitmap_daemon_work,bmc=1,頁屬性爲BITMAP_PAGE_PENDING和BITMAP_PAGE_NEEDWRITE。
這樣就會走到下面的流程,把BITMAP_PAGE_PENDING清掉
if (*bmc == 1 && !bitmap->need_sync) {
/* we can clear the bit */
*bmc = 0;
bitmap_count_page(bitmap,
(sector_t)j << CHUNK_BLOCK_SHIFT(bitmap),
-1);
/* clear the bit */
// 這裏纔是真正的位圖文件緩存頁bit位清0的地方
paddr = kmap_atomic(page, KM_USER0);
if (bitmap->flags & BITMAP_HOSTENDIAN)
clear_bit(file_page_offset(bitmap, j),
paddr);
else
__clear_bit_le(
file_page_offset(bitmap,
j),
paddr);
kunmap_atomic(paddr, KM_USER0);
}
2)第三次進入bitmap_daemon_work,bmc=1,頁屬性爲BITMAP_PAGE_NEEDWRITE。
會走到下面的流程,清掉BITMAP_PAGE_NEEDWRITE,然後調用write_page刷到磁盤中,至此,清理操作才完成,
總共需要調用三次bitmap_daemon_work才能完成一個bit的清0操作。
if (!test_page_attr(bitmap, page, BITMAP_PAGE_PENDING)) { int need_write = test_page_attr(bitmap, page, BITMAP_PAGE_NEEDWRITE); if (need_write) clear_page_attr(bitmap, page, BITMAP_PAGE_NEEDWRITE);spin_unlock_irqrestore(&bitmap->lock, flags);
if (need_write)
write_page(bitmap, page, 0);
spin_lock_irqsave(&bitmap->lock, flags);
j |= (PAGE_BITS – 1);
continue;
}
這種異步清零的機制好處在於,在還未清零或者內存位圖清0但沒有刷到磁盤的時候,又有對該頁的寫請求到來,就只用增加bmc計數器或者只是把內存位圖置位,而不用再寫到外存的位圖文件中,從而減少了一次寫外存位圖的io。
更多技術交流,請關注我們哦!後續會更新更多的技術文章!