從MySQL中的double write問題說開去
有句話說得好,世上隻有兩種工具,一種是被人罵的,另一種是沒人用的。被罵得越多,側麵反映出關注度越高,使用率越高,越用越成熟,這一點上, MySQL就是一個很不錯的例子。而MySQL可支持的存儲引擎很多,目前以InnoDB最佳,算為上品。
自MySQL 5.5.5開始,InnoDB是作為默認的存儲引擎,而之前MyISAM存儲引擎其實也占有一席之地,但MySQL開發團隊自宣布MySQL 8.0.0開發裏程碑版本DMR開始,就把MySQL版本一下子從5.x跳躍到了8.0。其中的一個亮點就是事務性數據字典,完全脫離MyISAM存儲引擎,所以InnoDB寶刀不老,是我們學習MySQL重點需要了解的存儲引擎。而其中InnoDB的double write特性很有意思,也是我們今天討論的重點內容。
其實在MySQL和Oracle都會麵臨這類問題,不過各自有著不同的解決方案。我也看到網上有很多DBA在這個地方糾結、爭論。相比而言,Oracle這邊更沉默一些。我看了他們的討論,但目前為止還沒有看到一個把兩方麵都照顧到的解讀。所以我決定做這個事情,以此來對比MySQL和Oracle中的一些實現和差別。很多都是個人之言,所以有些說法不一定對,算是一次嚐試,希望引起一些思考和討論。
InnoDB中的double write
首先我們來說說InnoDB和double write。
InnoDB有三大閃亮特性:insert buffer、double write和自適應哈希,其實還有幾個比如異步IO、Flush neighbour Page(刷新鄰接頁),這個和係統層麵關聯性較高,所以三大亮點還是有普適性的。
首先我們來簡單了解一下double write為什麼要這麼設計、解決了什麼樣的問題。對此我畫了一個相對簡陋的圖,還有很多細節沒有照顧到,但是能夠說明意思。
總體來說,double write buffer就是一種緩衝緩存技術,主要的目的就是為了防止數據在係統斷電,異常crash情況下丟失數據。裏麵有幾個點需要注意的就是,數據在buffer pool中修改後成了髒頁,這個過程會產生Binglog記錄和redo記錄,當然緩存數據寫入數據文件是一個異步的工作。如果細看,在共享表空間(system tablespace)中會存在一個2M的空間,分為2個單元,一共128個頁,其中120個用於批量刷髒數據,另外8個用於Single Page Flush。
根據阿裏翟衛祥同學分析,之所以這樣做是因為批量刷髒是後台線程做的,這樣不影響前台線程。而Single Page Flush是用戶線程發起的,需要盡快地刷髒並替換出一個空閑頁出來。所以不是一個嚴格的64+64的拆分,最後也給出了這篇文章的鏈接。(https://yq.aliyun.com/articles/50627)
而數據刷新過程,是先使用memcopy把髒數據複製到內存中的double write buffer,分兩次寫完,每次寫1MB到共享表空間,然後就是調用fsync來同步到磁盤。這裏有一點需要注意的是,這個刷新到共享表空間的過程,雖然是兩次,但是是順序寫,所以開銷不會很大,也就不會像大家想象的那樣,覺得double write性能可能很差。根據Percona的測試,大概也就是5%左右的差別,數據重要還是性能更重要,這是一個基本的命題。當然後續會再寫入對應的表空間文件中,這個過程就是隨機寫,性能開銷就會大一些。所以早些時候試用SSD時很多人也帶有如此的顧慮,順序寫還是隨機寫,這個顧慮在這篇文章中也會有一些解釋。
當然double write這麼設計就是為了恢複而用,要不這麼大張旗鼓就不值得了。對於文件校驗來說,一個中心詞就是checksum。如果出現了partial write的時候,比如斷電,那麼兩次寫的過程中,很可能page是不一致的,這樣checksum校驗就很可能出現問題。而出現問題時,因為有了前期寫入共享表空間的頁信息,所以就可以重構出頁的信息重新寫入。
double write其實還有一個特點,就是將數據從double write buffer寫到真正的segment中時,係統會自動合並連接空間刷新的方式,這樣一來每次就可以刷新多個pages,從而提高效率。
比如下麵的環境,我們可以根據show status的結果來得到一個合並頁的情況。
> show status like'%dbl%';
+----------------------------+----------+
|Variable_name | Value |
+----------------------------+----------+
| Innodb_dblwr_pages_written | 23196544 |
| Innodb_dblwr_writes | 4639373 |
+----------------------------+----------+
通過InnoDB_dblwr_pages_written/InnoDB_dblwr_writes
或者通過指標也可基本看明白,這個例子中比例是5:1,證明數據變更頻率很低。
當然對於double write,在Percona中也在持續改進,在Percona 5.7版本中做了一個改進,你可以看到一個新參數,innodb_parallel_doublewrite_path。
|innodb_parallel_doublewrite_path | xb_doublewrite |
在係統層麵,也會存在一個30M的文件對應。
-rw-r----- 1 mysql mysql31457280Mar28 17:54 xb_doublewrite
這就是並行double write,實現了並行刷髒。關於這個特性的詳細描述和測試,可以參考以下鏈接:
https://www.percona.com/blog/2016/05/09/percona-server-5-7-parallel-doublewrite/?utm_source=tuicool&utm_medium=referral
裏麵提供了很多詳細的測試對比和分析。當然MariaDB、Facebook、Aurora在這方麵也有一些自己的實現方式和考慮。MariaDB是定製了新的參數innodb_use_atomic_writes來控製原子寫。當在啟動時檢查到支持atomic write時,即使開啟了innodb_doublewrite,也會關閉掉。
Facebook則是提供了一個選項,寫page之前,隻將對應的page number寫到dblwr中(不是寫全page),崩潰恢複讀出記錄在dblwr中的page號,間接恢複。
Aurora則是采用了存儲和數據庫服務器分離的方式來實現,無須開啟double write,有興趣的同學可以看一看。
到此為止,MySQL 層麵double write的解釋就差不多了。但我們肯定有一些疑問,因為partial write的問題是很多數據庫設計中都需要考慮到這麼一個臨界點的問題。MySQL中的頁是16k,數據的校驗是按照這個為單位進行的,而操作係統層麵的數據單位肯定達不到16k(比如是4k),那麼一旦發生斷電時,隻保留了部分寫入,如果是Oracle DBA一般對此都會很淡定,說用redo來恢複嘛。但可能我們被屏蔽了一些細節,MySQL在恢複的過程中一個基準是檢查page的checksum,也就是page的最後事務號,發生這種partial page write 的問題時,因為page已經損壞,所以就無法定位到page中的事務號,這個時候redo就無法直接恢複。
由此引申一點,partial write的問題在Oracle中肯定也會存在,隻是Oracle替我們把這個過程平滑做好了。其中有設計的差異,還有恢複技術的差別。但無論如何這個問題都不會繞過去,還是得解決。所以在此我需要和Oracle結合起來,來對比哪裏好,哪裏不好,這是一個很好的習慣和學習方法,為此我們引出兩個問題。
Oracle裏麵怎麼做?
要回答這個問題,就需要從以下兩個方麵來解讀。
-
Oracle中是否存在partial write?
-
Oracle是怎麼解決partial write的?
我們得把MySQL和Oracle放在一起,像拿著兩個玩具一般,左看右比,不光從外向對比還要看內部實現。有的同學說有些Internal的東西又用不著,看了也沒用,而且學起來很耗時間和精力。這個得辯證地看,很多東西掌握到了一定程度,就需要突破自己,深入理解總是沒壞處,這個過程是個潛移默化的過程。毛主席說:理論聯係實際、密切聯係群眾(在這裏就是我們的DBA和用戶)、批評與自我批評,很值得借鑒。
Oracle是否存在partial write
毫無疑問,Oracle中也是存在這種情況的,不過情況會有一些差別,處理方式不同。
我們先來看看一種很類似的說法,很多Oracle DBA和MySQL DBA總是在糾結這個地方。
Oracle裏麵有一種備份方式是熱備份(hot backup),就是在數據庫open狀態可以直接拷貝數據文件做備份,備份開始使用begin backup聲明,備份結束使用end backup結束。這個過程中很可能出現拷貝的文件發生數據變化,導致不一致的情況,被稱為split block。這一類數據塊也叫fractured block,在官方文檔11g中是這麼解釋的,而在10g的官方文檔描述是錯誤的。
fractured block
簡單來說就是在數據塊頭部和尾部的SCN不一致導致。在用戶管理方式的備份中,操作係統工具(比如cp命令)在DBWR正在更新文件的同時備份數據文件。
操作係統工具可能以一種半更新的狀態讀取塊,結果上半部分更新複製到了備份介質的塊,而下半部分仍包含較舊的數據。在這種情況下,這個塊就是斷裂的。
對於非RMAN備份的方式,ALTER TABLESPACE ... BEGIN BACKUP或ALTER DATABASE BEGIN BACKUP命令是斷裂塊問題的解決方案。當表空間處於熱備模式,並且對數據塊進行更改時,數據庫將在更改之前記錄整個塊鏡像的副本,以便數據庫可以在介質恢複發現該塊被破壞時重建該塊。
在10g中是被描述如下,注意下麵標紅的“每次”,這是文檔裏的一個錯誤描述。
當表空間處於熱備模式,並且每次對數據塊進行更改時,數據庫將在更改之前記錄整個塊鏡像的副本,以便數據庫可以在介質恢複發現該塊被破壞時重建該塊。
Jonathan Lewis這位大師對此做了進一步的闡釋,把話說得更明確了。
簡單翻譯一下就是:
官方文檔如果這麼說就錯了,在檢查點完成之後,將塊加載到緩存之後的第一次變更(或者緩存中任意塊的第一次更改),當前版本的塊信息會全量寫入redo,而數據塊在緩衝區中後續的變更不會重複寫。
文檔描述問題在10g文檔存在,在11g中做了修正。而實際應用中使用cp命令進行拷貝是因為寫入磁盤的是操作會使用文件係統塊大小作為最小IO,但是RMAN寫入磁盤的時候使用Oracle block size作為最小IO,所以不會出現split block。
為此我們來提一提Oracle中的數據塊。Oracle中block的大小大體有這幾類,分別是數據塊、重做日誌數據塊和控製文件數據塊。
-
數據塊data block,是讀寫數據文件的最小單位,默認是8KB,可以查詢select file#,name,block_size from v$datafile;
-
重做日誌數據塊叫作redo block,大小一般等於操作係統塊的大小,可以查詢select lebsz from x$kccle;
-
控製文件數據塊叫作control file block,可以查詢select block_size from v$controlfile。
由此我們擴展一個概念,在11g中redo添加了一個新的屬性blocksize。這個blocksize的值是在數據庫的源代碼中固定的,與操作係統相關,默認的值為512,在不同的操作係統中會有所不同。
查看blocksize的配置,可以使用基表x$kccle從Oracle的內部視圖中獲得:
SQL> select max(lebsz) from x$kccle;
MAX(LEBSZ)
----------
512
以上可以看出通過redo重構數據庫來恢複是沒有問題的,但是就涉及到一個很重要的概念,檢查點。
Oracle可以很自信地確認,如果數據做了commit而且成功返回,那麼下一秒斷電後數據是肯定能恢複的。光有自信不行,我們得有理論的支持說明,如何通過redo進行數據恢複。
Oracle如何通過redo進行恢複
我們假設redo寫的時候也是存在問題,即partial write。
在MySQL中有這樣的一個梗:因為page已經損壞,所以就無法定位到page中的事務號,所以這個時候redo就無法直接恢複。
Oracle怎麼做呢?看看下麵的圖,其實細看有一個文件很有意思,那就是控製文件。Oracle是有控製文件來做數據的檢查點,對控製文件描述得更形象一些,它就是數據庫的大腦,由此可見它的地位,盡管它的功能相對會比較單一,但是很重要。
用戶提交數據的變更之後,在Oracle寫入到數據文件中,這是一個異步的過程,但是同時從數據安全性方麵又需要保證數據不會丟失,在數據變更後會在redo log buffer中構造重做數據條目(redo entry),描述了修改前和修改後的數據變化。Lgwr會把重做條目刷入redo日誌,當然這個過程還要細分一下,分為後台寫和同步寫。
後台寫的觸發條件會多一些,比如3秒的間隔;或者數據還沒有刷新到redo日誌時,DBWR會觸發LGWR去寫,直至寫完;或者是達到日誌緩衝區1/3時觸發LGWR;或者是達到1M時觸發,還有其它更多的細節,可以移步官方文檔看看。
同步寫的觸發條件相對簡單,就是用戶commit時觸發LGWR去寫,所以說如果出現over commit的情況時,總是會有很明顯的log file sync的等待事件。
這個過程和CKPT有什麼關係呢?簡單來說,Oracle不斷地定位這個起點,這樣在不可預期的實例崩潰中能夠有效地保護並恢複數據,這也是CKPT的使命所在。這個起點如果太靠近日誌文件頭部就意味著要處理很多redo條目,恢複效率會很差;其次,這個起點不能太靠近日誌文件尾部,太靠近日誌文件尾部則說明隻有很少的髒數據塊沒有寫入數據,也就需要DBWR頻繁去刷數據。所以Oracle中會存在檢查點隊列的概念,就是一個LRU鏈表,上麵都是數據塊頭(buffer header),同時如果一個數據塊被修改了多次的話,在該鏈表上也隻出現一次,和Jonathan Lewis的解讀如出一轍。
而在MySQL中也是LRU的方式,控製方式更加清晰,可以通過參數innodb_lru_scan_depth控製LRU列表中可用頁數量,通過參數innodb_max_dirty_pages_pact來控製刷髒頁的頻率(默認是75,穀歌的壓測推薦是80)。
小結一下:就是CKPT是一個關鍵,會有檢查點隊列和增量檢查點來提高數據恢複的效率和減少DBWR頻繁刷盤。而這個所謂檢查點不光在redo、數據文件、數據文件頭,關鍵的是控製文件中也還會持續跟蹤記錄。這個就是我們數據恢複的基石SCN,在MySQL裏麵叫做LSN。
所以數據恢複時,從控製文件中發現數據文件的檢查點為空,意味著這是異常宕機,就會啟動crash recovery。這個檢查點在控製文件中會抓取到最近的,然後就是應用redo,達到一個奔潰前的狀態,就是常說的前滾,然後為了保證事務一致性,回滾那些未提交的事務,所以控製文件的設計就很有意義。以上就是一個較為粗略的恢複過程。
反問1: 批判與自我批判
好了,到翻盤的時候了,我相信很多MySQL DBA看到這裏會有更多疑問,我自我批判一下,應該是兩個問題。
-
MySQL雖然數據單位是頁16k,但是寫入redo log到文件的時候是以512字節為單位來寫的,這個你怎麼解釋
-
你說的Checkpoint技術MySQL也有。
這個理解完全沒錯,我來解釋一下。
MySQL InnoDB中也有檢查點LSN,會隨著log buffer的增長而增長。innodb_os_log_written是隨著redo log文件的寫入量而增長,可以通過show global status like '%Innodb_os_log_written%' 看到一個累計值,增量的差值即為512的倍數,所以單純看這裏我們看不出差異,盡管他們有不同粒度的細分。
MySQL InnoDB的檢查點技術很豐富,主要分為兩類,Sharp checkpoint和fuzzy checkpoint。
Sharp checkpoint是全量檢查點,通過參數innodb_fast_shutdown=1來設置,有點類似Oracle中的alter system checkpoint;而fuzzy checkpoint就豐富多了,總體來說是部分頁刷新,刷新的場景會有一些複雜。
-
Master Thread Checkpoint
-
FLUSH_LUR_LIST Checkpoint
-
Async/Sync Flush Checkpoint
-
Dirty Page too much Checkpoint
而回到問題的本質,那就是這些都是InnoDB層麵去做的檢查點,所以就會出現我們開始所說的情況。
因為page已經損壞,所以就無法定位到page中的事務號,所以這個時候redo就無法直接恢複。
而Oracle有控製文件這一層級,數據恢複都是在mount狀態下,掛載控製文件後開始的。
這個時候我們Oracle DBA再來反問一下MySQL DBA。
反問2: MySQL中為什麼Binlog和redo會並存
Binlog是MySQL Server範疇的。記錄的是數據的變更操作,支持多種存儲引擎。也就是說無論是MyISAM、InnoDB等存儲引擎,Binlog都會記錄,所以數據恢複和搭建slave經常會用到。另外根據二階段提交的場景,崩潰恢複也會用到Binlog。
而redo是InnoDB引擎範疇的,記錄的是記錄物理頁的修改,是做崩潰恢複所用。
總體來說,MySQL為了兼容其它事務引擎,在Server層引入了Binlog,這樣就能夠保證對所有的引擎啟用複製。同時一個事務提交會寫Binlog和redo,Binlog和redo的一致性也需要協調,主要是通過二階段提交來解決。
而Oracle是隻有redo,相當於把Binlog和redo的功能做了整合,因為Oracle不是插件式數據庫,不支持其它第三方的存儲引擎。所以這些都是從體係結構裏都統一了的。
所以對這個問題的總結就是:不要手裏拿著錘子,眼裏看到的都是釘子,技術架構不同使然。
InnoDB是插件式存儲引擎。事務支持是存儲引擎層麵來做,如果再多說一句,外鍵這種實現本來就不應該是存儲引擎做的,但是這是一個特例,因為Server層不支持,最後還是由InnoDB來實現了。簡單來說存儲引擎是麵向表的,而不是數據庫,明白了這一點很重要,也對InnoDB的定位會更加清晰。
但我們看待問題也不能孤立的看,不應該僅僅從數據庫層麵、係統層麵考慮,還需要考慮存儲層麵,也需要聽聽存儲界的觀點。
存儲和double write的關係
存儲層麵來說,我會引用社群三位專家的分享內容來說明。
首先是社群的一篇文章幹貨分享——《基於PCIe閃存卡的Oracle Online Redo Log優化》,裏麵這樣說道:
上一代存儲多采用512 bytes的扇區,現在的存儲則采用4k的扇區,扇區即每次最小IO的大小。4k 扇區有兩種工作模式:native mode 和emulation mode。
Native mode,即4k模式,物理和邏輯的block大小一樣,都是4096bytes。Native mode 的缺點是需要操作係統和軟件(如DB)的支持。Oracle 從11gR2 開始支持4k IO操作。Linux 內核在2.6.32 之後也開始支持4k IO操作。
emulation mode:物理塊是4k,但邏輯塊是512bytes。在該模式下,IO操作時底層物理還是4k進行操作,所以就會導致Partial I/O 和4k 對齊的問題。
在emulation mode下,每次IO操作大小是512 bytes,但存儲底層的IO操作大小必須是4k,如果要讀512 bytes的數據,實際需要讀4k,是原來的8倍,就是partial IO。而在寫時,也是先讀4k 的物理block,然後更新其中的512 bytes的數據,再把4k 寫回去。所以在emulation mode下,增加的工作會增加延時,降低性能。
對於SSD來說,double write會帶來兩個問題,性能懲罰和對SSD的磨損增加,這部分內容引用自社群之前的一次分享《閃存存儲特性以及數據庫相關優化思路》。
炫輝老師他們按照下麵的場景在閃存卡上進行了測試。
在安全性層麵,隻要Metadata Journal+DW或Metadata Journal+Data Journal(即上圖中的第2行和第3行數據),都可以保護數據庫數據的安全,也就是意外掉電數據不會損壞,數據庫可以正常啟動,數據不丟失。
但是在CPU bound(計算密集型) 的情況下,前個組合的性能衰減(8%)要小於後麵的保護組合(10%)。
如果是在IO bound(I/O密集型)的情況下,前個組合的性能衰減(10%)要小於後麵的保護組合(34%)。但是DW下的數據寫入量會比後者增加23%,也就是會增加SSD的磨損。這個是我們在應用時需要注意的。
而在文件係統層麵,我們還需要注意這些地方,以下內容摘自社群之前的分享《數據庫與存儲係統》。
絕大多數文件係統支持4k,(除了vxfs和zfs)。vxfs支持最大64k,可以設置成512byte,1k,2k,4k,8k,16k,32k,64k。
ZFS是一個特殊的怪物;數據塊是動態的,也就是說寫入多少數據,ZFS上那塊存放數據的塊就有那麼大。傳統上是支持動態的512byte到128k。
所以說ZFS本身就提供了部分寫失效防範機製,在這種情況下,就可以不開啟double write。
小結
MySQL和Oracle有時候想想真是有意思,一個開源,一個商業,一個最流行,一個最有範,看起來勢不兩立,但命運把他們又連接在一起。而我們學習起來多質疑,多思考,多嚐試一定會有所收獲。
原文發布時間為:2017-04-13
本文來自雲棲社區合作夥伴DBAplus
最後更新:2017-05-17 12:01:56