正常狀況下,只要主庫執行更新生成的全部 binlog,均可以傳到備庫並被正確地執行,備庫就能達到跟主庫一致的狀態,這就是最終一致性。
mysql
主備切換多是一個主動運維動做,好比軟件升級、主庫所在機器按計劃下線等,也多是被動操做,好比主庫所在機器掉電。與數據同步有關的時間點主要包括如下三個:sql
所謂主備延遲,就是同一個事務,在備庫執行完成的時間和主庫執行完成的時間之間的差值,也就是 T3-T1。
能夠在備庫上執行show slave status
命令,它的返回結果裏面會顯示seconds_behind_master
,用於表示當前備庫延遲了多少秒。seconds_behind_master 的計算方法是這樣的:數據庫
不會的。由於,備庫鏈接到主庫的時候,會經過執行 SELECT UNIX_TIMESTAMP() 函數來得到當前主庫的系統時間。若是這時候發現主庫的系統時間與本身不一致,備庫在執行seconds_behind_master
計算的時候會自動扣掉這個差值。網絡
須要說明的是,在網絡正常的時候,日誌從主庫傳給備庫所需的時間是很短的,即 T2-T1的值是很是小的。也就是說,網絡正常狀況下,主備延遲的主要來源是備庫接收完 binlog和執行完這個事務之間的時間差。因此說,主備延遲最直接的表現是,備庫消費中轉日誌(relay log)的速度,比主庫生產binlog 的速度要慢。session
通常的想法是,主庫既然提供了寫能力,那麼備庫能夠提供一些讀能力。或者一些運營後臺須要的分析語句,不能影響正常業務,因此只能在備庫上跑。
這種狀況,咱們通常能夠這麼處理:多線程
其中,一主多從的方式大都會被採用。由於做爲數據庫系統,還必須保證有按期全量備份的能力。而從庫,就很適合用來作備份。從庫和備庫在概念上其實差很少。併發
由於主庫上必須等事務執行完成纔會寫入 binlog,再傳給備庫。因此,若是一個主庫上的語句執行 10 分鐘,那這個事務極可能就會致使從庫延遲 10分鐘。不要一次性地用 delete 語句刪除太多數據。其實,這就是一個典型的大事務場景。運維
好比,一些歸檔類的數據,平時沒有注意刪除歷史數據,等到空間快滿了,業務開發人員要一次性地刪掉大量歷史數據。同時,又由於要避免在高峯期操做會影響業務(至少有這個意識仍是很不錯的),因此會在晚上執行這些大量數據的刪除操做。函數
另外一種典型的大事務場景,就是大表 DDL。工具
在圖 1 的雙 M 結構下,從狀態 1 到狀態 2 切換的詳細過程是這樣的:
這個切換流程,通常是由專門的 HA 系統來完成的,咱們暫時稱之爲可靠性優先流程。
備註:圖中的 SBM,是 seconds_behind_master 參數的簡寫。
能夠看到,這個切換流程中是有不可用時間的。由於在步驟 2 以後,主庫 A 和備庫 B 都處於 readonly 狀態,也就是說這時系統處於不可寫狀態,直到步驟 5 完成後才能恢復。在這個不可用狀態中,比較耗費時間的是步驟 3,可能須要耗費好幾秒的時間。這也是爲何須要在步驟 1 先作判斷,確保 seconds_behind_master
的值足夠小。
試想若是一開始主備延遲就長達 30 分鐘,而不先作判斷直接切換的話,系統的不可用時間就會長達 30 分鐘,這種狀況通常業務都是不可接受的。固然,系統的不可用時間,是由這個數據可靠性優先的策略決定的。你也能夠選擇可用性優先的策略,來把這個不可用時間幾乎降爲 0。
若是我強行把步驟 四、5 調整到最開始執行,也就是說不等主備數據同步,直接把鏈接切到備庫 B,而且讓備庫 B 能夠讀寫,那麼系統幾乎就沒有不可用時間了。咱們把這個切換流程,暫時稱做可用性優先流程。這個切換流程的代價,就是可能出現數據不一致的狀況。
insert into t(c) values(4); insert into t(c) values(5);
假設,如今主庫上其餘的數據表有大量的更新,致使主備延遲達到 5 秒。在插入一條 c=4的語句後,發起了主備切換。
下圖是可用性優先策略,且binlog_format=mixed時的切換流程和數據結果。
如今,咱們一塊兒分析下這個切換流程:
最後的結果就是,主庫 A 和備庫 B 上出現了兩行不一致的數據。能夠看到,這個數據不一致,是由可用性優先流程致使的。
可用性優先策略,但設置 binlog_format=row
由於 row 格式在記錄 binlog 的時候,會記錄新插入的行的全部字段值,因此最後只會有一行不一致。並且,兩邊的主備同步的應用線程會報錯 duplicate key error 並中止。也就是說,這種狀況下,備庫 B 的 (5,4) 和主庫 A 的 (5,5) 這兩行數據,都不會被對方執行。
從上面的分析中,你能夠看到一些結論:
有一個庫的做用是記錄操做日誌。這時候,若是數據不一致能夠經過 binlog 來修補,而這個短暫的不一致也不會引起業務問題。同時,業務系統依賴於這個日誌寫入邏輯,若是這個庫不可寫,會致使線上的業務操做沒法執行。
這時候,你可能就須要選擇先強行切換,過後再補數據的策略。固然,過後覆盤的時候,咱們想到了一個改進措施就是,讓業務邏輯不要依賴於這類日誌的寫入。也就是說,日誌寫入這個邏輯模塊應該能夠降級,好比寫到本地文件,或者寫到另一個臨時庫裏面。
假設,主庫 A 和備庫 B 間的主備延遲是 30 分鐘,這時候主庫 A 掉電了,HA 系統要切換B 做爲主庫。咱們在主動切換的時候,能夠等到主備延遲小於 5 秒的時候再啓動切換,但這時候已經別無選擇了。
採用可靠性優先策略的話,你就必須得等到備庫 B 的 seconds_behind_master=0 以後,才能切換。但如今的狀況比剛剛更嚴重,並非系統只讀、不可寫的問題了,而是系統處於徹底不可用的狀態。由於,主庫 A 掉電後,咱們的鏈接尚未切到備庫 B。
能不能直接切換到備庫 B,可是保持 B 只讀呢?這樣也不行。由於,這段時間內,中轉日誌尚未應用完成,若是直接發起主備切換,客戶端查詢看不到以前執行完成的事務,會認爲有「數據丟失」。雖然隨着中轉日誌的繼續應用,這些數據會恢復回來,可是對於一些業務來講,查詢到「暫時丟失數據的狀態」也是不能被接受的。
在知足數據可靠性的前提下,MySQL 高可用系統的可用性,是依賴於主備延遲的。延遲的時間越小,在主庫故障的時候,服務恢復須要的時間就越短,可用性就越高。
談到主備的並行複製能力,咱們要關注的是圖中黑色的兩個箭頭。一個箭頭表明了客戶端寫入主庫,另外一箭頭表明的是備庫上 sql_threa
執行中轉日誌(relay log)。若是用箭頭的粗細來表明並行度的話,那麼真實狀況就如圖 1 所示,第一個箭頭要明顯粗於第二個箭頭。
在主庫上,影響併發度的緣由就是各類鎖了。因爲 InnoDB 引擎支持行鎖,除了全部併發事務都在更新同一行(熱點行)這種極端場景外,它對業務併發度的支持仍是很友好的。因此,你在性能測試的時候會發現,併發壓測線程 32 就比單線程時,整體吞吐量高。
而日誌在備庫上的執行,就是圖中備庫上 sql_thread 更新數據 (DATA) 的邏輯。若是是用單線程的話,就會致使備庫應用日誌不夠快,形成主備延遲。
coordinator 就是原來的 sql_thread, 不過如今它再也不直接更新數據了,只負責讀取中轉日誌和分發事務。真正更新日誌的,變成了 worker 線程。而 work 線程的個數,就是由參數 slave_parallel_workers 決定的。根據個人經驗,把這個值設置爲 8~16 之間最好(32 核物理機的狀況),畢竟備庫還有可能要提供讀查詢,不能把 CPU 都吃光了。
事務能不能按照輪詢的方式分發給各個 worker,也就是第一個事務分給 worker_1,第二個事務發給 worker_2 呢?
實際上是不行的。由於,事務被分發給 worker 之後,不一樣的 worker 就獨立執行了。可是,因爲 CPU 的調度策略,極可能第二個事務最終比第一個事務先執行。而若是這時候恰好這兩個事務更新的是同一行,也就意味着,同一行上的兩個事務,在主庫和備庫上的執行順序相反,會致使主備不一致的問題。
同一個事務的多個更新語句,能不能分給不一樣的worker 來執行呢?
也不行。舉個例子,一個事務更新了表 t1 和表 t2 中的各一行,若是這兩條更新語句被分到不一樣 worker 的話,雖然最終的結果是主備一致的,但若是表 t1 執行完成的瞬間,備庫上有一個查詢,就會看到這個事務「更新了一半的結果」,破壞了事務邏輯的隔離性。
因此,coordinator 在分發的時候,須要知足如下這兩個基本要求:
官方 MySQL5.6 版本,支持了並行複製,只是支持的粒度是按庫並行。理解了上面介紹的按表分發策略和按行分發策略,你就理解了,用於決定分發策略的 hash 表裏,key 就是數據庫名。
這個策略的並行效果,取決於壓力模型。若是在主庫上有多個 DB,而且各個 DB 的壓力均衡,使用這個策略的效果會很好。相比於按表和按行分發,這個策略有兩個優點:
可是,若是你的主庫上的表都放在同一個 DB 裏面,這個策略就沒有效果了;或者若是不一樣DB 的熱點不一樣,好比一個是業務邏輯庫,一個是系統配置庫,那也起不到並行的效果。理論上你能夠建立不一樣的 DB,把相同熱度的表均勻分到這些不一樣的 DB 中,強行使用這個策略。不過據我所知,因爲須要特意移動數據,這個策略用得並很少。
由參數slave-parallel-type
來控制並行複製策略:
同時處於「執行狀態」的全部事務,是否是能夠並行?不能。由於,這裏面可能有因爲鎖衝突而處於鎖等待狀態的事務。若是這些事務在備庫上被分配到不一樣的 worker,就會出現備庫跟主庫不一致的狀況。
兩階段提交細化過程圖。
其實,不用等到 commit 階段,只要可以到達 redo log prepare 階段,就表示事務已經經過鎖衝突的檢驗了。所以,MySQL 5.7 並行複製策略的思想是:
binlog 的組提交,有兩個參數:
這兩個參數是用於故意拉長 binlog 從 write 到 fsync 的時間,以此減小 binlog 的寫盤次數。在 MySQL 5.7 的並行複製策略裏,它們能夠用來製造更多的「同時處於 prepare 階段的事務」。這樣就增長了備庫複製的並行度。也就是說,這兩個參數,既能夠「故意」讓主庫提交得慢些,又可讓備庫執行得快些。在MySQL 5.7 處理備庫延遲的時候,能夠考慮調整這兩個參數值,來達到提高備庫複製併發度的目的。
MySQL 增長了一個新的並行複製策略,基於 WRITESET 的並行複製。
相應地,新增了一個參數 binlog-transaction-dependency-tracking
,用來控制是否啓用這個新策略。這個參數的可選值有如下三種。
固然爲了惟一標識,這個 hash 值是經過「庫名 + 表名 + 索引名 + 值」計算出來的。若是一個表上除了有主鍵索引外,還有其餘惟一索引,那麼對於每一個惟一索引,insert 語句對應的 writeset 就要多增長一個 hash 值。你可能看出來了,這跟咱們前面介紹的基於 MySQL 5.5 版本的按行分發的策略是差很少的。不過,MySQL 官方的這個實現仍是有很大的優點:
所以,MySQL 5.7.22 的並行複製策略在通用性上仍是有保證的。固然,對於「表上沒主鍵」和「外鍵約束」的場景,WRITESET 策略也是無法並行的,也會暫時退化爲單線程模型。
以下圖所示,就是一個基本的一主多從結構。
圖中,虛線箭頭表示的是主備關係,也就是 A 和 A’互爲主備, 從庫 B、C、D 指向的是主庫 A。一主多從的設置,通常用於讀寫分離,主庫負責全部的寫入和一部分讀,其餘的讀請求則由從庫分擔。
以下圖所示,就是主庫發生故障,主備切換後的結果。
相比於一主一備的切換流程,一主多從結構在切換完成後,A’會成爲新的主庫,從庫 B、C、D 也要改接到 A’。正是因爲多了從庫 B、C、D 從新指向的這個過程,因此主備切換的複雜性也相應增長了。
當咱們把節點 B 設置成節點 A’的從庫的時候,須要執行一條 change master 命令:
CHANGE MASTER TO MASTER_HOST=$host_name MASTER_PORT=$por MASTER_USER=$user_name MASTER_PASSWORD=$password MASTER_LOG_FILE=$master_log_name MASTER_LOG_POS=$master_log_po
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 上已經執行過的事務。一種取同步位點的方法是這樣的:
mysqlbinlog File --stop-datetime=T --start-datetime=T
圖中,end_log_pos 後面的值「123」,表示的就是 A’這個實例,在 T 時刻寫入新的binlog 的位置。而後,咱們就能夠把 123 這個值做爲 $master_log_pos ,用在節點 B 的change master 命令裏。
固然這個值並不精確。爲何呢?你能夠設想有這麼一種狀況,假設在 T 這個時刻,主庫 A 已經執行完成了一個 insert 語句插入了一行數據 R,而且已經將 binlog 傳給了 A’和 B,而後在傳完的瞬間主庫 A 的主機就掉電了。那麼,這時候系統的狀態是這樣的:
這時候,從庫 B 的同步線程就會報告 Duplicate entry ‘id_of_R’ for key ‘PRIMARY’錯誤,提示出現了主鍵衝突,而後中止同步。因此,一般狀況下,咱們在切換任務的時候,要先主動跳過這些錯誤,有兩種經常使用的方法。
一種作法是,主動跳過一個事務。跳過命令的寫法是:set global sql_slave_skip_counter=1; start slave;
由於切換過程當中,可能會不止重複執行一個事務,因此咱們須要在從庫 B 剛開始接到新主庫 A’時,持續觀察,每次碰到這些錯誤就停下來,執行一次跳過命令,直到再也不出現停下來的狀況,以此來跳過可能涉及的全部事務。
另一種方式是,經過設置 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 的全稱是 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 來表示更容易理解。
TID 模式的啓動也很簡單,咱們只須要在啓動一個 MySQL 實例的時候,加上參數gtid_mode=on 和 enforce_gtid_consistency=on 就能夠了。在 GTID 模式下,每一個事務都會跟一個 GTID 一一對應。這個 GTID 有兩種生成方式,而使用哪一種方式取決於 session 變量 gtid_next 的值。
注意,一個 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);
能夠看到,事務的 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 的結果。
能夠看到實例 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 模式下,備庫 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 的邏輯是這樣的:
其實,這個邏輯裏面包含了一個設計思想:在基於 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 集合是同樣的。這就達到了咱們預期。
業務高峯期的慢查詢性能問題時,分析到若是是因爲索引缺失引發的性能問題,咱們能夠經過在線加索引來解決。可是,考慮到要避免新增索引對主庫性能形成的影響,咱們能夠先在備庫加索引,而後再切換。
在雙 M 結構下,備庫執行的 DDL 語句也會傳給主庫,爲了不傳回後對主庫形成影響,要經過 set sql_log_bin=off 關掉 binlog。
一個問題:這樣操做的話,數據庫裏面是加了索引,可是 binlog 並無記錄下這一個更新,是否是會致使數據和日誌不一致?
假設,這兩個互爲主備關係的庫仍是實例 X 和實例 Y,且當前主庫是 X,而且都打開了GTID 模式。這時的主備切換流程能夠變成下面這樣:在實例 X 上執行 stop slave。
在實例 Y 上執行 DDL 語句。
執行完成後,查出這個 DDL 語句對應的 GTID,並記爲 server_uuid_of_Y:gno。到實例 X 上執行如下語句序列:
set GTID_NEXT="server_uuid_of_Y:gno"; begin; commit; set gtid_next=automatic; start slave;
這樣作的目的在於,既可讓實例 Y 的更新有 binlog 記錄,同時也能夠確保不會在實例 X上執行這條更新。接下來,執行完主備切換,而後照着上述流程再執行一遍便可。