關於redo 和 undo log
InnoDB 有兩塊非常重要的日志,一個是undo log,另外一個是redo log,前者用來保證事務的原子性以及InnoDB的MVCC,後者用來保證事務的持久性。和大多數關系型數據庫一樣,InnoDB記錄了對數據文件的物理更改,並保證總是日志先行,也就是所謂的WAL(Write Ahead Log),即在持久化數據文件前,保證之前的redo日志已經寫到磁盤
一、概念
1、Innodb Crash Recovery
這是InnoDB引擎的一個特點,當故障發生,重新啟服務後,會自動完成恢復操作,將數據庫恢復到之前一個正常狀態(不需要重做所有的日志,只需要執行上次刷入點之後的日志,這個點就叫做Checkpoint)恢復過程有兩步
第一步:檢查redo日志,將之前完成並提交的事務全部重做;
第二步:將undo日志中,未完成提交的事務,全部取消
2、LSN
LSN(log sequence number) 用於記錄日志序號,它是一個不斷遞增的 unsigned long long 類型整數。
在 InnoDB 的日志系統中,LSN 無處不在,它既用於表示修改髒頁時的日志序號,也用於記錄checkpoint,通過LSN,可以具體的定位到其在redo log文件中的位置。
LSN 用字節偏移量來表示。每個page有LSN,redo log也有LSN,Checkpoint也有LSN。可以通過命令show engine innodb status來觀察:
---
LOG
---
Log sequence number 33646077360
Log flushed up to 33646077360
Last checkpoint at 33646077360
0 pending log writes, 0 pending chkp writes
49687445 log i/o's done, 1.25 log i/o's/second
為了管理髒頁,在 Buffer Pool 的每個instance上都維持了一個flush list,flush list 上的 page 按照修改這些 page 的LSN號進行排序。因此定期做redo checkpoint點時,選擇的 LSN 總是所有 bp instance 的 flush list 上最老的那個page(擁有最小的LSN)。由於采用WAL的策略,每次事務提交時需要持久化 redo log 才能保證事務不丟。而延遲刷髒頁則起到了合並多次修改的效果,避免頻繁寫數據文件造成的性能問題。
3、Dirty page
在InnoDB中,寫到bp裡面的數據一方面可以加快數據處理速度,同時也會造成數據的不一致(RAM vs DISK)
1、在對用戶每次有導致數據變更的請求中,Innodb引擎把數據和索引都載入到內存中的緩沖池(buffer pool)中,如果每次修改數據和索引都需要更新到磁盤,必定會大大增加I/O請求
因為每次更新的位置都是隨機的,磁頭需要頻繁定位導致效率低;數據暫放在內存中,也一定程度的提高了讀的速度。所以Innodb每處理完一個請求(Transaction)後只添加一條日志log,另外有一個線程負責智能地讀取日志文件並批量更新到磁盤上,實現最高效的磁盤寫入。innodb既然利用Mem buffer提高相應的速度,那當然也會帶來數據不一致,術語為髒數據,mysql稱之為dirty page。
發生過程:當事務(Transaction)需要修改某條記錄(row)時,InnoDB需要將該數據所在的page從disk讀到buffer pool中,事務提交後,InnoDB修改page中的記錄(row)。這時buffer pool中的page就已經和disk中的不一樣了,mem中的數據稱為髒數據(dirty page)。
2、在每次事務commit的時候,就立刻將事務更改操作記錄到redo log。所以即使buffer pool中的dirty page在斷電時丟失,InnoDB在啟動時,仍然會根據redo log中的記錄完成數據恢復,redo log的另一個作用是,通過延遲dirty page的flush最小化磁盤的random writes。(redo log會合並一段時間內TRX對某個page的修改)
3、正常情況下,dirty page什麼時候flush到disk上
redo log是一個環(ring)結構,當redo空間占滿時,將會將部分dirty page flush到disk上,然後釋放部分redo log。這種情況可以通過Innodb_log_wait(SHOW GLOBAL STATUS)觀察,情況發生該計數器會自增一次
當需要在Buffer pool分配一個page,但是已經滿了,並且所有的page都是dirty的(否則可以釋放不dirty的page),通常是不會發生的。這時候必須flush dirty pages to disk。這種情況將會記錄到Innodb_buffer_pool_wait_free中。一般地,可以可以通過啟動參數innodb_max_dirty_pages_pct控制這種情況,當buffer pool中的dirty page到達這個比例的時候,將會強制設定一個checkpoint,並把dirty page flush到disk中
檢測到系統空閒的時候,會flush,每次64 pages
涉及的InnoDB配置參數:innodb_flush_log_at_trx_commit、innodb_max_dirty_pages_pct;狀態參數:Innodb_log_wait、Innodb_buffer_pool_wait_free
4、dirty page既然是在Buffer pool中,那麼如果系統突然斷電Dirty page中的數據修改是否會丟失
例如如果一個用戶完成一個操作(數據庫完成了一個事務,page已經在buffer pool中修改,但dirty page尚未flush),這時系統斷電,buffer pool數據全部消失。那麼這個用戶完成的操作(導致的數據庫修改)是否會丟失呢?答案是不會(innodb_flush_log_at_trx_commit=1)。這就是redo log要做的事情,在disk上記錄更新(buffer pool中的數據並不是永久性)
系統故障造成數據庫不一致的原因有兩個:
未完成事務對數據庫的更新可能已寫入數據庫
已提交事務對數據庫的更新可能還留在緩沖區沒來得及寫入數據庫
在這裡我們先說恢復的一般方法:
正向掃描日志文件(從頭到尾),找出故障發生前已經提交的事務(存在begin transaction和commit記錄),將其標識記入重做(redo)隊列。同時找出故障發生時未完成的事務(只有begin transaction,沒commit),將其標識記入(undo)隊列
對undo隊列的各事務進行撤銷處理。進行undo的處理方法是,反向掃描日志文件,對每個undo事務的更新操作執行反操作,即將日志記錄中“更新前的值”寫入數據庫
對重做日志中的各事務進行重做操作。進行redo的處理方法是,正向掃描日志,對每個redo事務重新執行日志文件登記操作。即將日志中“更新後的值”寫入數據庫
innodb_flush_log_at_trx_commit
默認值1的意思是每一次事務提交或事務外的指令都需要把日志寫入(flush)硬盤,這是很費時。特別是使用電池供電緩存(Battery backed up cache)時。設成2對於很多運用,特別是從MyISAM表轉過來的是可以的,它的意思是不寫入硬盤而是寫入系統緩存。日志仍然會每秒flush到硬盤,所以一般不會丟失超過1-2秒的更新。設成0會更快一點,但安全方面比較差,即使MySQL掛了也可能會丟失事務的數據。而值2只會在整個操作系統掛了時才可能丟數據。
innodb_max_dirty_pages_pct
his is an integer in the range from 0 to 100. The default value is 90. The main thread in InnoDB tries to write pages from the buffer pool so that the percentage of dirty (not yet written) pages will not exceed this value.
4、ACID
原子性(Atomicity):事務中的所有操作,要麼全部完成,要麼不做任何操作,不能只做部分操作。如果在執行的過程中發生了錯誤,要回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過
事務的持久性(Durability):事務一旦完成,該事務對數據庫所做的所有修改都會持久的保存到數據庫中。為了保證持久性,數據庫系統會將修改後的數據完全的記錄到持久的存儲上
5、Log buffer
日志在內存裡也是有緩存的,這裡將其叫做log buffer。磁盤上的日志文件稱為log file。log file一般是追加內容,可以認為是順序寫,順序寫的磁盤IO開銷要小於隨機寫
當buffer cache中的數據塊被修改後,服務器進程生成redo數據並寫入到redo log buffer中。當滿足以下條件時,LGWR會將redo log buffer中的條目開始寫入在線重做日志:
redo log buffer滿1/3
每3秒超時(Timeout
log_buffer中的數據到達1M
事務提交時
如果我們的系統擁有快速處理器和I/O相對較慢的磁盤,處理器可能會填滿緩存區的其余空間,這時會促使LGWR移動緩沖區的部分數據到磁盤。在這種情況下,較大的日志緩沖區能夠臨時掩蓋 較慢磁盤對系統帶來的影響。你這可做下面的選擇:
提升checkpoint或歸檔進程
提升LGWR性能(也許你可以將所有的在線重做日志放置到速度更快的裸設備)
合理使用redo log buffer
執行批量操作時使用批量提交,以至LGWR能夠更高效的寫入重做條目到在線重做日志文件中
當加載大量數據時,使用nologging操作
設置Log Buffer
redo log buffer由初始化參數LOG_BUFFER決定,修改該參數需要重啟實例。適當的redo log buffer參數值能夠明顯的提升系統吞吐量,尤其對於插入,更新,刪除大數據量的系統。默認的log buffer值:MAX(0.5M,(128K*number of cpus)),通常默認值是足夠的。增加log buffer的值對系統性能或可恢復性不會產生負面影響,而僅僅會使用額外的內存。
log_buffer一般在3-5M就足夠了。超過3-5M,僅僅是浪費內存;當然太小了,也可能影響性能。在內存不太昂貴的今天,且如果你有大量“大事務”,log_buffer就設定為5M吧。
二、Undo log
Undo log是InnoDB MVCC事務特性的重要組成部分。當我們對記錄做了變更操作時就會產生undo記錄,Undo記錄默認被記錄到系統表空間(ibdata)中,但從5.6開始,也可以使用獨立的Undo 表空間
Undo記錄中存儲的是老版本數據,當一個舊的事務需要讀取數據時,為了能讀取到老版本的數據,需要順著undo鏈找到滿足其可見性的記錄。當版本鏈很長時,通常可以認為這是個比較耗時的操作(例如bug#69812)。
大多數對數據的變更操作包括INSERT/DELETE/UPDATE,其中INSERT操作在事務提交前只對當前事務可見,因此產生的Undo日志可以在事務提交後直接刪除(誰會對剛插入的數據有可見性需求呢!!),而對於UPDATE/DELETE則需要維護多版本信息,在InnoDB裡,UPDATE和DELETE操作產生的Undo日志被歸成一類,即update_undo。
原理
Undo Log的原理很簡單,為了滿足事務的原子性,在操作任何數據之前,首先將數據備份到一個地方(這個存儲數據備份的地方稱為Undo Log)。然後進行數據的修改。如果出現了錯誤或者用戶執行了ROLLBACK語句,系統可以利用Undo Log中的備份將數據恢復到事務開始之前的狀態。
除了可以保證事務的原子性,Undo Log也可以用來輔助完成事務的持久化。
Undo Log 是為了實現事務的原子性,在MySQL數據庫InnoDB存儲引擎中,還用Undo Log來實現多版本並發控制(簡稱:MVCC)。
用Undo Log實現原子性和持久化的事務的簡化過程
假設有A、B兩個數據,值分別為1,2。
A.事務開始.
B.記錄A=1到undo log.
C.修改A=3.
D.記錄B=2到undo log.
E.修改B=4.
F.將undo log寫到磁盤。
G.將數據寫到磁盤。
H.事務提交
這裡有一個隱含的前提條件:‘數據都是先讀到內存中,然後修改內存中的數據,最後將數據寫回磁盤’,之所以能同時保證原子性和持久化,是因為以下特點:
A. 更新數據前記錄Undo log。
B. 為了保證持久性,必須將數據在事務提交前寫到磁盤。只要事務成功提交,數據必然已經持久化。
C. Undo log必須先於數據持久化到磁盤。如果在G,H之間系統崩潰,undo log是完整的,可以用來回滾事務。
D. 如果在A-F之間系統崩潰,因為數據沒有持久化到磁盤。所以磁盤上的數據還是保持在事務開始前的狀態。
缺陷:每個事務提交前將數據和Undo Log寫入磁盤,這樣會導致大量的磁盤IO,因此性能很低,如果能夠將數據緩存一段時間,就能減少IO提高性能。但是這樣就會喪失事務的持久性。
三、Redo Log
Innodb的事務日志是指Redo log,保存在日志文件ib_logfile*裡面
redo log可以通過參數innodb_log_files_in_group配置成多個文件,另外一個參數innodb_log_file_size表示每個文件的大小。因此總的redo log大小為innodb_log_files_in_group * innodb_log_file_size,Redo log文件以ib_logfile[number]命名,日志目錄可以通過參數innodb_log_group_home_dir控制。
Redo log 以順序的方式寫入文件文件,寫滿時則回溯到第一個文件,進行覆蓋寫。(但在做redo checkpoint時,也會更新第一個日志文件的頭部checkpoint標記,所以嚴格來講也不算順序寫)
Redo log文件是循環寫入的,在覆蓋寫之前,總是要保證對應的髒頁已經刷到了磁盤。在非常大的負載下,Redo log可能產生的速度非常快,導致頻繁的刷髒操作,進而導致性能下降,通常在未做checkpoint的日志超過文件總大小的76%之後,InnoDB 認為這可能是個不安全的點,會強制的preflush髒頁,導致大量用戶線程stall住。如果可預期會有這樣的場景,我們建議調大redo log文件的大小。可以做一次干淨的shutdown,然後修改Redo log配置,重啟實例。
原理
和Undo Log相反,Redo Log記錄的是新數據的備份。在事務提交前,只要將Redo Log持久化即可,不需要將數據持久化。當系統崩潰時,雖然數據沒有持久化,但是Redo Log已經持久化。系統可以根據Redo Log的內容,將所有數據恢復到最新的狀態。
Undo + Redo事務的簡化過程
假設有A、B兩個數據,值分別為1,2.
A.事務開始.
B.記錄A=1到undo log.
C.修改A=3.
D.記錄A=3到redo log.
E.記錄B=2到undo log.
F.修改B=4.
G.記錄B=4到redo log.
H.將redo log寫入磁盤。
I.事務提交
Undo + Redo事務的特點
A. 為了保證持久性,必須在事務提交前將Redo Log持久化。
B. 數據不需要在事務提交前寫入磁盤,而是緩存在內存中。
C. Redo Log 保證事務的持久性。
D. Undo Log 保證事務的原子性。
E. 有一個隱含的特點,數據必須要晚於redo log寫入持久存儲。
四、IO性能
Undo + Redo的設計主要考慮的是提升IO性能。雖說通過緩存數據,減少了寫數據的IO,但是卻引入了新的IO,即寫Redo Log的IO。如果Redo Log的IO性能不好,就不能起到提高性能的目的。為了保證Redo Log能夠有比較好的IO性能,InnoDB 的 Redo Log的設計有以下幾個特點:
A. 盡量保持Redo Log存儲在一段連續的空間上。因此在系統第一次啟動時就會將日志文件的空間完全分配。以順序追加的方式記錄Redo Log,通過順序IO來改善性能。
B. 批量寫入日志。日志並不是直接寫入文件,而是先寫入redo log buffer.當需要將日志刷新到磁盤時(如事務提交),將許多日志一起寫入磁盤.
C. 並發的事務共享Redo Log的存儲空間,它們的Redo Log按語句的執行順序,依次交替的記錄在一起,以減少日志占用的空間。例如,Redo Log中的記錄內容可能是這樣的:
記錄1: <trx1, insert …>
記錄2: <trx2, update …>
記錄3: <trx1, delete …>
記錄4: <trx3, update …>
記錄5: <trx2, insert …>
D. 因為C的原因,當一個事務將Redo Log寫入磁盤時,也會將其他未提交的事務的日志寫入磁盤。
E. Redo Log上只進行順序追加的操作,當一個事務需要回滾時,它的Redo Log記錄也不會從Redo Log中刪除掉。
五、恢復(Recovery)
在recovery裡面可以看到log是非常必要的:當數據庫發生異常的時候,數據是可以恢復的。對於不是損壞磁盤驅動器的異常,恢復是自動進行的。InnoDB讀取最新的checkpoint日志記錄,檢查dirty pages是否在異常發生前寫到磁盤上了,如果沒有,則讀取影響該頁的log記錄並應用它們。這被稱為”rolling forward”。因為有LSN,所以InnoDB只需要比較這個數字就可以進行同步。
恢復策略
前面說到未提交的事務和回滾了的事務也會記錄Redo Log,因此在進行恢復時,這些事務要進行特殊的的處理.有2中不同的恢復策略:
A. 進行恢復時,只重做已經提交了的事務。
B. 進行恢復時,重做所有事務包括未提交的事務和回滾了的事務。然後通過Undo Log回滾那些未提交的事務。
當你使用UPDATE, INSERT, DELETE語句更新數據的時候,你就改變了兩個地方的數據:log buffer和data buffers。Buffers是固定長度的內存塊,通常是512字節。
LOG BUFFER DATA BUFFER
================= ===============
= Log Record #1 = = Page Header =
= Log Record #2 = = Data Row =
= Log Record #3 = = Data Row =
= Log Record #4 = = Data Row =
================= ===============
例如:INSERT INTO JOBS VALUES(1,2,3)語句執行之後,log buffer將增加一個新的log記錄,稱為Log Record #5,它包含一個rowid和新記錄的內容。同時,data buffer也將增加一個新行,但是,它會同時在頁頭標識:該頁最新的log記錄是Log Record #5。在這個例子中#5是Log Sequence Number(LSN),它對於接下來操作的時序安排是至關重要的。
下面是data-change的一些細節:
1.一個INSERT log記錄僅包含一個新數據,它對於在頁上重做操作是足夠的了,因此被稱為一個redo條目。
2. LSN不是log記錄的一個域,它是文件中的一個絕對地址的相對偏移值。
在InnoDB改變了log buffer和data buffer之後,接下來就是寫盤了。這就是復雜的地方。有多個線程在監控buffer的活動情況,有三種情況――overflow, checkpoint和commit――可以導致寫盤操作。
Overflows情況下發生了什麼
Overflow是很少發生的情況,因為InnoDB采用pro-active措施來防止buffers被填滿。但是我們還是來看看下面兩種情況:
1.如果log buffer滿了,InnoDB在buffer的末尾寫log。那麼情況向下面的圖一樣(log buffer只有四條記錄的空間,現在插入第五條記錄):
LOG FILE(S) BEFORE WRITING LOG RECORD #5
=================
= Log Record #1 =
= Log Record #2 =
= Log Record #3 =
= Log Record #4 =
=================
LOG FILE(S) AFTER WRITING LOG RECORD #5
=================
= Log Record #5 =
= Log Record #2 =
= Log Record #3 =
= Log Record #4 =
=================
logs不可能永遠增長。即使InnoDB使用了某些壓縮算法,log文件還是會由於太大而不能放到任何磁盤驅動器上。因此InnoDB采取循環寫的辦法,也就是說將會覆蓋前面就的log記錄。
2. 如果data buffer滿了,InnoDB將最近使用的buffer寫入到數據庫中,但是不可能足夠的快。這種情況下,頁頭的LSN就起作用了,InnoDB檢查它的LSN是否比log文件中最近的log記錄的LSN大,只有當log趕上了data的時候,才會將數據寫到磁盤。換句話說,數據頁不會寫盤,直到相應的log記錄需要寫盤的時候。這就是先寫日志策略。
CheckPoint的時候發生了什麼
Checkpoint機制每次刷新多少頁,從哪裡取髒頁,什麼時間觸發刷新?這些都是很復雜的。有兩種Checkpoint,分別為:Sharp Checkpoint、Fuzzy Checkpoint
Sharp Checkpoint發生在關閉數據庫時,將所有髒頁刷回磁盤
Fuzzy Checkpoint在運行時使用進行部分髒頁的刷新,部分髒頁刷新有以下幾種:
A、Master Thread Checkpoint
Master Thread以每秒或每十秒的速度從緩沖池的髒頁列表中刷新一定比例的頁回磁盤。這個過程是異步的,不會阻塞查詢線程。
B、FLUSH_LRU_LIST Checkpoint
InnoDB要保證LRU列表中有100左右空閒頁可使用。在InnoDB1.1.X版本前,要檢查LRU中是否有足夠的頁用於用戶查詢操作線程,如果沒有,會將LRU列表尾端的頁淘汰,如果被淘汰的頁中有髒頁,會強制執行Checkpoint刷回髒頁數據到磁盤,顯然這會阻塞用戶查詢線程。從InnoDB1.2.X版本開始,這個檢查放到單獨的Page Cleaner Thread中進行,並且用戶可以通過innodb_lru_scan_depth控制LRU列表中可用頁的數量,默認值為1024。
C、Async/Sync Flush Checkpoint
是指重做日志文件不可用時,需要強制將髒頁列表中的一些頁刷新回磁盤。這可以保證重做日志文件可循環使用。在InnoDB1.2.X版本之前,Async Flush Checkpoint會阻塞發現問題的用戶查詢線程,Sync Flush Checkpoint會阻塞所有查詢線程。InnoDB1.2.X之後放到單獨的Page Cleaner Thread。
D、Dirty Page too much Checkpoint
髒頁數量太多時,InnoDB引擎會強制進行Checkpoint。目的還是為了保證緩沖池中有足夠可用的空閒頁。其可以通過參數innodb_max_dirty_pages_pct來設置:
mysql> show variables like 'innodb_max_dirty_pages_pct';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| innodb_max_dirty_pages_pct | 90 |
+----------------------------+-------+
前面說過InnoDB采取了一些pro-active措施來保證不發生overflows,其中最重要的措施就是checkpointing。有一個分離的線程,或者說從一組修改buffers的線程中分離出來的一個線程。在特定的時間間隔,checkpointer將醒來,檢查buffer的改變,並保證寫盤操作已經發生了。
大部分DBMS在這個時候,將會把所有的buffer寫盤,這樣可以保證所有改變了但是沒寫盤的buffer都寫盤。就是說DBMS將通過”Sharp Checkpoint” flush所有”dirty”buffers。但是InnoDB只保證:
a、log和data buffers不會超過某個限制點;
b、log始終比data先寫盤;
c、沒有哪個data buffer的頁頭LSN等於被覆蓋寫的log記錄。也就是說InnoDB是”Fuzzy Checkpoint”。
在COMMIT的時候,InnoDB不會將dirty data page寫盤。之所以強調這個是因為,很容易讓人想到,提交改變就是將所有東西寫到一個持久媒介上。其實,只有log記錄需要寫。寫dirty data page只可能發生在overflow或checkpoint時刻,因為它們的內容是多余的。