在前面的第2四、25和26篇文章中,我和你介紹了 MySQL 主備複製的基礎結構,但這些都是一主一備的結構。mysql
大多數的互聯網應用場景都是讀多寫少,所以你負責的業務,在發展過程當中極可能先會遇到讀性能的問題。而在數據庫層解決讀性能問題,就要涉及到接下來兩篇文章要討論的架
構:一主多從。sql
今天這篇文章,咱們就先聊聊一主多從的切換正確性。而後,咱們在下一篇文章中再聊聊解決一主多從的查詢邏輯正確性的方法。數據庫
如圖 1 所示,就是一個基本的一主多從結構。bash
圖 1 一主多從基本結構session
圖中,虛線箭頭表示的是主備關係,也就是 A 和 A’互爲主備, 從庫 B、C、D 指向的是主庫 A。一主多從的設置,通常用於讀寫分離,主庫負責全部的寫入和一部分讀,其餘的
讀請求則由從庫分擔。架構
今天咱們要討論的就是,在一主多從架構下,主庫故障後的主備切換問題。如圖 2 所示,就是主庫發生故障,主備切換後的結果。工具
圖 2 一主多從基本結構 -- 主備切換性能
相比於一主一備的切換流程,一主多從結構在切換完成後,A’會成爲新的主庫,從庫B、C、D 也要改接到 A’。正是因爲多了從庫 B、C、D 從新指向的這個過程,因此主備
切換的複雜性也相應增長了。ui
接下來,咱們再一塊兒看看一個切換系統會怎麼完成一主多從的主備切換過程。spa
這裏,咱們須要先來回顧一個知識點。
當咱們把節點 B 設置成節點 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
這條命令有這麼 6 個參數:
MASTER_HOST、MASTER_PORT、MASTER_USER 和 MASTER_PASSWORD 四個參數,分別表明了主庫 A’的 IP、端口、用戶名和密碼。
最後兩個參數 MASTER_LOG_FILE 和 MASTER_LOG_POS 表示,要從主庫的master_log_name 文件的 master_log_pos 這個位置的日誌繼續同步。而這個位置就是咱們所說的同步位點,也就是主庫對應的文件名和日誌偏移量。
那麼,這裏就有一個問題了,節點 B 要設置成 A’的從庫,就要執行 change master 命令,就不可避免地要設置位點的這兩個參數,可是這兩個參數到底應該怎麼設置呢?
原來節點 B 是 A 的從庫,本地記錄的也是 A 的位點。可是相同的日誌,A 的位點和A’的位點是不一樣的。所以,從庫 B 要切換的時候,就須要先通過「找同步位點」這個邏輯。
我來和你分析一下看看這個位點通常是怎麼獲取到的,你就清楚其中不精確的緣由了。考慮到切換過程當中不能丟數據,因此咱們找位點的時候,老是要找一個「稍微往前」的,
而後再經過判斷跳過那些在從庫 B 上已經執行過的事務。
一、一種取同步位點的方法是這樣的:
一種取同步位點的方法是這樣的:
1. 等待新主庫 A’把中轉日誌(relay log)所有同步完成;
2. 在 A’上執行 show master status 命令,獲得當前 A’上最新的 File 和 Position;
3. 取原主庫 A 故障的時刻 T;
4. 用 mysqlbinlog 工具解析 A’的 File,獲得 T 時刻的位點。
mysqlbinlog File --stop-datetime=T --start-datetime=T
圖 3 mysqlbinlog 部分輸出結果
圖中,end_log_pos 後面的值「123」,表示的就是 A’這個實例,在 T 時刻寫入新的binlog 的位置。而後,咱們就能夠把 123 這個值做爲 $master_log_pos ,用在節點 B
的 change master 命令裏。
二、這樣方法取出值並不精確。爲何呢?
你能夠設想有這麼一種狀況,假設在 T 這個時刻,主庫 A 已經執行完成了一個 insert 語句插入了一行數據 R,而且已經將 binlog 傳給了 A’和 B,而後在傳完的瞬間主庫 A 的
主機就掉電了。
那麼,這時候系統的狀態是這樣的:
1. 在從庫 B 上,因爲同步了 binlog, R 這一行已經存在;
2. 在新主庫 A’上, R 這一行也已經存在,日誌是寫在 123 這個位置以後的;
3. 咱們在從庫 B 上執行 change master 命令,指向 A’的 File 文件的 123 位置,就會把插入 R 這一行數據的 binlog 又同步到從庫 B 去執行。
這時候,從庫 B 的同步線程就會報告 Duplicate entry ‘id_of_R’ for key‘PRIMARY’ 錯誤,提示出現了主鍵衝突,而後中止同步。
一、一種作法是,主動跳過一個事務。跳過命令的寫法是:
set global sql_slave_skip_counter=1; start slave;
由於切換過程當中,可能會不止重複執行一個事務,因此咱們須要在從庫 B 剛開始接到新主庫 A’時,持續觀察,每次碰到這些錯誤就停下來,執行一次跳過命令,直到再也不出現停
下來的狀況,以此來跳過可能涉及的全部事務。
二、另一種方式是,經過設置 slave_skip_errors 參數,直接設置跳過指定的錯誤。
另一種方式是,經過設置 slave_skip_errors 參數,直接設置跳過指定的錯誤。在執行主備切換時,有這麼兩類錯誤,是常常會遇到的:
1062 錯誤是插入數據時惟一鍵衝突;
1032 錯誤是刪除數據時找不到行。
所以,咱們能夠把 slave_skip_errors 設置爲 「1032,1062」,這樣中間碰到這兩個錯誤時就直接跳過。
這裏須要注意的是,這種直接跳過指定錯誤的方法,針對的是主備切換時,因爲找不到精確的同步位點,因此只能採用這種方法來建立從庫和新主庫的主備關係。
這個背景是,咱們很清楚在主備切換過程當中,直接跳過 1032 和 1062 這兩類錯誤是無損的,因此才能夠這麼設置 slave_skip_errors 參數。等到主備間的同步關係創建完成,並
穩定執行一段時間以後,咱們還須要把這個參數設置爲空,以避免以後真的出現了主從數據不一致,也跳過了。
經過 sql_slave_skip_counter 跳過事務和經過 slave_skip_errors 忽略錯誤的方法,雖然都最終能夠創建從庫 B 和新主庫 A’的主備關係,但這兩種操做都很複雜,並且容易出
錯。因此,MySQL 5.6 版本引入了 GTID,完全解決了這個困難。
那麼,GTID 究竟是什麼意思,又是如何解決找同步位點這個問題呢?如今,我就和你簡單介紹一下。
GTID 的全稱是 Global Transaction Identifier,也就是全局事務 ID,是一個事務在提交的時候生成的,是這個事務的惟一標識。它由兩部分組成,格式是:
GTID=server_uuid:gno
其中:
server_uuid 是一個實例第一次啓動時自動生成的,是一個全局惟一的值;gno 是一個整數,初始值是 1,每次提交事務的時候分配給這個事務,並加 1。
這裏我須要和你說明一下,在 MySQL 的官方文檔裏,GTID 格式是這麼定義的:
GTID=source_id:transaction_id
這裏的 source_id 就是 server_uuid;然後面的這個 transaction_id,我以爲容易形成誤導,因此我改爲了 gno。爲何說使用 transaction_id 容易形成誤解呢?
由於,在 MySQL 裏面咱們說 transaction_id 就是指事務 id,事務 id 是在事務執行過程當中分配的,若是這個事務回滾了,事務 id 也會遞增,而 gno 是在事務提交的時候纔會分配。
從效果上看,GTID 每每是連續的,所以咱們用 gno 來表示更容易理解。
GTID 模式的啓動也很簡單,咱們只須要在啓動一個 MySQL 實例的時候,加上參數gtid_mode=on 和 enforce_gtid_consistency=on 就能夠了。
在 GTID 模式下,每一個事務都會跟一個 GTID 一一對應。這個 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。
這樣,每一個 MySQL 實例都維護了一個 GTID 集合,用來對應「這個實例執行過的全部事務」。
這樣看上去不太容易理解,接下來我就用一個簡單的例子,來和你說明 GTID 的基本用法。
咱們在實例 X 中建立一個表 t。
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; insert into t values(1,1);
圖 4 初始化數據的 binlog
能夠看到,事務的 BEGIN 以前有一條 SET @@SESSION.GTID_NEXT 命令。這時,若是實例 X 有從庫,那麼將 CREATE TABLE 和 insert 語句的 binlog 同步過去執行的話,執
行事務以前就會先執行這兩個 SET 命令, 這樣被加入從庫的 GTID 集合的,就是圖中的這兩個 GTID。
假設,如今這個實例 X 是另一個實例 Y 的從庫,而且此時在實例 Y 上執行了下面這條插入語句:
insert into t values(1,1);
而且,這條語句在實例 Y 上的 GTID 是 「aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10」。
那麼,實例 X 做爲 Y 的從庫,就要同步這個事務過來執行,顯然會出現主鍵衝突,致使實例 X 的同步線程中止。這時,咱們應該怎麼處理呢?
處理方法就是,你能夠執行下面的這個語句序列:
set gtid_next='aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10'; begin; commit; set gtid_next=automatic; start slave;
其中,前三條語句的做用,是經過提交一個空事務,把這個 GTID 加到實例 X 的 GTID 集合中。如圖 5 所示,就是執行完這個空事務以後的 show master status 的結果。
圖 5 show master status 結果
能夠看到實例 X 的 Executed_Gtid_set 裏面,已經加入了這個 GTID。
這樣,我再執行 start slave 命令讓同步線程執行起來的時候,雖然實例 X 上仍是會繼續執行實例 Y 傳過來的事務,可是因爲「aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10」已經存
在於實例 X 的 GTID 集合中了,因此實例 X 就會直接跳過這個事務,也就不會再出現主鍵衝突的錯誤。
在上面的這個語句序列中,start slave 命令以前還有一句 set gtid_next=automatic。這句話的做用是「恢復 GTID 的默認分配行爲」,也就是說若是以後有新的事務再執行,就
仍是按照原來的分配方式,繼續分配 gno=3
如今,咱們已經理解 GTID 的概念,再一塊兒來看看基於 GTID 的主備複製的用法。在 GTID 模式下,備庫 B 要設置爲新主庫 A’的從庫的語法以下:
CHANGE MASTER TO MASTER_HOST=$host_name MASTER_PORT=$port MASTER_USER=$user_name MASTER_PASSWORD=$password master_auto_position=1
其中,master_auto_position=1 就表示這個主備關係使用的是 GTID 協議。能夠看到,
前面讓咱們頭疼不已的 MASTER_LOG_FILE 和 MASTER_LOG_POS 參數,已經不須要指定了。
咱們把如今這個時刻,實例 A’的 GTID 集合記爲 set_a,實例 B 的 GTID 集合記爲set_b。接下來,咱們就看看如今的主備切換邏輯。
一、咱們在實例 B 上執行 start slave 命令,取 binlog 的邏輯是這樣的:
1. 實例 B 指定主庫 A’,基於主備協議創建鏈接。
2. 實例 B 把 set_b 發給主庫 A’。
3. 實例 A’算出 set_a 與 set_b 的差集,也就是全部存在於 set_a,可是不存在於 set_b的 GITD 的集合,判斷 A’本地是否包含了這個差集須要的全部 binlog 事務。
a. 若是不包含,表示 A’已經把實例 B 須要的 binlog 給刪掉了,直接返回錯誤;
b. 若是確認所有包含,A’從本身的 binlog 文件裏面,找出第一個不在 set_b 的事務,發給 B;
4. 以後就從這個事務開始,日後讀文件,按順序取 binlog 發給 B 去執行。
其實,這個邏輯裏面包含了一個設計思想:在基於 GTID 的主備關係裏,系統認爲只要創建主備關係,就必須保證主庫發給備庫的日誌是完整的。所以,若是實例 B 須要的日誌已
經不存在,A’就拒絕把日誌發給 B。
這跟基於位點的主備協議不一樣。基於位點的協議,是由備庫決定的,備庫指定哪一個位點,主庫就發哪一個位點,不作日誌的完整性判斷。
二、基於上面的介紹,咱們再來看看引入 GTID 後,一主多從的切換場景下,主備切換是如何實現的。
因爲不須要找位點了,因此從庫 B、C、D 只須要分別執行 change master 命令指向實例A’便可。
其實,嚴謹地說,主備切換不是不須要找位點了,而是找位點這個工做,在實例 A’內部就已經自動完成了。但因爲這個工做是自動的,因此對 HA 系統的開發人員來講,很是友好。
以後這個系統就由新主庫 A’寫入,主庫 A’的本身生成的 binlog 中的 GTID 集合格式是:server_uuid_of_A’:1-M。
若是以前從庫 B 的 GTID 集合格式是 server_uuid_of_A:1-N, 那麼切換以後 GTID 集合的格式就變成了 server_uuid_of_A:1-N, server_uuid_of_A’:1-M。
固然,主庫 A’以前也是 A 的備庫,所以主庫 A’和從庫 B 的 GTID 集合是同樣的。這就達到了咱們預期。
接下來,我再舉個例子幫你理解 GTID。
以前在第 22 篇文章《MySQL 有哪些「飲鴆止渴」提升性能的方法?》中,我和你提到業務高峯期的慢查詢性能問題時,分析到若是是因爲索引缺失引發的性能問題,咱們能夠通
過在線加索引來解決。可是,考慮到要避免新增索引對主庫性能形成的影響,咱們能夠先在備庫加索引,而後再切換。
當時我說,在雙 M 結構下,備庫執行的 DDL 語句也會傳給主庫,爲了不傳回後對主庫形成影響,要經過 set sql_log_bin=off 關掉 binlog。
評論區有位同窗提出了一個問題:這樣操做的話,數據庫裏面是加了索引,可是 binlog並無記錄下這一個更新,是否是會致使數據和日誌不一致?
這個問題提得很是好。當時,我在留言的回覆中就引用了 GTID 來講明。今天,我再和你展開說明一下。
假設,這兩個互爲主備關係的庫仍是實例 X 和實例 Y,且當前主庫是 X,而且都打開了GTID 模式。這時的主備切換流程能夠變成下面這樣:
set GTID_NEXT="server_uuid_of_Y:gno"; begin; commit; set gtid_next=automatic; start slave;
這樣作的目的在於,既可讓實例 Y 的更新有 binlog 記錄,同時也能夠確保不會在實例X 上執行這條更新。
接下來,執行完主備切換,而後照着上述流程再執行一遍便可。
在今天這篇文章中,我先和你介紹了一主多從的主備切換流程。在這個過程當中,從庫找新主庫的位點是一個痛點。由此,咱們引出了 MySQL 5.6 版本引入的 GTID 模式,介紹了
GTID 的基本概念和用法。
能夠看到,在 GTID 模式下,一主多從切換就很是方便了。
所以,若是你使用的 MySQL 版本支持 GTID 的話,我都建議你儘可能使用 GTID 模式來作一主多從的切換。
在下一篇文章中,咱們還能看到 GTID 模式在讀寫分離場景的應用。
最後,又到了咱們的思考題時間。
你在 GTID 模式下設置主從關係的時候,從庫執行 start slave 命令後,主庫發現須要的binlog 已經被刪除掉了,致使主備建立不成功。這種狀況下,你以爲能夠怎麼處理呢?
你能夠把你的方法寫在留言區,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
上一篇文章最後,我給你留的問題是,若是主庫都是單線程壓力模式,在從庫追主庫的過程當中,binlog-transaction-dependency-tracking 應該選用什麼參數?
這個問題的答案是,應該將這個參數設置爲 WRITESET。
因爲主庫是單線程壓力模式,因此每一個事務的 commit_id 都不一樣,那麼設置爲COMMIT_ORDER 模式的話,從庫也只能單線程執行。
一樣地,因爲 WRITESET_SESSION 模式要求在備庫應用日誌的時候,同一個線程的日誌必須與主庫上執行的前後順序相同,也會致使主庫單線程壓力模式下退化成單線程複製。
因此,應該將 binlog-transaction-dependency-tracking 設置爲 WRITESET。
@慧鑫 coming 問了一個好問題,對同一行做更新的幾個事務,若是commit_id 相同,是否是在備庫並行執行的時候會致使數據不一致?這個問
題的答案是更新同一行的事務是不可能同時進入 commit 狀態的。
@老楊同志 對這個問題給出了更詳細的回答,你們能夠去看一下。