《MySQL實戰45講》實踐篇 24-29 學習筆記 (主備篇)

圖片來自於極客時間,如有版權問題,請聯繫我刪除。
在這裏插入圖片描述

24 | MySQL是怎麼保證主備一致的?

主備切換流程如下:
在這裏插入圖片描述
建議把節點 B(也就是備庫)設置成只讀(readonly)模式。
1.有時候一些運營類的查詢語句會被放到備庫上去查,設置爲只讀可以防止誤操作;
2.防止切換邏輯有 bug,比如切換過程中出現雙寫,造成主備不一致;
3.可以用 readonly 狀態,來判斷節點的角色

readonly 設置對超級 (super) 權限用戶是無效的,而用於同步更新數據的線程,就擁有超級權限。(所以一個readonly的從庫還是可以同步主庫的數據的)

主從同步的流程:
在這裏插入圖片描述
一個事務日誌同步的完整過程是這樣的:

  1. 在備庫 B 上通過 change master 命令,設置主庫 A 的 IP、端口、用戶名、密碼,以及文件名和日誌偏移量。
  2. 在備庫 B 上執行 start slave 命令,啓動兩個線程, io_thread 和 sql_thread
  3. 主庫 A 校驗完用戶名、密碼後,按照備庫 B 傳過來的位置,從本地讀取 binlog,發給 B。
  4. 備庫 B 拿到 binlog 後,寫到本地文件–中轉日誌(relay log)。
  5. sql_thread 讀取中轉日誌,解析出日誌裏的命令,並執行。

binlog 的三種格式對比

當 binlog_format=statement 時,binlog 裏面記錄的就是 SQL 語句的原文
但是在statement,如果一個更新SQL使用了limit,在反覆執行的時候可能實際用到的索引並不一樣,會導致結果可能出現不一致

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

mysql會產生一個warning
在這裏插入圖片描述
binlog_format=row, row 格式的 binlog 裏沒有了 SQL 語句的原文,而是替換成了兩個 event:Table_map 和 Delete_rows(這裏是針對delete語句)。
Table_map event,用於說明接下來要操作的表是 test 庫的表 t;
Delete_rows event,用於定義刪除的行爲

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

爲什麼會有 mixed 這種 binlog 格式的存在場景?

爲了避免主從不同步的同時,節省存儲空間和IO資源浪費
mixed 格式的意思是,MySQL 自己會判斷這條 SQL 語句是否可能引起主備不一致,如果有可能,就用 row 格式,否則就用 statement 格式。

在越來越多的場景要求把 MySQL 的 binlog 格式設置成 row。這麼做的理由有很多,我來給你舉一個可以直接看出來的好處:恢復數據 (statement的格式用來恢復數據比較困難,因爲不會記錄到底刪除了什麼數據)

有人 mysqlbinlog 解析出日誌,然後把裏面的 statement 語句直接拷貝出來執行 這是不提倡的
因爲有些語句的執行結果是依賴於上下文命令的,直接執行的結果很可能是錯誤的。
比如:
insert into t values(10,10, now());
在這裏插入圖片描述
它用 SET TIMESTAMP命令約定了接下來的now()函數的返回時間,這樣如果這個語句在從庫上是什麼時候執行的,其插入的數據和主庫都是一致的

所以,用 binlog 來恢復數據的標準做法是,用 mysqlbinlog 工具解析出來,然後把解析結果整個發給 MySQL 執行
類似如下:

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

主庫 A 從本地讀取 binlog,發給從庫 B;
老師,請問這裏的本地是指文件系統的 page cache還是disk呢?
作者回復: 好問題,
是這樣的,對於A的線程來說,就是“讀文件”,

  1. 如果這個文件現在還在 page cache中,那就最好了,直接讀走;
  2. 如果不在page cache裏,就只好去磁盤讀
    這個行爲是文件系統控制的,MySQL只是執行“讀文件”這個操作

雙主架構下的循環複製問題

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

那麼,如果節點 A 同時是節點 B 的備庫,相當於節點A又把節點 B 新生成的 binlog 拿過來執行了一次,就會出現循環複製的問題了

解決思路是利用MySQL 在 binlog 中記錄了這個命令第一次執行時所在實例的 server id

具體流程:一個備庫接到 binlog 並在重放的過程中,生成與原 binlog 的 server id 相同的新的 binlog;而每個庫在收到從自己的主庫發過來的日誌後,先判斷 server id,如果跟自己的相同,表示這個日誌是自己生成的,就直接丟棄這個日誌

課後問題:我們說 MySQL 通過判斷 server id 的方式,斷掉死循環。但是,這個機制其實並不完備,在某些場景下,還是有可能出現死循環。

一種場景是,在一個主庫更新事務後,用命令 set global server_id=x 修改了 server_id。等日誌再傳回來的時候,發現 server_id 跟自己的 server_id 不同,就只能執行了。

另一種場景是,有三個節點的時候,trx1 是在節點 B 執行的,因此 binlog 上的 server_id 就是 B,binlog 傳給節點 A,然後 A 和 A’搭建了雙 M 結構,就會出現循環複製。
在這裏插入圖片描述

這種三節點複製的場景,做數據庫遷移的時候會出現。如果出現了循環複製,可以在 A 或者 A’上,執行如下命令:

stop slave;
CHANGE MASTER TO IGNORE_SERVER_IDS=(server_id_of_B);
start slave;

這樣這個節點收到日誌後就不會再執行。過一段時間後,再執行下面的命令把這個值改回來。

stop slave;
CHANGE MASTER TO IGNORE_SERVER_IDS=();
start slave;

25 | MySQL是怎麼保證高可用的?(主備延遲)

與數據同步有關的時間點主要包括以下三個:
1.主庫 A 執行完成一個事務,寫入 binlog,我們把這個時刻記爲 T1;
2.之後傳給備庫 B,我們把備庫 B 接收完這個 binlog 的時刻記爲 T2;
3.備庫 B 執行完成這個事務,我們把這個時刻記爲 T3。
所謂主備延遲,就是同一個事務,在備庫執行完成的時間和主庫執行完成的時間之間的差值,也就是 T3-T1
在網絡正常的時候,日誌從主庫傳給備庫所需的時間是很短的,即 T2-T1 的值是非常小的。也就是說,網絡正常情況下,主備延遲的主要來源是備庫接收完 binlog 和執行完這個事務之間的時間差。
所以說,主備延遲最直接的表現是,備庫消費中轉日誌(relay log)的速度,比主庫生產 binlog 的速度要慢。

主備延遲的來源

首先,有些部署條件下,備庫所在機器的性能要比主庫所在的機器性能差

做這種部署時,一般都會將備庫設置爲“非雙 1”的模式。

第二種常見的可能了,即備庫的壓力大
將大量查詢工作交給了備庫
我們一般可以這麼處理:

  1. 一主多從。除了備庫外,可以多接幾個從庫,讓這些從庫來分擔讀的壓力。2.通過 binlog 輸出到外部系統,比如 Hadoop 這類系統,讓外部系統提供統計類查詢的能力

這就是第三種可能了,即大事務
主庫上必須等事務執行完成纔會寫入 binlog,再傳給備庫。所以,如果一個主庫上的語句執行 10 分鐘,那這個事務很可能就會導致從庫延遲 10 分鐘。

不要一次性地用 delete 語句刪除太多數據 其實,這就是一個典型的大事務場景

另一種典型的大事務場景,就是大表 DDL。這個場景,我在前面的文章中介紹過。處理方案就是,計劃內的 DDL,建議使用 gh-ost 方案(這裏,你可以再回顧下第 13 篇文章《爲什麼表數據刪掉一半,表文件大小不變?》中的相關內容)。

*備庫的並行複製能力。

在主備切換的時候,有相應的不同策略*

可靠性優先策略

在雙 M 結構下,從狀態 1 到狀態 2 切換的詳細過程是這樣的:
1.判斷備庫 B 現在的 seconds_behind_master,如果小於某個值(比如 5 秒)繼續下一步,否則持續重試這一步;
2.把主庫 A 改成只讀狀態,即把 readonly 設置爲 true;
3.判斷備庫 B 的 seconds_behind_master 的值,直到這個值變成 0 爲止;4.把備庫 B 改成可讀寫狀態,也就是把 readonly 設置爲 false;
5.把業務請求切到備庫 B

在切換過程中,從步驟2-5期間,MySQL集羣都不可用的

可用性優先策略

如果強行把步驟 4、5 調整到最開始執行,也就是說不等主備數據同步,直接把連接切到備庫 B,並且讓備庫 B 可以讀寫,那麼系統幾乎就沒有不可用時間了。

這個切換流程的代價,就是可能出現數據不一致的情況。
在這裏插入圖片描述

如果使用可用性優先策略,建議設置 binlog_format=row,情況又會怎樣呢?因爲 row 格式在記錄 binlog 的時候,會記錄新插入的行的所有字段值,所以最後只會有一行不一致。而且,兩邊的主備同步的應用線程會報錯 duplicate key error 並停止。
也就是說,這種情況下,備庫 B 的 (5,4) 和主庫 A 的 (5,5) 這兩行數據,都不會被對方執行。

主備切換的可用性優先策略會導致數據不一致。因此,大多數情況下,我都建議你使用可靠性優先策略(如果一定要使用可用性優先策略,建議選擇binlog_format=row))。畢竟對數據服務來說的話,數據的可靠性一般還是要優於可用性的。

有沒有哪種情況數據的可用性優先級更高呢?
答案是,有的。
有一個庫的作用是記錄操作日誌。這時候,如果數據不一致可以通過 binlog 來修補,而這個短暫的不一致也不會引發業務問題。同時,業務系統依賴於這個日誌寫入邏輯,如果這個庫不可寫,會導致線上的業務操作無法執行。

改進措施就是,讓業務邏輯不要依賴於這類日誌的寫入。也就是說,日誌寫入這個邏輯模塊應該可以降級,比如寫到本地文件,或者寫到另外一個臨時庫裏面。

按照可靠性優先的思路,異常切換會是什麼效果?

假設,主庫 A 和備庫 B 間的主備延遲是 30 分鐘,這時候主庫 A 掉電了,HA 系統要切換 B 作爲主庫。我們在主動切換的時候,可以等到主備延遲小於 5 秒的時候再啓動切換,但這時候已經別無選擇了。

採用可靠性優先策略的話,你就必須得等到備庫 B 的 seconds_behind_master=0 之後,才能切換。但現在系統處於完全不可用的狀態(主庫 A 掉電後,我們的連接還沒有切到備庫 B)

"我"認爲這種情形下應該也只能使用可用性優先策略了,但是由於主備延遲,導致客戶端查詢可能看不到之前執行完成的事務,會認爲有“數據丟失”。但是對於一些業務來說,查詢到“暫時丟失數據的狀態”也是不能被接受的。(比方說訂單的情形)

在滿足數據可靠性的前提下,MySQL 高可用系統的可用性,是依賴於主備延遲的。延遲的時間越小,在主庫故障的時候,服務恢復需要的時間就越短,可用性就越高。

在實際的應用中,我更建議使用可靠性優先的策略。畢竟保證數據準確,應該是數據庫服務的底線。在這個基礎上,通過減少主備延遲,提升系統的可用性。

課後問題:
一般現在的數據庫運維繫統都有備庫延遲監控,其實就是在備庫上執行 show slave status,採集seconds_behind_master的值。

假設,現在你看到你維護的一個備庫,它的延遲監控的圖像類似圖6,是一個45°斜向上的線段,你覺得可能是什麼原因導致呢?你又會怎麼去確認這個原因呢?

在這裏插入圖片描述
備庫的同步在這段時間完全被堵住
生這種現象典型的場景主要包括兩種:
1.一種是大事務(包括大表 DDL、一個事務操作很多行);
2.還有一種情況比較隱蔽,就是備庫起了一個長事務,比如

begin;
select * from t limit 1;

然後就不動了

這時候主庫對錶 t 做了一個加字段操作,即使這個表很小,這個 DDL 在備庫應用的時候也會被堵住,也能看到這個現象

26 | 備庫爲什麼會延遲好幾個小時?

如果備庫執行日誌的速度持續低於主庫生成日誌的速度,那這個延遲就有可能不斷增加,然後變成了小時級別

在官方的 5.6 版本之前,MySQL 只支持單線程複製,由此在主庫併發高、TPS 高時就會出現嚴重的主備延遲問題。

多線程複製

在這裏插入圖片描述
1.事務能不能按照輪詢的方式分發給各個 worker?
不行的。因爲,事務被分發給 worker 以後,不同的 worker 就獨立執行了。但是,由於 CPU 的調度策略,很可能第二個事務最終比第一個事務先執行。而如果這時候剛好這兩個事務更新的是同一行,也就意味着,同一行上的兩個事務,在主庫和備庫上的執行順序相反,會導致主備不一致的問題。

2.同一個事務的多個更新語句,能不能分給不同的 worker 來執行呢?
也不行。一個事務更新了表 t1 和表 t2 中的各一行,如果這兩條更新語句被分到不同 worker 的話,雖然最終的結果是主備一致的,但如果表 t1 執行完成的瞬間,備庫上有一個查詢,就會看到這個事務“更新了一半的結果”,破壞了事務邏輯的隔離性。

coordinator 在分發的時候,需要滿足以下這兩個基本要求:
1.不能造成更新覆蓋。這就要求更新同一行的兩個事務,必須被分發到同一個 worker 中。
2.同一個事務不能被拆開,必須放到同一個 worker 中。

MySQL 5.5 版本的並行複製策略

自己實現的兩個並行複製策略

按表分發策略

每個 worker 線程對應一個 hash 表,用於保存當前正在這個 worker 的“執行隊列”裏的事務所涉及的表。hash 表的 key 是“庫名. 表名”,value 是一個數字,表示隊列中有多少個事務修改這個表。
新的事務T:
1.當前worker中都沒有操作事務T涉及到的表,那選擇最空閒的一個
2.有>1的worker在操作事務T涉及到的表,則等到只剩一個
3.只有一個worker在操作事務T涉及到的表,就分給該worker

缺點:如果碰到熱點表,就變成類似單線程複製了

按行分發策略

按行復制的核心思路是:如果兩個事務沒有更新相同的行,它們在備庫上可以並行執行。要求 binlog 格式必須是 row。且必須有主鍵,不能有外鍵(表如果有外鍵,級聯更新的行就不會記錄在binlog中)

每個 worker,分配一個 hash 表。只是要實現按行分發,這時候的 key,就必須是“庫名 + 表名 + 唯一鍵的值”。(這裏的唯一鍵不單單指主鍵,還需要考慮唯一索引), key 應該是“庫名 + 表名 + 索引 a 的名字 +a 的值”

eg.在表 t1 上執行 update t1 set a=1 where id=2 語句(id爲主鍵,a爲唯一索引)
這個事務的 hash 表就有三項:
1.key=hash_func(db1+t1+“PRIMARY”+2), value=2; 這裏 value=2 是因爲修改前後的行 id 值不變,出現了兩次。
2.key=hash_func(db1+t1+“a”+2), value=1,表示會影響到這個表 a=2 的行。(a=2爲修改前的值)
3.key=hash_func(db1+t1+“a”+1), value=1,表示會影響到這個表 a=1 的行。

按行分發策略的並行度更高。 但對於操作很多行的大事務的話,按行分發的策略有兩個問題:
1.耗費內存。比如一個語句要刪除 100 萬行數據,這時候 hash 表就要記錄 100 萬個項。
2.耗費 CPU。解析 binlog,然後計算 hash 值,對於大事務,這個成本還是很高的

所以實現按行分發策略的時候會設置一個閾值,單個事務如果超過設置的行數閾值(比如超過 10 萬行),就暫時退化爲單線程模式
1.coordinator 暫時先 hold 住這個事務;
2.等待所有 worker 都執行完成,變成空隊列;
3.coordinator 直接執行這個事務;
4.恢復並行模式

MySQL 5.6 版本的並行複製策略

官方 MySQL5.6 版本,支持了並行複製,只是支持的粒度是按庫並行
優點:
1.構造 hash 值快,只需要庫名;一個實例上 DB 數不多,不會出現需要構造 100 萬個項這種情況。
2.不要求 binlog 的格式
但是對於單庫或者DB熱點不同,其效率就不是很好了(當然可以做分庫)

MariaDB 的並行複製策略

redo log 組提交 (group commit) 優化, 而 MariaDB 的並行複製策略利用的就是這個特性:
1.能夠在同一組裏提交的事務,一定不會修改同一行;
2.主庫上可以並行執行的事務,備庫上也一定是可以並行執行的。

流程:

1.在一組裏面一起提交的事務,有一個相同的 commit_id(遞增就行)
2.commit_id 直接寫到 binlog 裏面;
3.傳到備庫應用的時候,相同 commit_id 的事務分發到多個 worker 執行;
4.這一組全部執行完成後,coordinator 再去取下一批。

不足:
1.並沒有徹底實現“真正的模擬主庫併發度”這個目標。在主庫上,一組事務在 commit 的時候,下一組事務是同時處於“執行中”狀態的。而這裏不行
2.如果有大事務,就會由於當前事務沒有commit導致下一個組事務還不能開始執行

開啓並行複製後,事務是按照組來提交的,從庫也是根據commit_id來回放,如果從庫也開啓binlog的話,那是不是存在主從的binlog event寫入順序不一致的情況呢?

作者回復: 是有可能binlog event寫入順序不同的,好問題

MySQL 5.7 的並行複製策略

參數 : slave-parallel-type 參考了MariaDB 的並行複製策略思想

1.配置爲 DATABASE,表示使用 MySQL 5.6 版本的按庫並行策略;
2.配置爲 LOGICAL_CLOCK,表示的就是類似 MariaDB 的策略。不過,MySQL 5.7 這個策略,針對並行度做了優化。

MariaDB 這個策略的核心,是“所有處於 commit”狀態的事務可以並行。事務處於 commit 狀態,表示已經通過了鎖衝突的檢驗了
在這裏插入圖片描述

但其實,不用等到 commit 階段,只要能夠到達 redo log prepare 階段,就表示事務已經通過鎖衝突的檢驗了。

MySQL 5.7 並行複製策略的思想是:
1.同時處於 prepare 狀態的事務,在備庫執行時是可以並行的;
2.處於 prepare 狀態的事務,與處於 commit 狀態的事務之間,在備庫執行時也是可以並行的。

  1. binlog_group_commit_sync_delay 參數,表示延遲多少微秒後才調用 fsync;
  2. binlog_group_commit_sync_no_delay_count 參數,表示累積多少次以後才調用 fsync。

這兩個參數是用於故意拉長 binlog 從 write 到 fsync 的時間,以此減少 binlog 的寫盤次數。在 MySQL 5.7 的並行複製策略裏,它們可以用來製造更多的“同時處於 prepare 階段的事務”

這兩個參數,既可以“故意”讓主庫提交得慢些,又可以讓備庫執行得快些。在 MySQL 5.7 處理備庫延遲的時候,可以考慮調整這兩個參數值,來達到提升備庫複製併發度的目的。

MySQL 5.7.22 的並行複製策略

MySQL 5.7.22 版本里,MySQL 增加了一個新的並行複製策略,基於 WRITESET 的並行複製。
新增了一個參數 binlog-transaction-dependency-tracking,控制是否啓用這個新策略。

  1. COMMIT_ORDER,表示的就是前面介紹的,根據同時進入 prepare 和 commit 來判斷是否可以並行的策略。
  2. WRITESET,表示的是對於事務涉及更新的每一行,計算出這一行的 hash 值,組成集合 writeset。如果兩個事務沒有操作相同的行,也就是說它們的 writeset 沒有交集,就可以並行。
  3. WRITESET_SESSION,是在 WRITESET 的基礎上多了一個約束,即在主庫上同一個線程先後執行的兩個事務,在備庫執行的時候,要保證相同的先後順序。

爲了唯一標識,這個 hash 值是通過“庫名 + 表名 + 索引名 + 值”計算出來的。如果一個表上除了有主鍵索引外,還有其他唯一索引,那麼對於每個唯一索引,insert 語句對應的 writeset 就要多增加一個 hash 值。

最大的優勢(比起按行分發策略):
writeset是在主庫生成後並直接寫入binlog的,對binlog的格式也沒有要求(因爲備庫分發依賴於主庫生成的writeset而不是binlog內容)

對於“表上沒主鍵”和“外鍵約束”的場景,WRITESET 策略也是沒法並行的,也會暫時退化爲單線程模型。

課後問題:
假設一個MySQL 5.7.22版本的主庫,單線程插入了很多數據,過了3個小時後,我們要給這個主庫搭建一個相同版本的備庫。
這時候,你爲了更快地讓備庫追上主庫,要開並行複製。在binlog-transaction-dependency-tracking參數的COMMIT_ORDER、WRITESET和WRITE_SESSION這三個取值中,你會選擇哪一個呢?
你選擇的原因是什麼?如果設置另外兩個參數,你認爲會出現什麼現象呢?

應該將這個參數設置爲 WRITESET。
由於主庫是單線程壓力模式,所以每個事務的 commit_id 都不同,那麼設置爲 COMMIT_ORDER 模式的話,從庫也只能單線程執行。
同樣地,由於 WRITESET_SESSION 模式要求在備庫應用日誌的時候,同一個線程的日誌必須與主庫上執行的先後順序相同,也會導致主庫單線程壓力模式下退化成單線程複製。
所以,應該將 binlog-transaction-dependency-tracking 設置爲 WRITESET。

27 | 主庫出問題了,從庫怎麼辦?-- 主備切換

一主多從架構下,主庫發生故障後
在這裏插入圖片描述
A’會成爲新的主庫,從庫B、C、D也要改接到A’。

基於位點的主備切換

切換主庫時,從庫需要執行change master命令

CHANGE MASTER TO 
MASTER_HOST=$host_name 
MASTER_PORT=$port 
MASTER_USER=$user_name 
MASTER_PASSWORD=$password 
MASTER_LOG_FILE=$master_log_name 
MASTER_LOG_POS=$master_log_pos

MASTER_LOG_FILE 和 MASTER_LOG_POS 表示,要從主庫的 master_log_name 文件的 master_log_pos 這個位置的日誌繼續同步,也就是同步位點

一種取同步位點的方法是這樣的:

  1. 等待新主庫 A’把中轉日誌(relay log)全部同步完成;
  2. 在 A’上執行 show master status 命令,得到當前 A’上最新的 File 和 Position;
  3. 取原主庫 A 故障的時刻 T;
  4. 用 mysqlbinlog 工具解析 A’的 File,得到 T 時刻的位點。

因爲同步位點並不是精確的,可能會出現bin log重複執行的情形,出現主鍵衝突等錯誤。通常情況下,我們在切換任務的時候,要先主動跳過這些錯誤,有兩種常用的方法。
1.主動跳過一個事務。

set global sql_slave_skip_counter=1;
start slave;

sql_slave_skip_counter 跳過的是一個 event,由於 MySQL 總不能執行一半的事務,所以既然跳過了一個 event,就會跳到這個事務的末尾,因此 set global sql_slave_skip_counter=1;start slave 是可以跳過整個事務的。

2.通過設置 slave_skip_errors 參數,直接設置跳過指定的錯誤。
在執行主備切換時,有這麼兩類錯誤,是經常會遇到的:
1.1062 錯誤是插入數據時唯一鍵衝突;
2.1032 錯誤是刪除數據時找不到行。

GTID

GTID 的全稱是 Global Transaction Identifier,也就是全局事務 ID,是一個事務在提交的時候生成的,是這個事務的唯一標識。
它由兩部分組成,GTID=server_uuid:gno

server_uuid 是一個實例第一次啓動時自動生成的,是一個全局唯一的值;gno 是一個整數,初始值是 1,每次提交事務的時候分配給這個事務(回滾的不會算入),並加 1

GTID 模式的啓動: 啓動一個 MySQL 實例的時候,加上參數 gtid_mode=on 和 enforce_gtid_consistency=on

這個 GTID 有兩種生成方式,而使用哪種方式取決於 session 變量 gtid_next 的值。
1.如果 gtid_next=automatic,代表使用默認值。
這時,MySQL 就會把 server_uuid:gno 分配給這個事務。
a. 記錄 binlog 的時候,先記錄一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’;
b. 把這個 GTID 加入本實例的 GTID 集合。
2.如果 gtid_next 是一個指定的 GTID 的值,比如通過 set gtid_next='current_gtid’指定爲 current_gtid,那麼就有兩種可能:
a. 如果 current_gtid 已經存在於實例的 GTID 集合中,接下來執行的這個事務會直接被系統忽略;
b. 如果 current_gtid 沒有存在於實例的 GTID 集合中,就將這個 current_gtid 分配給接下來要執行的事務,也就是說系統不需要給這個事務生成新的 GTID,因此 gno 也不用加 1。

一個 current_gtid 只能給一個事務使用。這個事務提交後,如果要執行下一個事務,就要執行 set 命令,把 gtid_next 設置成另外一個 gtid 或者 automatic

使用GTID的話,如果insert sql是重複的,可以通過把對應的GTID添加到從庫的GTID的集合中來避免出現重複key的異常

GTID 的主備切換

GTID模式下的切換命令

CHANGE MASTER TO 
MASTER_HOST=$host_name 
MASTER_PORT=$port 
MASTER_USER=$user_name 
MASTER_PASSWORD=$password 
master_auto_position=1 

實例 A’的 GTID 集合記爲 set_a,實例 B 的 GTID 集合記爲 set_b。
主備切換的時候,B先將set_b發送給A’,計算出set_a和set_b之間的差集 (因爲A’和B都是原來A的從庫,bin log執行的速度可能不一樣,所以兩個實例的GTID集合不一定是一致的)
判斷 A’本地是否包含了這個差集需要的所有 binlog 事務。
a. 如果不包含,表示 A’已經把實例 B 需要的 binlog 給刪掉了,直接返回錯誤;
b. 如果確認全部包含,A’從自己的 binlog 文件裏面,找出第一個不在 set_b 的事務,發給 B;

之後就從這個事務開始,往後讀文件,按順序取 binlog 發給 B 去執行。

隱含條件:在基於GTID的主備關係裏,系統認爲只要建立主備關係,就必須保證主庫發給備庫的日誌是完整的。因此,如果實例B需要的日誌已經不存在,A’就拒絕把日誌發給B。

GTID 和在線 DDL

業務高峯期的慢查詢性能問題時,分析到如果是由於索引缺失引起的性能問題,我們可以通過在線加索引來解決。但是,考慮到要避免新增索引對主庫性能造成的影響,我們可以先在備庫加索引,然後再切換。

當時我說,在雙 M 結構下,備庫執行的 DDL 語句也會傳給主庫,爲了避免傳回後對主庫造成影響,要通過 set sql_log_bin=off 關掉 binlog

評論區有位同學提出了一個問題:這樣操作的話,數據庫裏面是加了索引,但是 binlog 並沒有記錄下這一個更新,是不是會導致數據和日誌不一致?

假設,這兩個互爲主備關係的庫還是實例 X 和實例 Y,且當前主庫是 X,並且都打開了 GTID 模式。
這時的主備切換流程可以變成下面這樣:
1.在實例 X 上執行 stop slave。
2.在實例 Y 上執行 DDL 語句。(不需要關閉 binlog)。執行完成後,查出這個 DDL 語句對應的 GTID,並記爲 server_uuid_of_Y:gno。
3.到實例 X 上執行以下語句序列:

set GTID_NEXT=“server_uuid_of_Y:gno”;
begin;
commit;
set gtid_next=automatic;
start slave;

接下來,執行完主備切換,然後照着上述流程再執行一遍即可。
通過GTID,讓實例 Y 的更新有 binlog 記錄,同時也可以確保不會在實例 X (主庫)上執行這條更新。

課後問題: 在 GTID 模式下,如果一個新的從庫接上主庫,但是需要的 binlog 已經沒了,要怎麼做?

1.如果業務允許主從不一致的情況,那麼可以在主庫上先執行 show global variables like ‘gtid_purged’,得到主庫已經刪除的 GTID 集合,假設是 gtid_purged1;然後先在從庫上執行 reset master,再執行 set global gtid_purged =‘gtid_purged1’;最後執行 start slave,就會從主庫現存的 binlog 開始同步。binlog 缺失的那一部分,數據在從庫上就可能會有丟失,造成主從不一致。

2.如果需要主從數據一致的話,最好還是通過重新搭建從庫來做。

3.如果有其他的從庫保留有全量的 binlog 的話,可以把新的從庫先接到這個保留了全量 binlog 的從庫,追上日誌以後,如果有需要,再接回主庫。
4.如果 binlog 有備份的情況,可以先在從庫上應用缺失的 binlog,然後再執行 start slave。

28 | 讀寫分離有哪些坑?

客戶端直連方案,
因爲少了一層 proxy 轉發,所以查詢性能稍微好一點兒,並且整體架構簡單,排查問題更方便。但在出現主備切換、庫遷移等操作的時候,客戶端都會感知到,並且需要調整數據庫連接信息。一般採用這樣的架構,一定會伴隨一個負責管理後端的組件,比如 Zookeeper,儘量讓業務端只專注於業務邏輯開發。
帶 proxy 的架構,對客戶端比較友好
客戶端不需要關注後端細節,連接維護、後端信息維護等工作,都是由 proxy 完成的。但這樣的話,對後端維護團隊的要求會更高。而且,proxy 也需要有高可用架構。因此,帶 proxy 架構的整體就相對比較複雜

由於主從可能存在延遲,客戶端執行完一個更新事務後馬上發起查詢,如果查詢選擇的是從庫的話,就有可能讀到剛剛的事務更新之前的狀態。
這種“在從庫上會讀到系統的一個過期狀態”的現象,在這篇文章裏,我們暫且稱之爲“過期讀”

強制走主庫方案

將查詢請求做分類,需要實時讀取到最新的數據的走主庫,可以接受延遲的走從庫
但是對於要求所有查詢都不能是過期讀的,就沒法進行讀寫分離了

Sleep 方案

主庫更新後,讀從庫之前先 sleep 一下。具體的方案就是,類似於執行一條 select sleep(1) 命令。
這個方案存在的問題就是不精確。
這個不精確包含了兩層意思:
1.如果這個查詢請求本來 0.5 秒就可以在從庫上拿到正確結果,也會等 1 秒;
2.如果延遲超過 1 秒,還是會出現過期讀

判斷主備無延遲方案

show slave status 結果裏的 seconds_behind_master 參數的值,可以用來衡量主備延遲時間的長短。

第一種確保主備無延遲的方法是,每次從庫執行查詢請求前,先判斷 seconds_behind_master 是否已經等於 0。如果還不等於 0 ,那就必須等到這個參數變爲 0 才能執行查詢請求

show slave status結果
在這裏插入圖片描述
第二種方法,對比位點確保主備無延遲:

Master_Log_File 和 Read_Master_Log_Pos,表示的是讀到的主庫的最新位點;
Relay_Master_Log_File 和 Exec_Master_Log_Pos,表示的是備庫執行的最新位點
這兩組值完全相同,就表示接收到的日誌已經同步完成

第三種方法,對比 GTID 集合確保主備無延遲:

  • Auto_Position=1 ,表示這對主備關係使用了 GTID 協議。
  • Retrieved_Gtid_Set,是備庫收到的所有日誌的 GTID 集合;
  • Executed_Gtid_Set,是備庫所有已經執行完成的 GTID 集合。

如果這兩個集合相同,也表示備庫接收到的日誌都已經同步完成

但還是有一個問題,上面判斷主備無延遲的邏輯,是“備庫收到的日誌都執行完成了”。但是,從 binlog 在主備之間狀態的分析中,不難看出還有一部分日誌,處於客戶端已經收到提交確認,而備庫還沒收到日誌的狀態。也就是在從主庫向從庫傳遞binlog的過程中還會存在一部分延遲的數據

配合 semi-sync

半同步複製
semi-sync 做了這樣的設計:

  1. 事務提交的時候,主庫把 binlog 發給從庫;
  2. 從庫收到 binlog 以後,發回給主庫一個 ack,表示收到了;
  3. 主庫主要收到這個 ack(任何一個從庫) 以後,才能給客戶端返回“事務完成”的確認

semi-sync 配合前面關於位點的判斷,就能夠確定在從庫上執行的查詢請求,可以避免過期讀。
但是,semi-sync+ 位點判斷的方案,只對一主一備的場景是成立的。在一主多從場景中,主庫只要等到一個從庫的 ack,就開始給客戶端返回確認

判斷同步位點的方案還有另外一個潛在的問題,即:如果在業務更新的高峯期,主庫的位點或者 GTID 集合更新很快,那麼上面的兩個位點等值判斷就會一直不成立,很可能出現從庫上遲遲無法響應查詢請求的情況

當發起一個查詢請求以後,我們要得到準確的結果,其實並不需要等到“主備完全同步”。 只需要當前查詢的結果在從庫已經同步就行了

semi-sync 配合判斷主備無延遲的方案,存在兩個問題:
1.一主多從的時候,在某些從庫執行查詢請求會存在過期讀的現象;
2.在持續延遲的情況下,可能出現過度等待的問題。

等主庫位點方案

select master_pos_wait(file, pos[, timeout]);

它是在從庫執行的;
參數 file 和 pos 指的是主庫上的文件名和位置;
timeout 可選,設置爲正整數 N 表示這個函數最多等待 N 秒。

這個命令正常返回的結果是一個正整數 M,表示從命令開始執行,到應用完 file 和 pos 表示的 binlog 位置,執行了多少事務。

如果執行期間,備庫同步線程發生異常,則返回 NULL;
如果等待超過 N 秒,就返回 -1;
如果剛開始執行的時候,就發現已經執行過這個位置了,則返回 0

採用下面的流程就可以避免出現上述主備無延遲配合半同步複製情形下的問題:

  1. trx1 事務更新完成後,馬上執行 show master status 得到當前主庫執行到的 File 和 Position;
  2. 選定一個從庫執行查詢語句,在從庫上執行 select master_pos_wait(File, Position, 1);
  3. 如果返回值是 >=0 的正整數,則在這個從庫執行查詢語句;否則,到主庫執行查詢語句
等待GTID 方案

前提是開啓了GTID模式

select wait_for_executed_gtid_set(gtid_set, 1);

這條命令的邏輯是:等待,直到這個庫執行的事務中包含傳入的 gtid_set,返回 0;超時返回 1。

MySQL 5.7.6 版本開始,允許在執行完更新類事務後,把這個事務的 GTID 返回給客戶端(將參數 session_track_gtids 設置爲 OWN_GTID,然後通過 API 接口 mysql_session_track_get_first 從返回包解析出 GTID 的值),這樣等 GTID 的方案就可以減少一次查詢。
https://dev.mysql.com/doc/refman/5.7/en/c-api-functions.html

具體流程和等主庫位點的類似

課後問題: 如果使用 GTID 等位點的方案做讀寫分離,在對大表做 DDL 的時候會怎麼樣。

假設,這條語句在主庫上要執行 10 分鐘,提交後傳到備庫就要 10 分鐘(這裏的10min就是指DDL在主庫的執行時間,典型的大事務)。那麼,在主庫 DDL 之後再提交的事務的 GTID,去備庫查的時候,就會等 10 分鐘纔出現。

這樣,這個讀寫分離機制在這 10 分鐘之內都會超時,然後走主庫。這種預期內的操作,應該在業務低峯期的時候,確保主庫能夠支持所有業務查詢,然後把讀請求都切到主庫,再在主庫上做 DDL。等備庫延遲追上以後,再把讀請求切回備庫。

使用gh-ost方案來解決這個問題也是不錯的選擇

我的疑問:
那10min是等DDL執行完,並寫完binlog可以發送到從庫,期間其他更新SQL在主庫不是也可以正常執行嗎,那相關的操作不是也能寫入binlog,爲什麼不能發送給從庫執行啊??

今天的問題,大表做DDL的時候可能會出現主從延遲,導致等 GTID 的方案可能會導致這部分流量全打到主庫,或者全部超時。如果這部分流量太大的話,我會選擇上一篇文章介紹的兩種方法:1.在各個從庫先SET sql_log_bin = OFF,然後做DDL,所有從庫及備主全做完之後,做主從切換,最後在原來的主庫用同樣的方式做DDL。2.從庫上執行DDL;將從庫上執行DDL產生的GTID在主庫上利用生成一個空事務GTID的方式將這個GTID在主庫上生成出來。各個從庫做完之後再主從切換,然後再在原來的主庫上同樣做一次。需要注意的是如果有MM架構的情況下,承擔寫職責的主庫上的slave需要先停掉。

29 | 如何判斷一個數據庫是不是出問題了?

select 1 判斷

select 1 成功返回,只能說明這個庫的進程還在,並不能說明主庫沒問題

set global innodb_thread_concurrency=3;
在這裏插入圖片描述
innodb_thread_concurrency 參數的目的是,控制 InnoDB 的併發線程上限。也就是說,一旦併發線程數達到這個值,InnoDB 在接收到新請求的時候,就會進入等待狀態,直到有線程退出

通常情況下,我們建議把 innodb_thread_concurrency 設置爲 64~128 之間的值–這主要是針對併發查詢(在線程進入鎖等待以後,併發線程的計數會減一,也就是說等行鎖(也包括間隙鎖)的線程是不算在併發線程數裏面的。)

併發連接和併發查詢,並不是同一個概念。
你在 show processlist 的結果裏,看到的幾千個連接,指的就是併發連接。而“當前正在執行”的語句,纔是我們所說的併發查詢

查表判斷

一般的做法是,在系統庫(mysql 庫)裏創建一個表,比如命名爲 health_check,裏面只放一行數據,然後定期執行

mysql> select * from mysql.health_check;

但是針對binlog空間滿了情形又不能檢測出來

更新事務要寫 binlog,而一旦 binlog 所在磁盤的空間佔用率達到 100%,那麼所有的更新語句和事務提交的 commit 語句就都會被堵住。但是,系統這時候還是可以正常讀數據的

更新判斷

mysql> update mysql.health_check set t_modified=now();

這樣會有新的問題:主庫要判斷庫是否可用,備庫也需要,那麼就都需要執行上述更新語句,而雙M架構下主庫的binlog又會發送給備庫,更新同一行數據就可能出現行衝突,也就是可能會導致主備同步停止–需要多行(以server_id爲主鍵)

但有可能,機器的I/O已經100%,但剛好健康檢查的sql拿到了資源,成功返回了 – 出現了誤判,判定延遲

內部統計

MySQL 5.6 版本以後提供的 performance_schema 庫,就在 file_summary_by_event_name 表裏統計了每次 IO 請求的時間。
在這裏插入圖片描述
圖中這一行表示統計的是redo log的寫入時間,第一列EVENT_NAME 表示統計的類型。
剩下的三組分表是所有IO操作的統計,讀操作的統計以及寫操作的統計
SUM_NUMBER_OF_BYTES_READ統計的是,總共從redo log裏讀了多少個字節。
最後的第四組數據,是對其他類型數據的統計。在redo log裏,你可以認爲它們就是對fsync的統計。

建議只打開自己需要的項進行統計。你可以通過下面的方法打開或者關閉某個具體項的統計。
打開redolog的時間監控

mysql> update setup_instruments set ENABLED=‘YES’, Timed=‘YES’ where name like ‘%wait/io/file/innodb/innodb_log_file%’;

可以通過 MAX_TIMER 的值來判斷數據庫是否出問題了。比如,設定閾值,單次 IO 請求時間超過 200 毫秒屬於異常

mysql> select event_name,MAX_TIMER_WAIT FROM performance_schema.file_summary_by_event_name where event_name in (‘wait/io/file/innodb/innodb_log_file’,‘wait/io/file/sql/binlog’) and MAX_TIMER_WAIT>200* 1000000000;

發現異常後,取到你需要的信息,再通過下面這條語句把之前的統計信息清空

mysql> truncate table performance_schema.file_summary_by_event_name;

我個人比較傾向的方案,是優先考慮 update 系統表,然後再配合增加檢測 performance_schema 的信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章