原文地址:http://hatemysql.com/tag/sync_binlog/
1. 概述
很多企業選擇MySQL都會擔心它的數據丟失問題,從而選擇Oracle,但是其實並不十分清楚什么情況下,各種原因導致MySQL會丟失部分數據。本文不討論Oracle和MySQL的優劣,僅僅關注MySQL丟失數據的幾種情況。希望能夠拋磚引玉,讓各位MySQL大牛們梳理出MySQL最安全或者性價比合適的適合各種應用場景的方案。
2. 問題定義
一般我們希望把一系列的數據作為一個原子操作,這樣的話,這一系列操作,要么提交,要么全部回滾掉。
當我們提交一個事務,數據庫要么告訴我們事務提交成功了,要么告訴我們提交失敗。
數據庫為了效率等原因,數據只保存在內存中,沒有真正的寫入到磁盤上去。如果數據庫響應為“提交成功”,但是由於數據庫掛掉,操作系統,數據庫主機等任何問題導致這次“提交成功”的事務對數據庫的修改沒有生效,那么我們認為這個事務的數據丟失了。這個對銀行或者支付寶這種業務場景來說是不能接受的。所以,保證數據不丟失也是數據庫選擇的一個重要衡量指標
mysql的架構和普通的數據庫架構最大的差異在於它使用插件式的存儲引擎。數據的存取由存儲引擎負責。要了解MySQL數據丟失的問題就需要從MySQL server層和InnoDB目前最流行的支持事務的存儲引擎分別來分析了。
3. InnoDB事務數據丟失
首先,我們來看一下InnoDB事務數據丟失的情況。
3.1. InnoDB事務基本原理
InnoDB的事務提交需要寫入undo log,redo log,以及真正的數據頁。專業的介紹可以參考丁奇和雲華的兩篇文章。我們這里通俗一點簡單介紹一下。
InnoDB跟Oracle非常類似,使用日志先行的策略,將數據的變更在內存中完成,並且將事務記錄成redo,轉換為順序IO高效的提交事務。這里日志先行,也就是說,日志記錄到數據庫以后,對應的事務就可以返回給用戶,表示事務完成。但是實際上,這個數據可能還只在內存中修改完成,並沒有刷到磁盤上去,俗稱“還沒有落地”。內存是易失的,如果在數據“落地”之前,機器掛了,那么這部分數據就丟失了。而數據庫怎么保證這些數據還是能夠找回來列?否則,用戶提交了一個事務,數據庫響應請求並回應為事務“提交成功”,數據庫重啟以后,這部分修改數據的卻回到了事務提交之前的狀態。
3.2. InnoDB事務崩潰恢復基本原理
InnoDB和Oracle都是利用redo來保證數據一致性的。如果你有從數據庫新建一直到數據庫掛掉的所有redo,那么你可以將數據完完整整的重新build出來。但是這樣的話,速度肯定很慢。所以一般每隔一段時間,數據庫會做一個checkpoint的操作,做checkpoint的目的就是為了讓在該時刻之前的所有數據都”落地”。這樣的話,數據庫掛了,內存中的數據丟了,不用從最原始的位置開始恢復,而只需要從最新的checkpoint來恢復。將已經提交的所有事務變更到具體的數據塊中,將那些未提交的事務回滾掉。
3.3. InnoDB redo日志
這樣的話,保證事務的redo日志刷到磁盤就成了事務數據是否丟失的關鍵。而InnoDB為了保證日志的刷寫的高效,使用了內存的log buffer,另外,由於InnoDB大部分情況下使用的是文件系統,(linux文件系統本身也是有buffer的)而不是直接使用物理塊設備,這樣的話就有兩種丟失日志的可能性:日志保存在log_buffer中,機器掛了,對應的事務數據就丟失了;日志從log buffer刷到了linux文件系統的buffer,機器掛掉了,對應的事務數據就丟失了。當然,文件系統的緩存刷新到硬件設備,還有可能被raid卡的緩存,甚至是磁盤本身的緩存保留,而不是真正的寫到磁盤介質上去了。這個就不在我們這次討論的范圍內了。
InnoDB的日志你還可以參考這篇文章
3.4. innodb_flush_log_at_trx_commit
所以InnoDB有一個特別的參數用於設置這兩個緩存的刷新: innodb_flush_log_at_trx_commit。
默認,innodb_flush_log_at_trx_commit=1,表示在每次事務提交的時候,都把log buffer刷到文件系統中去,並且調用文件系統的“flush”操作將緩存刷新到磁盤上去。這樣的話,數據庫對IO的要求就非常高了,如果底層的硬件提供的IOPS比較差,那么MySQL數據庫的並發很快就會由於硬件IO的問題而無法提升。
為了提高效率,保證並發,犧牲一定的數據一致性。innodb_flush_log_at_trx_commit還可以設置為0和2。
innodb_flush_log_at_trx_commit=0時,每隔一秒把log buffer刷到文件系統中去,並且調用文件系統的“flush”操作將緩存刷新到磁盤上去。這樣的話,可能丟失1秒的事務數據。
innodb_flush_log_at_trx_commit=2時,在每次事務提交的時候會把log buffer刷到文件系統中去,但是每隔一秒調用文件系統的“flush”操作將緩存刷新到磁盤上去。如果只是MySQL數據庫掛掉了,由於文件系統沒有問題,那么對應的事務數據並沒有丟失。只有在數據庫所在的主機操作系統損壞或者突然掉電的情況下,數據庫的事務數據可能丟失1秒之類的事務數據。這樣的好處就是,減少了事務數據丟失的概率,而對底層硬件的IO要求也沒有那么高(log buffer寫到文件系統中,一般只是從log buffer的內存轉移的文件系統的內存緩存中,對底層IO沒有壓力)。MySQL 5.6.6以后,這個“1秒”的刷新還可以用innodb_flush_log_at_timeout 來控制刷新間隔。
在大部分應用環境中,應用對數據的一致性要求並沒有那么高,所以很多MySQL DBA會設置innodb_flush_log_at_trx_commit=2,這樣的話,數據庫就存在丟失最多1秒的事務數據的風險。
引用應元的一個圖如下:
4. 數據庫復制導致數據丟失
MySQL相比其他數據庫更適用於互聯網的其中一個重要特性就是MySQL的復制。對於互聯網這種需要提供7*24小時不間斷的服務的要求,MySQL提供異步的數據同步機制。利用這種復制同步機制,當數據庫主庫無法提供服務時,應用可以快速切換到跟它保持同步的一個備庫中去。備庫繼續為應用提供服務,從而不影響應用的可用性。
這里有一個關鍵的問題,就是應用切換到備庫訪問,備庫的數據需要跟主庫的數據一致才能保證不丟失數據。由於目前MySQL還沒有提供全同步的主備復制解決方案所以這里也是可能存在數據丟失的情況。
目前MySQL提供兩種主備同步的方式:異步(asynchronous)和半同步(Semi-sync)
4.1. MySQL復制原理簡介
MySQL復制的原理簡介如下:MySQL主庫在事務提交時寫binlog,並通過sync_binlog參數來控制binlog刷新到磁盤“落地”。而備庫通過IO線程從主庫拉取binlog,並記錄到本地的relay log中;由本地的SQL線程再將relay log中的數據應用到本地數據庫中。
異步的方式下,幾個線程都是獨立的,相互不依賴。
而在半同步的情況下,主庫的事務提交需要保證至少有一個備庫的IO線程已經拉到了數據,這樣保證了至少有一個備庫有最新的事務數據,避免了數據丟失。這里稱為半同步,是因為主庫並不要求SQL線程已經執行完成了這個事務。
半同步在MySQL 5.5才開始提供,並且可能引起並發和效率的一系列問題,比如只有一個備庫,備庫掛掉了,那么主庫在事務提交10秒(rpl_semi_sync_master_timeout控制)后,才會繼續,之后變成傳統的異步方式。所以目前在生產環境下使用半同步的比較少。
在異步方式下,如何保證數據盡量不丟失就成了主要問題。這個問題其實就是如何保證數據庫的binlog不丟失,盡快將binlog落地,這樣就算數據庫掛掉了,我們還可以通過binlog來將丟失的部分數據手工同步到備庫上去(MHA會自動抽取缺失的部分補全備庫)。
圖示如下:
4.2. sync_binlog
這個問題就跟上一個innodb_flush_log_at_trx_commit的問題類似了。MySQL提供一個sync_binlog參數來控制數據庫的binlog刷到磁盤上去。雖然binlog也有binlog cache,但是MySQL並沒有控制binlog cache同步到文件系統緩存的相關考慮。所以我們這里不涉及binlog cache。
默認,sync_binlog=0,表示MySQL不控制binlog的刷新,由文件系統自己控制它的緩存的刷新。
如果sync_binlog>0,表示每sync_binlog次事務提交,MySQL調用文件系統的刷新操作將緩存刷下去。最安全的就是sync_binlog=1了,表示每次事務提交,MySQL都會把binlog刷下去。這樣的話,在數據庫所在的主機操作系統損壞或者突然掉電的情況下,系統才有可能丟失1個事務的數據。但是binlog雖然是順序IO,但是設置sync_binlog=1,多個事務同時提交,同樣很大的影響MySQL和IO性能。雖然可以通過group commit的補丁緩解,但是刷新的頻率過高對IO的影響也非常大。
所以很多MySQL DBA設置的sync_binlog並不是最安全的1,而是100或者是0。這樣犧牲一定的一致性,可以獲得更高的並發和性能。
5. MySQL和InnoDB協同
5.1. 兩段式事務提交
最后我們需要討論一下上述兩個參數對應的redolog和 binlog協同的問題。這兩個log都影響數據丟失,但是他們分別在InnoDB和MySQL server層維護。由於一個事務可能使用兩種事務引擎,所以MySQL用兩段式事務提交來協調事務提交。我們先簡單了解一下兩段式事務提交的過程
第一階段:
首先,協調者在自身節點的日志中寫入一條的日志記錄,然后所有參與者發送消息prepare T,詢問這些參與者(包括自身),是否能夠提交這個事務;
參與者在接受到這個prepare T 消息以后,會根據自身的情況,進行事務的預處理,如果參與者能夠提交該事務,則會將日志寫入磁盤,並返回給協調者一個ready T信息,同時自身進入預提交狀態狀態;如果不能提交該事務,則記錄日志,並返回一個not commit T信息給協調者,同時撤銷在自身上所做的數據庫改;
參與者能夠推遲發送響應的時間,但最終還是需要發送的。
第二階段:
協調者會收集所有參與者的意見,如果收到參與者發來的not commit T信息,則標識着該事務不能提交,協調者會將Abort T 記錄到日志中,並向所有參與者發送一個Abort T 信息,讓所有參與者撤銷在自身上所有的預操作;
如果協調者收到所有參與者發來prepare T信息,那么協調者會將Commit T日志寫入磁盤,並向所有參與者發送一個Commit T信息,提交該事務。若協調者遲遲未收到某個參與者發來的信息,則認為該參與者發送了一個VOTE_ABORT信息,從而取消該事務的執行。
參與者接收到協調者發來的Abort T信息以后,參與者會終止提交,並將Abort T 記錄到日志中;如果參與者收到的是Commit T信息,則會將事務進行提交,並寫入記錄
一般情況下,兩階段提交機制都能較好的運行,當在事務進行過程中,有參與者宕機時,他重啟以后,可以通過詢問其他參與者或者協調者,從而知道這個事務到底提交了沒有。當然,這一切的前提都是各個參與者在進行每一步操作時,都會事先寫入日志。
具體的介紹可以參考《事務和兩階段提交》以及《分布式事務設計-兩階段提交》
5.2. innodb_support_xa
innodb_support_xa可以開關InnoDB的xa兩段式事務提交。默認情況下,innodb_support_xa=true,支持xa兩段式事務提交。此時MySQL首先要求innodb prepare,對應的redolog 將寫入log buffer;如果有其他的引擎,其他引擎也需要做事務提交的prepare,然后MySQL server將binlog將寫入;並通知各事務引擎真正commit;InnoDB將commit標志寫入,完成真正的提交,響應應用程序為提交成功。這個過程中任何出錯將導致事務回滾,響應應用程序為提交失敗。也就是說,在這種情況下,基本不會出錯。
但是由於xa兩段式事務提交導致多余flush等操作,性能影響會達到10%,所有為了提高性能,有些DBA會設置innodb_support_xa=false。這樣的話,redolog和binlog將無法同步,可能存在事務在主庫提交,但是沒有記錄到binlog的情況。這樣也有可能造成事務數據的丟失。
綜上,我們列舉了影響InnoDB數據丟失的參數innodb_flush_log_at_trx_commit,影響MySQL復制數據丟失的sync_binlog,以及由於MySQL和InnoDB需要協調而可能導致數據丟失的參數innodb_support_xa。
mysql innodb_flush_log_at_trx_commit翻譯
知道innodb_flush_log_at_trx_commit的意思,但是對它取值0,1,2一直有點模糊不清。特地找了MySQL 5.1的refrence,自己翻譯一下。雖然,也有官方的中文版翻譯,但是不好意思,有點不相信它。
英文原文如下:
innodb_flush_log_at_trx_commit
Command-Line Format –innodb_flush_log_at_trx_commit[=#]
Config-File Format innodb_flush_log_at_trx_commit
Option Sets Variable Yes, innodb_flush_log_at_trx_commit
Variable Name innodb_flush_log_at_trx_commit
Variable Scope Global
Dynamic Variable Yes
Permitted Values
Type numeric
Default 1
Valid Values 0, 1, 2
If the value of innodb_flush_log_at_trx_commit is 0, the log buffer is written out to the log file once per second and the flush to disk operation is performed on the log file, but nothing is done at a transaction commit. When the value is 1 (the default), the log buffer is written out to the log file at each transaction commit and the flush to disk operation is performed on the log file. When the value is 2, the log buffer is written out to the file at each commit, but the flush to disk operation is not performed on it. However, the flushing on the log file takes place once per second also when the value is 2. Note that the once-per-second flushing is not 100% guaranteed to happen every second, due to process scheduling issues.
如果innodb_flush_log_at_trx_commit設置為0,log buffer將每秒一次地寫入log file中,並且log file的flush(刷到磁盤)操作同時進行;但是,這種模式下,在事務提交的時候,不會有任何動作。如果innodb_flush_log_at_trx_commit設置為1(默認值),log buffer每次事務提交都會寫入log file,並且,flush刷到磁盤中去。如果innodb_flush_log_at_trx_commit設置為2,log buffer在每次事務提交的時候都會寫入log file,但是,flush(刷到磁盤)操作並不會同時進行。這種模式下,MySQL會每秒一次地去做flush(刷到磁盤)操作。注意:由於進程調度策略問題,這個“每秒一次的flush(刷到磁盤)操作”並不是保證100%的“每秒”。
The default value of 1 is the value required for ACID compliance. You can achieve better performance by setting the value different from 1, but then you can lose at most one second worth of transactions in a crash. With a value of 0, any mysqld process crash can erase the last second of transactions. With a value of 2, then only an operating system crash or a power outage can erase the last second of transactions. However, InnoDB’s crash recovery is not affected and thus crash recovery does work regardless of the value.
默認值1是為了ACID (atomicity, consistency, isolation, durability)原子性,一致性,隔離性和持久化的考慮。如果你不把innodb_flush_log_at_trx_commit設置為1,你將獲得更好的性能,但是,你在系統崩潰的情況,可能會丟失最多一秒鍾的事務數據。當你把innodb_flush_log_at_trx_commit設置為0,mysqld進程的崩潰會導致上一秒鍾所有事務數據的丟失。如果你把innodb_flush_log_at_trx_commit設置為2,只有在操作系統崩潰或者系統掉電的情況下,上一秒鍾所有事務數據才可能丟失。(下面的這句話到底是針對innodb_flush_log_at_trx_commit為2說的,還是針對前面這一整段說的,我就搞不清楚了,下次問問編寫這一段文檔的MySQL的人去。感覺是針對整段的:就是說InnoDB的crash recovery會利用log file來恢復數據文件,跟innodb_flush_log_at_trx_commit的值沒有關系,管你這個值怎么設置的,我從log file拿到多少數據,就恢復多少數據。)InnoDB的crash recovery崩潰恢復機制並不受這個值的影響,不管這個值設置為多少,crash recovery崩潰恢復機制都會工作。
For the greatest possible durability and consistency in a replication setup using InnoDB with transactions, use innodb_flush_log_at_trx_commit = 1 and sync_binlog = 1 in your master server my.cnf file.
為了在使用InnoDB事務的搭建復制環境中,達到最大的持久化和一致性,你需要在你的master主機的my.cnf中設置innodb_flush_log_at_trx_commit = 1並且設置sync_binlog = 1。
Caution
Many operating systems and some disk hardware fool the flush-to-disk operation. They may tell mysqld that the flush has taken place, even though it has not. Then the durability of transactions is not guaranteed even with the setting 1, and in the worst case a power outage can even corrupt the InnoDB database. Using a battery-backed disk cache in the SCSI disk controller or in the disk itself speeds up file flushes, and makes the operation safer. You can also try using the Unix command hdparm to disable the caching of disk writes in hardware caches, or use some other command specific to the hardware vendor.
注意:
很多操作系統和一些磁盤硬件系統並不會真正的做flush-to-disk刷新到磁盤的這個操作。他們即使並沒有真正刷到磁盤也會告訴mysqld說flush刷新到磁盤的操作已經完成了。這樣的話,即使innodb_flush_log_at_trx_commit設置為1,也不能保證事務的持久化,最糟的情況下,一個主機掉電,就有可能導致InnoDB數據庫崩潰。你可以考慮在SCSI磁盤控制器里面或者磁盤本身中,使用帶蓄電池后備電源的磁盤緩存disk cache,來提高文件刷新操作的速度,使得這個操作更加安全。你同樣可以嘗試使用Unix的hdparm命令來阻止硬件緩存hardware cache的寫磁盤緩存操作,或者使用其他硬件提供商hardware vendor提供的命令來避免寫磁盤緩存。
這里既然提到了sync_binlog就順便把它也翻譯一下。
sync_binlog
Command-Line Format –sync-binlog=#
Config-File Format sync_binlog
Option Sets Variable Yes, sync_binlog
Variable Name sync_binlog
Variable Scope Global
Dynamic Variable Yes
Permitted Values
Platform Bit Size 32
Type numeric
Default 0
Range 0-4294967295
Permitted Values
Platform Bit Size 64
Type numeric
Default 0
Range 0-18446744073709547520
If the value of this variable is greater than 0, the MySQL server synchronizes its binary log to disk (using fdatasync()) after every sync_binlog writes to the binary log. There is one write to the binary log per statement if autocommit is enabled, and one write per transaction otherwise. The default value of sync_binlog is 0, which does no synchronizing to disk — in this case, the server relies on the operating system to flush the binary log’s contents from to time as for any other file. A value of 1 is the safest choice because in the event of a crash you lose at most one statement or transaction from the binary log. However, it is also the slowest choice (unless the disk has a battery-backed cache, which makes synchronization very fast).
當sync_binlog變量設置為大於0的值時,MySQL在每次“sync_binlog”這么多次寫二進制日志binary log時,會使用fdatasync()函數將它的寫二進制日志binary log同步到磁盤中去。如果啟用了autocommit,那么每一個語句statement就會有一次寫操作;否則每個事務對應一個寫操作。sync_binlog的默認值是0,這種模式下,MySQL不會同步到磁盤中去。這樣的話,MySQL依賴操作系統來刷新二進制日志binary log,就像操作系統刷其他文件的機制一樣。當sync_binlog變量設置為1是最安全的,因為在crash崩潰的情況下,你的二進制日志binary log只有可能丟失最多一個語句或者一個事務。但是,這也是最慢的一種方式(除非磁盤有使用帶蓄電池后備電源的緩存cache,使得同步到磁盤的操作非常快)。
找了一下man fdatasync:
fdatasync() flushes all data buffers of a file to disk (before the system call returns). It resembles fsync() but is not required to update the metadata such as access time.
fdatasync() (在系統調用system call返回前)將文件中所有的數據緩存區data buffers都flush刷到磁盤中去。它類似於fsync()函數,但是它不會更新元數據metadata:比如最后訪問時間等。