有句話說得好,世上隻有兩種工具,一種是被人罵的,另一種是沒人用的。被罵得越多,側面反映出關注度越高,使用率越高,越用越成熟,這一點上, 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。
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的關系
存儲層面來說,我會引用社群三位專家的分享内容來說明。
上一代存儲多采用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下,增加的工作會增加延時,降低性能。
炫輝老師他們按照下面的場景在閃存卡上進行了測試。
在安全性層面,隻要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