23 mysql怎麽保證數據不丟失?
MySQL的wal機制,得到的結論是:只要redo log和binlog 持久化到磁盤,就能確保mysql異常重新啟動後,數據是可以恢復的。
binlog的寫入機制
其實,binlog的寫入邏輯比較簡單:事務執行過程中,先把日誌寫到binlog cache,事務提交的時候,再把binlog cache的內容寫到binlog文件中。
一個事務的binlog是不能被拆開的,因此不論事務多大,也要確保一次性寫入,這就涉及到binlog cache的保存問題。
系統給binlog cache分配了一片內存,每個線程一個,參數binlog_cache用於控制單個線程內binlog cache所占內存的大小,如果超過這個值,就要
事務提交的時候,執行器把binlog cache的完整事務寫入到binlog 文件中,並清空binlog cache,狀態如圖
可以看到每個線程都自己的binlog cache,但是公用一個binlog文件。
圖中的write就是把日誌寫入到文件系統中的page cache,並沒有把數據持久化到磁盤,所以速度比較快
圖中的fsync,將數據持久化到磁盤的操作,一般情況下,我們認為fsync才是占磁盤的IOPS。
write和fsync由參數sync_binlog控制
1 sync_binlog=0 表示每次提交的時候事務都只write,不fsync
2 sync_binlog=1 表示事務每次提交都會
3 sync_binlog=N(N>1)表示每次提交事務都會write,但累計N個事務後才fsync
因此,在出現io瓶頸的情況下,可以調整sync_binlog的值偏大一點,可以提升性能,但是在實際業務場景中,考慮到丟失日誌量的可控性,一般不建議將該值設置為0,比較常見的是設置為100~1000中的某個數值。
但是,將sync_binlog設置為N,對應的風險是:如果主機發生異常重啟,將丟失N個事務的binlog 日誌。
redo log的寫入機制
事務在執行過程中,生成的redo log是要先寫到redo log buffer,那redo log buffer的內容是不是每次生成後都要直接持久化到磁盤呢
如果事務運行期間,mysql發生異常重啟,那麽這部分日誌就丟失了,由於事務還沒有提交,丟失的就不影響。
那麽在事務還沒有提交的時候,redo log buffer中的部分日誌會不會持久化到磁盤呢?答案是會有的,這個要從redo log的三種狀態說起
三種狀態分別是:
1 存在redo log buffer中,物理上就是mysql的進程內存中,紅色部分。
2 寫到磁盤(write),但是沒有持久化(fsync)到磁盤物理上就是操作系統的page cache,黃色的部分。
3 持久化到磁盤,對應的是hard disk,圖中綠色的部分。
日誌寫到redo log buffer是很快的,write寫到page cache也很快,但是持久化的速度就相對要慢很多。為了控制redo log的寫入策略,innodb提供了參數innodb_fush_log_at_trx_commit,它有三種取值:
1 設置為0的時候,表示每次事務提交都會只把redo log留在redo log buffer中
2 設置為1的時候,事務的每次提交都會持久化到磁盤
3 設置為2的時候,事務的每次提交都會把redo log 寫入到page cache中
Innodb有一個後臺線程,每1秒就會把redo log buffer中的寫入到redo log,調用write寫入系統的page cache,然後調用fsync持久化到磁盤。
註意,事務執行過程中的redo log也是直接寫在redo log buffer中的,這些redo log也會被後臺線程一起持久化到磁盤,也就是說,一個沒有提交的事務的redo log,也可能會已經持久化到了磁盤的。
實際上,除了後臺線程每秒一次的輪詢操作,還有場景讓沒有提交的事務的redo log寫入到磁盤中。
1 redo log buffer占用的空間即將達到innodb_log_buffer_size的一半,後臺線程會主動寫盤。註意,由於這個事務並沒有提交,所以這個寫盤動作只是write,而沒有調用fsync,也就是只留在了文件系統page cache中。
2 並行的事務提交的時候,順帶將這個redo log buffer持久到了磁盤。假設事務A執行到一半,已經寫了一些redo log buffer,這時候另外一個線程的事務B提交,如果innodb_flush_log_at_trx_commit設置為1,那麽按照這個參數的邏輯,事務B要把redo buffer裏的日誌全部持久化到磁盤,這時候,就會帶上事務A的redo log buffer的日子一起持久化到磁盤。
兩階段提交的時候,時序上redo log先prepare,在寫binlog,最後再把redo log commit。
如果把參數innodb_flush_log_at_trx_commit設置為1,那麽redo log在prepare階段就要持久化一次,因為有一個崩潰恢復邏輯是要依賴於prepare的redo log,再加上binlog來恢復。
每秒一次的後臺輪詢刷盤,再加上崩潰恢復的這個邏輯,innodb就認為redo log在commit的時候就不需要fsync了,只會write到文件系統的page cache中就夠了。
通常我們說的”雙1”模式,就是把參數sync_binlog和innodb_flush_log_at_trx_commit都設置為1。也就說,一個事務完整提交前,需要等待兩次刷盤,一次是redo log(prepare)階段,一次是binlog。
這時候,如果看到mysql的tps是2w的話,每秒就會寫4w次磁盤,但是,用工具測試,磁盤能力也就2w左右,怎麽實現tps為2w呢?
要解釋這個問題,就要用到組提交(group commit)機制了
日誌的邏輯序列號(LSN)是單調遞增的,用來對應redo log的一個個寫入點,每次寫入的長度為length的redo log,LSN就會加上length。
LSN也會寫到innodb的數據頁中,來確保數據不會被多次執行重復的redo log,關於lsn,redo log和checkpoint後面會提到。
如圖所示,是三個並發事務(trx1,trx2,trx3)在prepare階段,都寫完redo log buffer,持久化到磁盤的過程,對應的LSN分別是50,120,160
從圖中看到
1 trx1是第一個到達,會被選為這個組的leader
2 等trx1要開始寫盤的時候,這個組裏面已經有了三個事務,這時候LSN變成了160
3 trx1去寫盤的時候,帶的就是lsn=160,因此等trx1返回時,所有lsn小於160的redo log都已經持久化到磁盤
4 這時候trx2和trx3就可以直接返回了。
所以,一次組提交裏面,組員越多,節約磁盤的iops的效果就越好,但如果只是單線程壓測,那就還是一個事務對應一次持久化操作了。
在並發更新的場景下,第一個事務寫完redo log buffer以後,接下來這個fsync越晚調用,組員就能越多,節約的iops效果就越好。
為了讓一次fsync帶的組員更多,mysql有一個優化,托時間。
圖中,把寫binlog當成一個動作,但實際上寫binlog分為兩步
1 先把binlog從binlog cache中寫道磁盤上的binlog文件
2 調用fsync持久化
Mysql為了讓組提交的效果更好,把redo log做fsync的時間拖到了步驟1之後,上圖變成如下
這麽一來,binlog也可以組提交了,在執行上圖的第四步把binlog fsync到磁盤時,如果有多個事務的binlog已經寫完了,也是一起持久化的,這樣也可以減少iops的消耗。
不過通常情況下第3步執行得會很快,所以binlog的write和fsync間的間隔很短,導致能合到一起持久化的binlog比較少,因此binlog的組提交的效果通常不如redo log的效果那麽好。
如果想提升binlog的組提交效果,可以通過設置(5.7)binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count來實現
1 binlog_group_commit_sync_delay參數,表示延遲多少微妙後才調用fsync
2 binlog_group_commit_sync_no_delay_count 參數,表示累計多少次後才調用fsync
這兩個條件是或的關系,也就說只要滿足其中一個就會調用fsync
當binlog_group_commit_sync_delay為0的時候,另外的參數也就無效了。
WAL機制主要得益於兩個方面
1 redo log和binlog都是順序寫,磁盤的順序寫比隨機寫速度快。
2 組提交機制,可以大幅度降低磁盤的iops的消耗。
到這裏,如果mysql出現了性能瓶頸,而且瓶頸在io上,可以考慮呢些方法來提升?
1 設置binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count參數,減少binlog的寫盤次數,這個方法是基於”額外的故意等待來實現”,因此可能會增加語句返回的響應時間,但是沒有丟失數據的風險。
2 將sync_binlog設置為大於1的值(100~1000),這樣做的風險是,主機掉電會丟失binlog日誌。
3 將innodb_flush_log_at_trx_commit=2,這樣做的風險是,主機掉電就丟失數據。
不建議把innodb_flush_log_at_trx_commit設置為0,如果=0的話,redo只保存在內存中,這樣的話,mysql本身異常重啟也會丟失數據,風險太大,
而redo log寫到文件系統的page cache的速度也是很快,所以這個參數設置為2和0的性能差不多,但是,這樣2就不會在mysql異常重啟時丟失數據。
小結
如果保證binlog和redo log是完整的,就可以保證mysql是crash-safe的。
問題1,執行一個update語句以後,再去執行hexdump命令直接查看idb文件,為什麽沒有看到數據改變?
回答:這可能是因為WAL機制的原因,update語句執行完成,innodb只保證redolog,內存,可能還沒來得及將數據寫到磁盤。
問題2,為什麽binlog cache每個線程自己維護,而redo log buffer是全局公用?
回答:mysql這麽設計,binlog是不能”被打斷”,一個事務的binlog必須連續寫,因此要整個事務完成後,再一起寫入到文件裏。
而redo log沒有這個要求,中間生成的日誌可以寫到redo log buffer中,還可以被寫到磁盤中。
問題3,事務執行期間,還沒到提交階段,如果發生crash,redo log肯定丟失,這會不會導致主備不一致。
回答:不會,這時候binlog還在binlog cache裏,沒發給備庫,carsh後,redo log和binlog都沒有,從業務角度看事務還沒有提交,所以數據是一致的。
問題4,如果binlog寫完後發生crash,這時候還沒有給客戶端答復就重啟了,等客戶端重新連接進來,發現事務已經提交成功,這是不是bug
回答:不是,
假設整個事務提交成功了,redo log commit完成了,備庫也收到binlog並執行了,但是主庫和客戶端網絡斷開了,導致事務成功的包返回不了,這時候客戶端收到”網絡斷開”異常,這種也是算事務成功的,不能認為是bug
實際上數據庫的crash-safe保證的是:
1 如果客戶端收到事務成功的消息,事務就一定持久化了
2 如果客戶端收到失敗(比如pk沖突,rollback等)的消息,事務就一定失敗了
3 如果客戶端收到”執行異常”的消息,應用需要重連通過當前查詢來繼續後續的邏輯,此時數據庫只需要保證內部(數據和日誌之間,主庫和備庫之間)一致就可以了。
23 mysql怎麽保證數據不丟失?