為什么數(shù)據(jù)庫能邊跑邊備份?MySQL備份這個坑,90%的程序員都掉過
這是一篇關于MySQL數(shù)據(jù)庫、redo log、LSN、崩潰恢復、在線熱備的長文。耐心讀完,要是沒收獲,你來捶我。
研發(fā)的童鞋每次對MySQL庫表做重大操作之前——比如修改表結構、批量修改或刪除數(shù)據(jù)——都會向DBA申請進行數(shù)據(jù)庫備份。
又或者說,不備份直接操作啦?
DBA到底是怎么給MySQL做備份的?
隨手問了幾十個RD和QA,答案基本就三種:
- (1)不太清楚;
- (2)在線邏輯備份,用mysqldump;
- (3)離線物理備份(冷備),拷貝從庫文件。
實際上呢?現(xiàn)在DBA基本都在用PXB方案。
今天就和大家聊聊MySQL備份的來龍去脈,以及它背后的內核原理。
mysqldump:在線邏輯備份長什么樣?
mysqldump是MySQL工具集里的一個工具,可以用來導出或備份數(shù)據(jù)。它導出的是一個SQL語句集合,包含了建表和插入數(shù)據(jù)的語句,大概長這樣:
--MySQL dump 1.2.3
--Host: localhost Database: test
--Server version 4.5.6
CREATE TABLE t_user (
id int(11)NOT NULL unique,
name varchar(40) NOT NULL default '',
PRIMARY KEY (id)
);
INSERT INTO t_user VALUES (1,'xiaobei');
INSERT INTO t_user VALUES (2,'zhangsan');
INSERT INTO t_user VALUES (3,'lisi');因為導出的是SQL語句,所以才叫邏輯備份。
這種方式的好處很明顯:可以在線進行,不影響數(shù)據(jù)庫持續(xù)對外服務。
但問題也很明顯:相比直接拷貝庫文件的物理備份,備份和恢復的速度都慢太多了。
離線物理備份:直接拷貝庫文件行不行?
為了提高備份效率、縮短備份時間,就有了直接物理備份庫文件的方案。
圖片
數(shù)據(jù)庫集群通常是上面這種1主2從架構。離線物理備份的操作流程是這樣的:
- 把一個從庫從集群里摘下來并下線,這時候離線庫文件就不會再變化了。
- 用scp拷貝庫文件,備份就完成了。文件拷完后,再把從庫掛回集群。
這種方式備份和恢復都很快,但缺點也很致命:
備份過程中從庫無法對線上提供服務。
那問題來了:有沒有一種方案,既能快速備份物理文件,又能讓數(shù)據(jù)庫持續(xù)對外服務?
這就是如今MySQL備份最流行的PXB方案。
PXB到底是個什么東西?
PXB全稱是Percona XtraBackup,官網(wǎng)是這么介紹的:PXB是全世界唯一一款開源免費的、支持MySQL熱備的、非阻塞備份工具。
那PXB是怎么做到的呢?它要解決這樣一個矛盾:
- 數(shù)據(jù)庫持續(xù)對外服務,庫文件一直在變化
- 通過拷貝MySQL文件來完成物理熱備份
要把這個問題講透,就得從redo log、LSN,以及MySQL的故障恢復(crash-recovery)機制聊起。
為什么要有redo log?
事務提交后,必須把事務對數(shù)據(jù)頁的修改刷(fsync)到磁盤,才能保證事務的ACID特性。
這個刷盤動作是隨機寫。隨機寫性能比較差,如果每次事務提交都刷盤,數(shù)據(jù)庫性能會很糟糕。
怎么優(yōu)化隨機寫?
架構設計里有兩個經(jīng)典套路:
- 先寫日志(write log first),把隨機寫變成順序寫
- 把單次寫變成批量寫
這兩招數(shù)據(jù)庫都用上了。
對數(shù)據(jù)的修改先順序寫到日志里——這個日志就是redo log。
redo log有個三層架構來實現(xiàn)批量寫:
圖片
- log buffer:應用層緩沖
- OS cache:操作系統(tǒng)緩存
- redo log file:物理文件
這不是今天的重點,就不展開了。
要是數(shù)據(jù)庫突然崩潰,來不及把數(shù)據(jù)頁刷盤怎么辦?數(shù)據(jù)庫重啟時,會重做redo log里的內容,保證已提交事務的影響都刷到磁盤上。
一句話:redo log既保證了已提交事務的ACID特性,又提升了數(shù)據(jù)庫性能。
redo log到底長什么樣?
邏輯上,MySQL以行(row)為單位管理數(shù)據(jù)。物理上,MySQL以頁(page)為單位管理數(shù)據(jù)。緩沖池(buffer)機制也是按頁管理的。
事務提交后,不用每次都隨機寫落盤刷新數(shù)據(jù)頁,而是通過順序寫redo log來提高性能。
那redo log是直接保存等待刷盤的數(shù)據(jù)頁嗎?
如果直接保存數(shù)據(jù)頁,會有個問題:假如某條SQL只修改了一行記錄里的一個屬性,比如:
update set sex=1 where name='xiaobei'物理上其實只改了1個字節(jié),難道redo log要把這個屬性所在的整頁數(shù)據(jù)(16K)全部保存下來?
完全不用。redo log只需要記錄:
- 哪個數(shù)據(jù)頁(page num)
- 哪個偏移位置(offset)
- 什么類型的數(shù)據(jù)(type)
- 改成了什么值(value)
這樣一來,redo log既能以頁為單位順序刷盤,又大大縮小了日志大小,性能又上了一個臺階。
還是剛才那條SQL,假設它修改了第1234頁、偏移量5678處的1個字節(jié),把sex從0改成1,那redo log的結構大概是這樣的:
圖片
數(shù)據(jù)庫崩潰時,如果緩沖池的數(shù)據(jù)沒來得及刷盤,就可以通過redo log,把第1234頁偏移量5678處的1個字節(jié)改為1,恢復數(shù)據(jù)。
MySQL用一系列數(shù)據(jù)結構來管理redo log,最小單位是一個512字節(jié)的數(shù)據(jù)塊(block)。這個塊由12字節(jié)的header、508字節(jié)的body、4字節(jié)的trailer組成,body里保存的就是上面說的數(shù)據(jù)頁修改記錄。
記錄redo log的文件有若干個,每個都是固定大小,循環(huán)使用。
LSN是個什么東西?
聊redo log和故障恢復,LSN是繞不開的。
LSN是什么?
LSN,Log Sequence Number,直譯就是日志序列號。它是InnoDB里隨著日志寫入,不斷遞增的8字節(jié)序列號。
聽起來像是日志專用的,但LSN不只存在redo log里,數(shù)據(jù)頁里也存儲了LSN。緩沖池的數(shù)據(jù)頁和磁盤上的數(shù)據(jù)頁都有LSN。
數(shù)據(jù)頁里的LSN可以理解為數(shù)據(jù)頁的"版本號",記錄該數(shù)據(jù)頁最后一次被修改時對應的日志序列位置。
舉個例子,假設邏輯上連續(xù)執(zhí)行了兩個事務,都已經(jīng)提交:
trx1:
update set sex=0 where name='xiaobei'
redo log lsn=1000
trx2:
update set sex=1 where name='xiaobeo'
redo log lsn=1001lsn增加了。
再假設trx1已經(jīng)刷盤,trx2還沒刷盤,只寫了redo log。
最近一次刷盤的頁,也就是最近一次檢查點(checkpoint),也是通過LSN記錄的,它會被寫入redo log。
這兩個事務修改的是同一個數(shù)據(jù)頁,很容易推斷:
磁盤數(shù)據(jù)頁上的LSN=1000
而redo log里有兩條記錄:
- redo log lsn=1000
- redo log lsn=1001
數(shù)據(jù)庫基本都用WAL(Write Ahead Log)的方式——先寫日志再刷盤,所以磁盤數(shù)據(jù)頁的LSN通常會小于最新redo log里的LSN。這時候redo log記錄的checkpoint也是1000。
LSN有什么用?
它和MySQL的故障恢復(crash-recovery)機制緊密相關。
InnoDB的故障恢復是怎么做的?
這里說的故障恢復,是指MySQL非正常退出后再啟動時,要恢復數(shù)據(jù)一致性的操作。叫崩潰恢復可能更準確。
InnoDB崩潰恢復的過程分四步:
第一步,redo log操作:保證已提交事務影響的最新數(shù)據(jù)刷到數(shù)據(jù)頁。
第二步,undo log操作:保證未提交事務影響的數(shù)據(jù)頁回滾。
第三步,寫緩沖(change buffer)合并。
第四步,purge操作。InnoDB的一種垃圾收集機制,用單獨的后臺線程周期性處理索引中標記刪除的數(shù)據(jù),也不是今天重點,以后可以詳細講。
redo log操作是怎么恢復最新數(shù)據(jù)頁的?
從redo log讀取checkpoint lsn,它記錄的是最后一次刷盤的頁對應的日志LSN。
如果redo log里記錄的日志LSN小于checkpoint,說明相關數(shù)據(jù)已經(jīng)刷盤了,不用額外操作。
如果redo log里記錄的日志LSN大于checkpoint,說明相關數(shù)據(jù)只寫了redo log,沒來得及刷盤,就需要對相關數(shù)據(jù)頁重做日志,比如:
圖片
把第1234頁偏移量5678處的1個字節(jié)改為1,恢復數(shù)據(jù)。
崩潰恢復時MySQL的啟動日志更直觀地展示了這個過程:先找到checkpoint,然后不斷掃描大于checkpoint的redo log,持續(xù)恢復數(shù)據(jù)。
順便說一句,redo log還有兩個特性:
冪等性:同一條redo log執(zhí)行多次,不影響數(shù)據(jù)恢復。
崩潰恢復時,從比checkpoint更早的LSN開始執(zhí)行恢復,也不影響數(shù)據(jù)最終一致性,因為一個數(shù)據(jù)頁最終一定會被更大的LSN日志恢復到最新數(shù)據(jù)。
PXB在線熱備的原理終于可以說了
不知不覺寫了幾千字,差點忘了最開始的問題。
PXB是怎么做到的:
在數(shù)據(jù)庫持續(xù)對外服務、庫文件不斷變化的情況下,通過拷貝MySQL文件來完成物理熱備份?
有了前面的鋪墊,這個問題就好回答了。
PXB啟動一個線程,持續(xù)監(jiān)聽并復制redo log的增量到另外的文件。不能直接備份redo log,是因為redo log循環(huán)使用,PXB必須記錄下checkpoint LSN之后的所有redo log。
接著,PXB啟動另一個線程開始復制數(shù)據(jù)文件。復制過程可能比較長,整個過程中數(shù)據(jù)文件可能在不停修改,導致數(shù)據(jù)不一致。但沒關系,所有修改都已經(jīng)記錄在第一步額外記錄的redo log里了。
務必注意:備份redo log的線程必須在開始備份數(shù)據(jù)文件之前啟動,在之后結束。
通過備份的數(shù)據(jù)文件重放redo log,執(zhí)行類似MySQL崩潰恢復的動作,就能讓數(shù)據(jù)文件恢復到能保證一致性的checkpoint檢查點。
圖片
PXB還可以對非MySQL、非InnoDB進行在線熱備,這里就不展開了。
是不是很神奇!
這是一篇關于MySQL數(shù)據(jù)庫、redo log、LSN、崩潰恢復、在線熱備的長文。耐心讀完,要是沒收獲,你來捶我。























