一:為啥會有兩次寫?必要了解partial page write 問題 :
InnoDB 的Page Size一般是16KB,其數據校驗也是針對這16KB來計算的,將數據寫入到磁盤是以Page為單位進行操作的。而計算機硬件和操作系統,寫文件是以4KB作為單位的,那么每寫一個innodb的page到磁盤上,在os級別上需要寫4個塊.通過以下命令可以查看文件系統的塊大小.
dumpe2fs /dev/vda1 |grep "Block size" dumpe2fs 1.41.12 (17-May-2010) Block size: 4096
在極端情況下(比如斷電)往往並不能保證這一操作的原子性,16K的數據,寫入4K 時,發生了系統斷電/os crash ,只有一部分寫是成功的,這種情況下就是 partial page write 問題。有人會想到系統恢復后MySQL可以根據redolog 進行恢復,而mysql在恢復的過程中是檢查page的checksum,checksum就是pgae的最后事務號,發生partial page write 問題時,page已經損壞,找不到該page中的事務號,就無法恢復。
二 doublewrite 原理
書上這里沒有畫圖,直接介紹單頁面刷盤跟批量刷盤。

為了解決 partial page write 問題 ,
- 當mysql將臟數據flush到data file的時候, 先使用memcopy 將臟數據復制到內存中的double write buffer ,
- 通過double write buffer再分2次,每次寫入1MB到共享表空間,
- 然后馬上調用fsync函數,同步到磁盤上,避免緩沖帶來的問題。
在這個過程中,doublewrite是順序寫,開銷並不大,在完成doublewrite寫入后,在將double write buffer寫入各表空間文件,這時是離散寫入。如果發生了極端情況(斷電),InnoDB再次啟動后,發現了一個Page數據已經損壞,那么此時就可以從doublewrite buffer中進行數據恢復了。
兩次寫需要額外添加兩個部分:
- 內存中的兩次寫緩沖(doublewrite buffer),大小為2MB
- 磁盤上共享表空間中連續的128頁,大小也為2MB。其中120個用於批量寫臟頁,另外8個用於Single Page Flush。做區分的原因是批量刷臟是后台線程做的,不影響前台線程。而Single page flush是用戶線程發起的,需要盡快的刷臟頁並替換出一個空閑頁出來。
show status like "%InnoDB_dblwr%"; +----------------------------+------------+ | Variable_name | Value | +----------------------------+------------+ | Innodb_dblwr_pages_written | 2212378738 | | Innodb_dblwr_writes | 133881618 | +----------------------------+------------+
InnoDB_dblwr_pages_written:從bp flush 到 DBWB的個數
InnoDB_dblwr_writes:寫文件的次數
從這個數據來看,系統數據變更的頻率不是特別高。
三 單頁面刷盤
單一頁面刷盤,實際是mysql5.5版本中實現方式,MYSQL會在系統頁面,也就是ibdata的page5頁面(同時也是存儲事務信息的一個頁面中存儲兩次寫的信息),偏移位置就是頁面結束位置的200字節處,內容如下:
兩次寫總共包括2M(默認值)的數據,有2個BLOCK,那么每一個BLOCK是1M,每個頁面是16K,那么一個BLOCK包括64個頁面,正是一個簇的大小。,所以其實兩次寫頁面的空間是2個簇的空間。
除上面的信息需要持久化到文件中之外,還會有一個空間用來存儲這128個頁面的頁面信息,這是在內存中的,每次在刷盤前,都會把要刷盤的頁面信息臨時保存到數組中,這是一個長度為128的數組。這個緩存被稱為兩次寫緩存數組。
原理上面已經介紹過了。需要注意的是,buffer pool中的頁面,刷到真實文件的時是異步IO的,只有當自己刷到自己表空間的刷盤操作完成以后,兩次寫緩存數組的數據才能被覆蓋。
四 批量頁面刷盤
單一頁面的兩次寫,導致IO增多性能下降,mysql 5.7還有批量頁面刷盤方式。當buffer pool中的free list不足時,為了獲取一個空閑block,通常會觸發page驅逐操作.刷盤包括兩種方式:LRU方式和LIST方式。LRU就是系統把LRU列表找到最老的頁面,進行批量刷盤,講空間還原到空閑空間去。而當空間不足或者主線程在定時刷盤時,不需要區分頁面新舊狀態,只需要選擇LSN最小的頁面,從前到后刷一批到文件,就是LIST方式。
書上沒有展開討論,補充下相關知識點:
4.1 LRUlist
這里用到了順序表list來作為緩沖池,每個數據節點稱為block。LRU-list維護着最近不常用的頁面列表。該算法采用“中點插入法”:當插入一個新block時,移除表尾最近最少使用的block,在中點插入新block。這個中點將鏈表分為兩部分:
靠近表頭的一部分,為young區,這里的block是最近使用的節點 MRU
靠近表尾的一部分,為old區,這里的block是最近少使用的 LRU
MRU位於LRU-list的5/8之前,其余部分為LRU。也就是5/8之前存儲着常用的頁面,其余3/8為不常用頁面(可以修改innodb_old_blocks_pct系統變量值控制MRU與LRU之間的分割點,默認值為5/8)。刪除內容會在最不常用鏈表的末端刪除幾個頁面。刪除之前會進行刷臟。
當無法從LRU上獲得一個可替換的Page時,說明當前Buffer pool可能存在大量臟頁,這時候會觸發single page flush(buf_flush_single_page_from_LRU),即用戶線程主動去刷一個臟頁並替換掉。這是個慢操作,尤其是如果並發很高的時候,可能觀察到系統的性能急劇下降。除了single page flush外,在MySQL 5.7版本里還引入了多個page cleaner線程,根據一定的啟發式算法,可以定期且高效的的做page flush操作。
4.2 兩次寫組織結構
在兩次寫中,兩種刷盤算法對應的兩次寫空間互不影響,同時INNODB 自身的整個buffer pool分為多個Instance,每一個Instance管理屬於自己的一套兩次寫空間。書上給出了圖如下:

圖中每一個shard,其實是一個batch,參數是INNODB_double_write_batch_size。一個shard有一個數組,長度就是INNODB_double_write_batch_size。
批量刷臟過程
舉例為LRU方式,系統將當前頁加入到兩次寫緩存中,根據當前頁面所在的instance號及刷盤類型就找到對應的shard緩存,照后判斷shard緩存是否滿了(是否達到INNODB_double_write_batch_size),不滿將當前頁面追加到shard緩存中即可。不需要像單頁面那樣雙寫。
如果當前shard緩存已經滿了,則不得不把shard緩存的頁面寫入兩次寫文件中,再把兩次寫文件flush到磁盤中。最后將對應的真實頁面刷盤,但此時可能就是隨機寫入了。上面寫入連續的INNODB_double_write_batch_size個頁面,所以性能比連續多次,每次寫一個頁面也好很多。
五 總結
性能損耗
表面看上去,它是每個頁面都寫了2遍,會非常影響性能。但實際上,由於所寫的頁面會先緩存到內存中,因此每一部分緩存空間在滿了之后才會真正地寫入文件。並且doublewrite是一個連接的存儲空間,所以硬盤在寫數據的時候是順序寫,而不是隨機寫,這樣性能很高。doublewrite有效利用這個特地那,所以降低並不會相差1倍,經過測試,大概5-10%左右。當然,這是針對普通磁盤。對於目前比較流行的SSD來說,隨機寫已經不是問題,性能影響可能更小。
doublewrite默認開啟,參數skip_innodb_doublewrite雖然可以禁止使用doublewrite功能,但還是強烈建議大家使用doublewrite。避免部分寫失效問題,當然,如果你的數據表空間放在本身就提供了部分寫失效防范機制的文件系統上,如ZFS/FusionIO/DirectFS文件系統,在這種情況下,就可以不開啟doublewrite了。
其實兩次寫並不是什么特性或優點,它只是一個被動解決方案而已。這個問題的本質就是磁盤在寫入時,都是以512字節為單位,不能保證MySQL數據頁面16KB的一次性原子寫,所以才有可能產生頁面斷裂的問題。而目前有些廠商從硬件驅動層面做了優化,可以保證16KB(或其他配置)數據的原子性寫入。如果真是這樣,那么兩次寫就完全沒有必要了,取消兩次寫,才是最終級優化,值得期待。
兩次寫的作用
在數據庫啟動時(異常關閉的情況下),都會做數據庫恢復(redo)操作,恢復的過程中,數據庫都會檢查頁面是不是合法(校驗等等),如果發現一個頁面校驗結果不一致,則此時會用到兩次寫這個功能,這個特點也正是為了處理這樣的錯誤而設計的。
此時的操作很明白了,將兩次寫的2個BLOCK(簇)都讀出來,然后將所有這些頁面寫回到對應的頁面中去,那么這時可以保證這些頁面是正確的,並且是在寫入前已經更新過的(最新數據)。在寫回對應頁面中去之后,那么就可以在這基礎上繼續做數據庫恢復了,之后則不會再遇到這樣的問題了,因為已經將最后有可能產生寫斷裂的數據頁面都恢復了。
如果是寫doublewrite buffer本身失敗,那么這些數據不會被寫到磁盤,InnoDB此時會從磁盤載入原始的數據,然后通過InnoDB的事務日志來計算出正確的數據,重新寫入到doublewrite buffer。
