MySQL實戰45講學習筆記:第二十四講

1、引子

在前面的文章中,我不止一次地和你提到了 binlog,你們知道 binlog 能夠用來歸檔,也能夠用來作主備同步,但它的內容是什麼樣的呢?爲何備庫執行了 binlog 就能夠跟主
庫保持一致了呢?今天我就正式地和你介紹一下它。mysql

絕不誇張地說,MySQL 可以成爲現下最流行的開源數據庫,binlog 功不可沒。sql

在最開始,MySQL 是以容易學習和方便的高可用架構,被開發人員青睞的。而它的幾乎全部的高可用架構,都直接依賴於 binlog。雖然這些高可用架構已經呈現出愈來愈複雜的
趨勢,但都是從最基本的一主一備演化過來的。數據庫

今天這篇文章我主要爲你介紹主備的基本原理。理解了背後的設計原理,你也能夠從業務開發的角度,來借鑑這些設計思想。bash

2、MySQL 主備的基本原理

一、MySQL 主備切換流程

如圖 1 所示就是基本的主備切換流程。多線程

圖 1 MySQL 主備切換流程架構

在狀態 1 中,客戶端的讀寫都直接訪問節點 A,而節點 B 是 A 的備庫,只是將 A 的更新都同步過來,到本地執行。這樣能夠保持節點 B 和 A 的數據是相同的。

當須要切換的時候,就切成狀態 2。這時候客戶端讀寫訪問的都是節點 B,而節點 A 是 B的備庫。函數

二、雖然備庫沒有被直接訪問、設置成只讀(readonly)模式。這樣作,有什麼好處

在狀態 1 中,雖然節點 B 沒有被直接訪問,可是我依然建議你把節點 B(也就是備庫)設置成只讀(readonly)模式。這樣作,有如下幾個考慮:工具

1. 有時候一些運營類的查詢語句會被放到備庫上去查,設置爲只讀能夠防止誤操做;
2. 防止切換邏輯有 bug,好比切換過程當中出現雙寫,形成主備不一致;
3. 能夠用 readonly 狀態,來判斷節點的角色。學習

你可能會問,我把備庫設置成只讀了,還怎麼跟主庫保持同步更新呢?spa

三、 主備流程圖

這個問題,你不用擔憂。由於 readonly 設置對超級 (super) 權限用戶是無效的,而用於同步更新的線程,就擁有超級權限。

接下來,咱們再看看節點 A 到 B 這條線的內部流程是什麼樣的。圖 2 中畫出的就是一個update 語句在節點 A 執行,而後同步到節點 B 的完整流程圖

圖 2 主備流程圖

圖 2 中,包含了我在上一篇文章中講到的 binlog 和 redo log 的寫入機制相關的內容,能夠看到:主庫接收到客戶端的更新請求後,執行內部事務的更新邏輯,同時寫 binlog。

備庫 B 跟主庫 A 之間維持了一個長鏈接。主庫 A 內部有一個線程,專門用於服務備庫 B的這個長鏈接。一個事務日誌同步的完整過程是這樣的:

1. 在備庫 B 上經過 change master 命令,設置主庫 A 的 IP、端口、用戶名、密碼,以及要從哪一個位置開始請求 binlog,這個位置包含文件名和日誌偏移量。

2. 在備庫 B 上執行 start slave 命令,這時候備庫會啓動兩個線程,就是圖中的 io_thread和 sql_thread。其中 io_thread 負責與主庫創建鏈接。

3. 主庫 A 校驗完用戶名、密碼後,開始按照備庫 B 傳過來的位置,從本地讀取 binlog,發給 B。

4. 備庫 B 拿到 binlog 後,寫到本地文件,稱爲中轉日誌(relay log)。

5. sql_thread 讀取中轉日誌,解析出日誌裏的命令,並執行。

這裏須要說明,後來因爲多線程複製方案的引入,sql_thread 演化成爲了多個線程,跟咱們今天要介紹的原理沒有直接關係,暫且不展開。

分析完了這個長鏈接的邏輯,咱們再來看一個問題:binlog 裏面究竟是什麼內容,爲何備庫拿過去能夠直接執行。

3、binlog 的三種格式對比

我在第 15 篇答疑文章中,和你提到過 binlog 有兩種格式,一種是 statement,一種是row。可能你在其餘資料上還會看到有第三種格式,叫做 mixed,其實它就是前兩種格式的混合。

爲了便於描述 binlog 的這三種格式間的區別,我建立了一個表,並初始化幾行數據。

mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `a` (`a`),
  KEY `t_modified`(`t_modified`)
) ENGINE=InnoDB;

insert into t values(1,1,'2018-11-13');
insert into t values(2,2,'2018-11-12');
insert into t values(3,3,'2018-11-11');
insert into t values(4,4,'2018-11-10');
insert into t values(5,5,'2018-11-09');

若是要在表中刪除一行數據的話,咱們來看看這個 delete 語句的 binlog 是怎麼記錄的。

一、statement 模式日誌解析

注意,下面這個語句包含註釋,若是你用 MySQL 客戶端來作這個實驗的話,要記得加 -c參數,不然客戶端會自動去掉註釋。

mysql> delete from t /*comment*/  where a>=4 and t_modified<='2018-11-10' limit 1;

當 binlog_format=statement 時,binlog 裏面記錄的就是 SQL 語句的原文。

mysql> show binlog events in 'master.000001';

你能夠用命令看 binlog 中的內容。

圖 3 statement 格式 binlog 示例

如今,咱們來看一下圖 3 的輸出結果。

二、binglog日誌解析

第一行 SET @@SESSION.GTID_NEXT='ANONYMOUS’你能夠先忽略,後面文章咱們會在介紹主備切換的時候再提到;

第二行是一個 BEGIN,跟第四行的 commit 對應,表示中間是一個事務;

第三行就是真實執行的語句了。能夠看到,在真實執行的 delete 命令以前,還有一個「use ‘test’」命令。這條命令不是咱們主動執行的,而是 MySQL 根據當前要操
做的表所在的數據庫,自行添加的。這樣作能夠保證日誌傳到備庫去執行的時候,不論當前的工做線程在哪一個庫裏,都可以正確地更新到 test 庫的表 t。

use 'test’命令以後的 delete 語句,就是咱們輸入的 SQL 原文了。能夠看到,binlog「忠實」地記錄了 SQL 命令,甚至連註釋也一併記錄了。

最後一行是一個 COMMIT。你能夠看到裏面寫着 xid=61。你還記得這個 XID 是作什麼用的嗎?若是記憶模糊了,能夠再回顧一下第 15 篇文章中的相關內容。

三、statement 和 row 格式的區別

爲了說明 statement 和 row 格式的區別,咱們來看一下這條 delete 命令的執行效果圖:

圖 4 delete 執行 warnings

能夠看到,運行這條 delete 命令產生了一個 warning,緣由是當前 binlog 設置的是statement 格式,而且語句中有 limit,因此這個命令多是 unsafe 的。

爲何這麼說呢?這是由於 delete 帶 limit,極可能會出現主備數據不一致的狀況。好比上面這個例子:

1. 若是 delete 語句使用的是索引 a,那麼會根據索引 a 找到第一個知足條件的行,也就是說刪除的是 a=4 這一行;
2. 但若是使用的是索引 t_modified,那麼刪除的就是 t_modified='2018-11-09’也就是 a=5 這一行。

因爲 statement 格式下,記錄到 binlog 裏的是語句原文,所以可能會出現這樣一種狀況:在主庫執行這條 SQL 語句的時候,用的是索引 a;而在備庫執行這條 SQL 語句的時
候,卻使用了索引 t_modified。所以,MySQL 認爲這樣寫是有風險的。

那麼,若是我把 binlog 的格式改成 binlog_format=‘row’, 是否是就沒有這個問題了呢?咱們先來看看這時候 binog 中的內容吧。

圖 5 row 格式 binlog 示例

能夠看到,與 statement 格式的 binlog 相比,先後的 BEGIN 和 COMMIT 是同樣的。可是,row 格式的 binlog 裏沒有了 SQL 語句的原文,而是替換成了兩個 event:
Table_map 和 Delete_rows。

1. Table_map event,用於說明接下來要操做的表是 test 庫的表 t;
2. Delete_rows event,用於定義刪除的行爲。

其實,咱們經過圖 5 是看不到詳細信息的,還須要藉助 mysqlbinlog 工具,用下面這個命令解析和查看 binlog 中的內容。由於圖 5 中的信息顯示,這個事務的 binlog 是從
8900 這個位置開始的,因此能夠用 start-position 參數來指定從這個位置的日誌開始解析。

mysqlbinlog  -vv data/master.000001 --start-position=8900;

圖 6 row 格式 binlog 示例的詳細信息

從這個圖中,咱們能夠看到如下幾個信息:

server id 1,表示這個事務是在 server_id=1 的這個庫上執行的。

每一個 event 都有 CRC32 的值,這是由於我把參數 binlog_checksum 設置成了CRC32。

Table_map event 跟在圖 5 中看到的相同,顯示了接下來要打開的表,map 到數字226。如今咱們這條 SQL 語句只操做了一張表,若是要操做多張表呢?每一個表都有一個
對應的 Table_map event、都會 map 到一個單獨的數字,用於區分對不一樣表的操做。

咱們在 mysqlbinlog 的命令中,使用了 -vv 參數是爲了把內容都解析出來,因此從結果裏面能夠看到各個字段的值(好比,@1=四、 @2=4 這些值)。

binlog_row_image 的默認配置是 FULL,所以 Delete_event 裏面,包含了刪掉的行的全部字段的值。若是把 binlog_row_image 設置爲 MINIMAL,則只會記錄必要的信息,在這個例子裏,就是隻會記錄 id=4 這個信息。最後的 Xid event,用於表示事務被正確地提交了。

四、statement 的優缺點

你能夠看到,當 binlog_format 使用 row 格式的時候,binlog 裏面記錄了真實刪除行的主鍵 id,這樣 binlog 傳到備庫去的時候,就確定會刪除 id=4 的行,不會有主備刪除不一樣行的問題。

4、爲何會有 mixed 格式的 binlog?

一、爲何會有 mixed 這種 binlog 格式的存在場景的推理過程

基於上面的信息,咱們來討論一個問題:爲何會有 mixed 這種 binlog 格式的存在場景?推論過程是這樣的:

由於有些 statement 格式的 binlog 可能會致使主備不一致,因此要使用 row 格式。但 row 格式的缺點是,很佔空間。好比你用一個 delete 語句刪掉 10 萬行數據,用

statement 的話就是一個 SQL 語句被記錄到 binlog 中,佔用幾十個字節的空間。但若是用 row 格式的 binlog,就要把這 10 萬條記錄都寫到 binlog 中。這樣作,不只會佔
用更大的空間,同時寫 binlog 也要耗費 IO 資源,影響執行速度。

因此,MySQL 就取了個折中方案,也就是有了 mixed 格式的 binlog。mixed 格式的意思是,MySQL 本身會判斷這條 SQL 語句是否可能引發主備不一致,若是有可能,就
用 row 格式,不然就用 statement 格式。

也就是說,mixed 格式能夠利用 statment 格式的優勢,同時又避免了數據不一致的風險。

所以,若是你的線上 MySQL 設置的 binlog 格式是 statement 的話,那基本上就能夠認爲這是一個不合理的設置。你至少應該把 binlog 的格式設置爲 mixed。

好比咱們這個例子,設置爲 mixed 後,就會記錄爲 row 格式;而若是執行的語句去掉limit 1,就會記錄爲 statement 格式

二、什麼場景要求把 MySQL 的 binlog 格式設置成 row?

固然我要說的是,如今愈來愈多的場景要求把 MySQL 的 binlog 格式設置成 row。這麼作的理由有不少,我來給你舉一個能夠直接看出來的好處:恢復數據。

接下來,咱們就分別從 delete、insert 和 update 這三種 SQL 語句的角度,來看看數據恢復的問題。

經過圖 6 你能夠看出來,即便我執行的是 delete 語句,row 格式的 binlog 也會把被刪掉的行的整行信息保存起來。因此,若是你在執行完一條 delete 語句之後,發現刪錯數據
了,能夠直接把 binlog 中記錄的 delete 語句轉成 insert,把被錯刪的數據插入回去就能夠恢復了。

若是你是執行錯了 insert 語句呢?那就更直接了。row 格式下,insert 語句的 binlog 裏會記錄全部的字段信息,這些信息能夠用來精肯定位剛剛被插入的那一行。這時,你直接
把 insert 語句轉成 delete 語句,刪除掉這被誤插入的一行數據就能夠了。

若是執行的是 update 語句的話,binlog 裏面會記錄修改前整行的數據和修改後的整行數據。因此,若是你誤執行了 update 語句的話,只須要把這個 event 先後的兩行信息對調
一下,再去數據庫裏面執行,就能恢復這個更新操做了。

其實,由 delete、insert 或者 update 語句致使的數據操做錯誤,須要恢復到操做以前狀態的狀況,也時有發生。MariaDB 的Flashback工具就是基於上面介紹的原理來回滾數據的。

雖然 mixed 格式的 binlog 如今已經用得很少了,但這裏我仍是要再借用一下 mixed 格式來講明一個問題,來看一下這條 SQL 語句:

mysql> insert into t values(10,10, now());

若是咱們把 binlog 格式設置爲 mixed,你以爲 MySQL 會把它記錄爲 row 格式仍是statement 格式呢?

先不要着急說結果,咱們一塊兒來看一下這條語句執行的效果。

圖 7 mixed 格式和 now()

能夠看到,MySQL 用的竟然是 statement 格式。你必定會奇怪,若是這個 binlog 過了1 分鐘才傳給備庫的話,那主備的數據不就不一致了嗎?

三、TIMESTAMP時間戳的做用

接下來,咱們再用 mysqlbinlog 工具來看看:

圖 8 TIMESTAMP 命令

從圖中的結果能夠看到,原來 binlog 在記錄 event 的時候,多記了一條命令:SETTIMESTAMP=1546103491。它用 SET TIMESTAMP 命令約定了接下來的 now() 函數的
返回時間。

所以,不論這個 binlog 是 1 分鐘以後被備庫執行,仍是 3 天后用來恢復這個庫的備份,這個 insert 語句插入的行,值都是固定的。也就是說,經過這條 SET TIMESTAMP 命
令,MySQL 就確保了主備數據的一致性。

四、用 binlog 來恢復數據的標準作法

我以前看過有人在重放 binlog 數據的時候,是這麼作的:用 mysqlbinlog 解析出日誌,而後把裏面的 statement 語句直接拷貝出來執行。

你如今知道了,這個方法是有風險的。由於有些語句的執行結果是依賴於上下文命令的,
直接執行的結果極可能是錯誤的。

因此,用 binlog 來恢復數據的標準作法是,用 mysqlbinlog 工具解析出來,而後把解析結果整個發給 MySQL 執行。相似下面的命令:

mysqlbinlog master.000001  --start-position=2738 --stop-position=2973 | mysql -h127.0.0.1 -P13000 -u$user -p$pwd;

這個命令的意思是,將 master.000001 文件裏面從第 2738 字節到第 2973 字節中間這段內容解析出來,放到 MySQL 去執行

5、循環複製問題

經過上面對 MySQL 中 binlog 基本內容的理解,你如今能夠知道,binlog 的特性確保了在備庫執行相同的 binlog,能夠獲得與主庫相同的狀態。

所以,咱們能夠認爲正常狀況下主備的數據是一致的。也就是說,圖 1 中 A、B 兩個節點的內容是一致的。其實,圖 1 中我畫的是 M-S 結構,但實際生產上使用比較多的是雙 M
結構,也就是圖 9 所示的主備切換流程。

圖 9 MySQL 主備切換流程 -- 雙 M 結構

對比圖 9 和圖 1,你能夠發現,雙 M 結構和 M-S 結構,其實區別只是多了一條線,即:節點 A 和 B 之間老是互爲主備關係。這樣在切換的時候就不用再修改主備關係。

一、雙 M 結構存在什麼問題

可是,雙 M 結構還有一個問題須要解決。

業務邏輯在節點 A 上更新了一條語句,而後再把生成的 binlog 發給節點 B,節點 B 執行完這條更新語句後也會生成 binlog。(我建議你把參數 log_slave_updates 設置爲 on,
表示備庫執行 relay log 後生成 binlog)。

那麼,若是節點 A 同時是節點 B 的備庫,至關於又把節點 B 新生成的 binlog 拿過來執行了一次,而後節點 A 和 B 間,會不斷地循環執行這個更新語句,也就是循環複製了。這個
要怎麼解決呢?

二、如何解決雙 M 結構存在的問題

從上面的圖 6 中能夠看到,MySQL 在 binlog 中記錄了這個命令第一次執行時所在實例的 server id。所以,咱們能夠用下面的邏輯,來解決兩個節點間的循環複製的問題:

1. 規定兩個庫的 server id 必須不一樣,若是相同,則它們之間不能設定爲主備關係;
2. 一個備庫接到 binlog 並在重放的過程當中,生成與原 binlog 的 server id 相同的新的binlog;
3. 每一個庫在收到從本身的主庫發過來的日誌後,先判斷 server id,若是跟本身的相同,表示這個日誌是本身生成的,就直接丟棄這個日誌。

按照這個邏輯,若是咱們設置了雙 M 結構,日誌的執行流就會變成這樣:

1. 從節點 A 更新的事務,binlog 裏面記的都是 A 的 server id;
2. 傳到節點 B 執行一次之後,節點 B 生成的 binlog 的 server id 也是 A 的 server id;
3. 再傳回給節點 A,A 判斷到這個 server id 與本身的相同,就不會再處理這個日誌。因此,死循環在這裏就斷掉了。

6、小結

今天這篇文章,我給你介紹了 MySQL binlog 的格式和一些基本機制,是後面我要介紹的讀寫分離等系列文章的背景知識,但願你能夠認真消化理解。

binlog 在 MySQL 的各類高可用方案上扮演了重要角色。今天介紹的能夠說是全部MySQL 高可用方案的基礎。在這之上演化出了諸如多節點、半同步、MySQL group
replication 等相對複雜的方案。

我也跟你介紹了 MySQL 不一樣格式 binlog 的優缺點,和設計者的思考。但願你在作系統開發時候,也能借鑑這些設計思想。

最後,我給你留下一個思考題吧。

說到循環複製問題的時候,咱們說 MySQL 經過判斷 server id 的方式,斷掉死循環。可是,這個機制其實並不完備,在某些場景下,仍是有可能出現死循環

你能構造出一個這樣的場景嗎?又應該怎麼解決呢?

你能夠把你的設計和分析寫在評論區,我會在下一篇文章跟你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

7、上期問題時間

上期我留給你的問題是,你在何時會把線上生產庫設置成「非雙 1」。我目前知道的場景,有如下這些:

1. 業務高峯期。通常若是有預知的高峯期,DBA 會有預案,把主庫設置成「非雙 1」。
2. 備庫延遲,爲了讓備庫儘快遇上主庫。@永恆記憶和 @Second Sight 提到了這個場景。
3. 用備份恢復主庫的副本,應用 binlog 的過程,這個跟上一種場景相似。
4. 批量導入數據的時候。

通常狀況下,把生產庫改爲「非雙 1」配置,是設置

innodb_flush_logs_at_trx_commit=二、sync_binlog=1000。

評論區留言點贊板:

@way

同窗提到了一個有趣的現象,因爲從庫設置了binlog_group_commit_sync_delay 和binlog_group_commit_sync_no_delay_count 致使一直延遲的狀況。我
們在主庫設置這兩個參數,是爲了減小 binlog 的寫盤壓力。備庫這麼設置,尤爲在「快要追上」的時候,就反而會受這兩個參數的拖累。通常追主備就用「非雙 1」(追上記得改回來)。

@一大隻

同窗驗證了在 sync_binlog=0 的狀況下,設置 sync_delay 和sync_no_delay_count 的現象,點贊這種發現邊界的意識和手動驗證的好習慣。是這樣的:
sync_delay 和 sync_no_delay_count 的邏輯先走,所以該等仍是會等。等到知足了這兩個條件之一,就進入 sync_binlog 階段。這時候若是判斷 sync_binlog=0,就直接跳過,仍是不調 fsync。


@鍋子

同窗提到,設置 sync_binlog=0 的時候,仍是能夠看到 binlog 文件立刻作了修改。這個是對的,咱們說「寫到了 page cache」,就是文件系統的 page cache。而你用 ls 命令看到的就是文件系統返回的結果。

相關文章
相關標籤/搜索