詳解Mysql 高可用方案 之 Failover mha
在分散式系統中,我們往往會考慮系統的高可用,對於無狀態程式來講,高可用實施相對簡單一些,縱向、橫向擴充套件起來相對容易,然而對於資料密集型應用,像資料庫的高可用,就不太好擴充套件。我們在考慮資料庫高可用時,主要考慮發生系統宕機意外中斷的時候,儘可能的保持資料庫的可用性,保證業務不會被影響;其次是備份庫,只讀副本節點需要與主節點保持資料實時一致,當資料庫切換後,應當保持資料的一致性,不會存在資料缺失或者資料不一致影響業務。很多分散式資料庫都把這個問題解決了,也能夠通過很靈活的方式去滿足業務需求,如同步、半同步方式、資料副本數量、主從切換、failover 等等(下面會提到),然而我們平時使用的社群官方版 mysql5.7及以前的版本 (不包括 Mysql 其他分支像 PhxSQL,Percona XtraDB Cluster,MariaDB Galera Cluster) 都在支援分散式和系統可用性這塊處理得不是很完善。針對這個系列問題,下面分析下如何解決這個問題。
在這期間發現並提交合並了一個mha線上切換master 而導致master 和slave 資料不一致的bug
何為failover
提 mha 之前,提前普及一下failover。何為failover,即當活動的服務或應用意外終止時,快速啟用冗餘或備用的伺服器、系統、硬體或者網路接替它們工作,故障轉移(failover)與交換轉移操作基本相同,只是故障轉移通常是自動完成的,沒有警告提醒手動完成,而交換轉移需要手動進行,對於要求高可用和高穩定性的伺服器、系統或者網路,系統設計者通常會設計故障轉移功能。
簡單來說就是當系統某塊服務不可用了,系統其它服務模組能夠自動的繼續提供服務,有很多設計良好的開源軟體設計都會自動包含failover,像負載均衡nginx,haproxy可以支援後端檢測,backup,當檢測到後端upstream(endpoints)異常,會自動的無縫進行切換到正常的backup上,然後像分散式的資料密集型應用也會包含failover,包括mongdb副本集,etcd/zookeeper 節點選舉,elasticsearch副本集等等,當存在部分資料節點異常,選舉資料節點為master/primary/leader,甚至訊息佇列像rabbitmq映象佇列,kafka replicas都會包含failover。
資料複製
前面提到了failover,那麼既然系統支援failover,那必須要保證能有 backup 或者是 replics 來作為新 "master"(這裡把leader/master/primary都統稱為master )繼續提供服務。前面提到了很多開源軟體設計過程中就自帶了資料同步功能,就是資料所有的插入、刪除、更新都在 master 上進行,然後資料會同步到 slave 裡,簡單來說需要保持 master-slave 資料一致。這裡同步的方式可以像mysql-bin log,mongdb optlog 通過日誌的方式實現,將 update(),delete(),insert() 等操作記錄到 log 中,然後這些語句都轉發給每個從庫,每個從庫解析並執行該SQL語句,就像從客戶端請求收到一樣,在從庫中重放該資料;也可以通過傳輸預寫式日誌(wal)的方式,日誌優先寫入到磁碟,如SSTables 和 LSM 樹實現的引擎,所以日誌都是包含所有資料庫寫入的僅追加位元組序列,可以使用完全相同的日誌在另一個節點上構建副本,將日誌寫入磁碟同時,主庫可以通過網路將其傳送給其它從節點,如etcd狀態機同步; 還有的方式是通過叢集內部的程序直接傳送需要同步的資料,如rabbitmq映象佇列。
下圖就是一個數據複製的應用場景:一個使用者有寫入操作更新到寫庫,然後其他使用者可能從從庫中讀取資料,可能資料是最新的,也可能出現從庫由於延時不是最新的,複製系統針對這種場景化分為了幾種複製方式。

示例資料圖
複製系統的一個重要細節是:複製是 同步(synchronously)還是非同步(asynchronousl)
關於複製方式 :
同步複製(synchronous) :
同步複製就是當有資料更新請求到 master 節點,需要保證該更新操作在 slave 節點上執行成功後才返回客戶端,從庫保證有與主庫完全一致的最新資料副本。如果主庫突然失效,我們可以確信這些資料仍能在從庫上找到,但是該方式也有缺點,如果從庫沒有響應(比如它已經崩潰,或者出現網路故障,或其它任何原因),主庫就無法處理寫入操作,主庫必須阻止所有寫入,並等待同步副本再次可用,這個過程資料庫是無法更新插入資料。
非同步複製(asynchronous) :
非同步複製就是當有更新資料請求到 master 節點,master 操作結束直接返回客戶端,不需要 slave 確認,slave 後臺從 master 更新資料。該方式的缺點是,如果主庫失效且不可恢復,則任何尚未複製給從庫的寫入都會丟失,這意味著即使已經向客戶端確認成功,寫入也不能保證持久(Durable)。該方式的優點是:即使所有的從庫都落後了,主庫也可以繼續處理寫入操作,服務繼續執行。
半同步複製(semi-synchronous):
半同步複製是一種中間策略,當有更新資料請求到 master 節點,需要保證該操作在某個 slave 上也執行成功才最終返回客戶端,如果某個同步的 slave 變得緩慢,則可以使一個非同步 slave 變為同步,這樣在保證一定資料一致性的前提下也能保證可靠性(這裡可能會導致資料不一致,還可能產生資料延時)。
mysql的半同步複製(mysql semi-synchronous):
master在執行完更新操作後立即向 slave 複製資料,slave 接收到資料並寫到 relay log (無需執行)後才向 master 返回成功資訊,master 必須在接受到 slave 的成功資訊後再向客戶端返回響應,僅在資料複製發生異常(slave 節點不可用或者資料複製所用網路發生異常)的情況下,master 才會暫停(mysql 預設約 10 秒左右) 對客戶端的響應,將複製方式降級為非同步複製。當資料複製恢復正常,複製方式將恢復為半同步複製。
mysql 的資料同步和 failover
mysql 支援相對嚴格的 ACID,是一個性能和穩定性都非常不錯的關係型資料庫,但是對分散式支援不是很友好,雖然它實現了NDB,不過感覺使用不太廣泛,國內使用較多的還是基礎的主從複製方式。mysql支援前面提到的各種資料複製方式,所以只需要針對各種的場景選取對應的複製方式即可。像對可用性要求較高,資料一致性要求較低的可以選取 非同步複製; 對資料一致性要求很高的場景,金融場景可以選用 強同步複製; 網際網路場景對可用性和一致性可能都有一定要求,但要求不是特別高,可以選擇 半同步複製。 下面簡述 mysql 主從同步的邏輯
首先開啟mysql master上的 binlog, mysql slave上通過一個 I/O 執行緒從 mysql master上讀取binlog,然後傳輸到 mysql slave 的中繼日誌中,接著mysql slave 的 sql 執行緒從中繼日誌中讀取中繼日誌,應用到mysql slave的 資料庫中,這樣就實現了主從資料同步功能。

mysql主從同步邏輯
不過 mysql 自身沒有實現 failover,所以當 master 異常的時候,需要制定策略去實現 failover 並處理資料庫切換。failover 的邏輯是當 master 異常,slave 自動提升為主庫的,然後 讓其他從庫知道新的 master,並繼續從新的 master同步資料。 在這裡我們就要用到 mha 了,一個mysql 高可用管理工具。mha 能做到在 0~30 秒之內自動完成資料庫的故障切換操作,並且在進行故障切換的過程中,能在最大程度上保證資料的一致性,以達到真正意義上的高可用。關於 mha 的各種細節我這裡就不詳細展開了,這些內容都在官方 wike 中(文件真的非常詳細,作者考慮很多通用的場景,很多引數可配置化)。
連結:
等等等....
這裡只分析他實現 failover 的架構與原理,結構如下(官網的圖片,略模糊)

mha架構圖
mha 由兩部分組成:
mha manager(管理節點): 單獨部署在一臺獨立的機器上管理多個 master-slave 叢集(最好和mysql相關伺服器管理),也可以部署在一臺 slave 節點上,作用是多mysql server服務的管理,master檢測,master選舉,連線檢查,master故障切換等工作。
mha node(資料節點): 執行在每臺 mysql 伺服器上,作用是拷貝 master 二進位制日誌;然後擁有最新資料的slave上生成差異中繼日誌,應用差異日誌;最後在不停止SQL執行緒的情況下刪除中繼日誌。
原理:
(1)從宕機崩潰的master儲存二進位制日誌事件(binlog events);
(2)識別含有最新更新的slave;
(3)應用差異的中繼日誌(relay log)到其他的slave;
(4)應用從master儲存的二進位制日誌事件(binlog events);
(5)使其他的slave連線新的master進行復制;
(6)使其他的slave連線新的master進行復制;
mha需要解決的問題 :
如何確定新的master :
由於 mysql 沒有像 elasticsearch, etcd 這樣分散式的叢集決策節點,所以這裡的 master 選舉節點就是 mha manager節點,mha 主要參考幾個因素:1 配置檔案中手動配置的候選servers引數 **candidate_master=1**(如: 希望同機房,或則同機架的機器優先為 master), 2 依據各個 slave 中最新的二進位制檔案,最新的slave節點即可提升為master。

選取新的master
如何 保證資料一致性:
mha 最大程度的保證資料不丟失,當 mysql master 異常時,但是機器正常提供服務,那麼 mha 會去對比 master 節點與將成為 master 節點的 slave 節點的資料差,然後將差值資料拷貝到新的 slave,然後應用這部分資料差去補全資料。

diff master
如果是 mysql master 異常,機器也異常,那麼系統中儲存的二進位制 binlog 檔案也無法訪問,這就沒法拷貝,那麼會略過拷貝流程,直接會從 salve 候選者中選取新的 master。如果使用 mysql 5.5 的半同步複製,可以大大降低資料丟失的風險。

拷貝bin-log
節點之間資料如何拷貝:
由於 mysql 內部沒有做這樣的 bin-log 拷貝功能,所以我們有自定義的需求去實現複製。這裡 mha 需要依賴 ssh 協議,就是通過 scp 協議傳輸檔案(搭建 mha 需要保證各個主機間可以 ssh 互通)。

拷貝bin-log
其他 slave 節點如何知道新的 master:
當候選 master 提升為 master 後,mha manager 會用 mysql change replication 的方式更改目前叢集的所有 slave的同步源。

slave更新master
管理節點如何解決網路分割槽問題 :
在上邊的網路結構中,我們可以猜到系統可能存在一個很大問題,就是網路分割槽。網路分割槽指的是由於網路分離造成系統分裂為兩個叢集,各自相互不信任。對應無狀態的系統,幾乎沒有影響,正常處理請求,比如nginx;資料系統出現分割槽問題時,如果系統設計或者配置存在不合理就會導致資料不一致,而這個問題修復起來會非常複雜,所以 elasticsearch , etcd, mongdb 等天生支援分散式的資料系統中,都有機制避免由於網路分割槽導致的資料不一致問題,解決方式是讓 叢集大多數能正常通訊的節點正常服務。 比如你的叢集是有 5 個節點,分割槽導致一個分割槽 2 個節點,一個分割槽 3 個,那麼 2 個節點的分割槽就會被認為是異常的,不能正常提供服務,這裡也會有一些特定的演算法可以解決類似的問題,如raft。
比如下邊這個圖,3個節點,那麼最少信任叢集的節點數應該有2個,所以C節點會被標記為異常,不會正常提供服務
mha 的網路分割槽和上邊提到的有點不同,由於叢集只有一個 mha management(注意這裡只能部署一個management,不允許部署多個,否則會出現異常),所以 mha management 不存在腦裂問題,這裡指的網路分割槽指的 mha management 節點與 mysql master 節點出現分割槽,如下:

mha management與mysql master發生網路分割槽
當 mha management 和 mysql master 出現在兩個分割槽,mha 認為 mysql master 異常,但是實際 mysql master 和 mysql slave都正常工作,提供服務,但是這時候 mha 還是會切換 master,可能對應用程式來說(如果前端有負載均衡器),會出現2個master,而導致資料不一致。 面對這種情況,mha 提供了一種二次檢測的方式,既多條鏈路檢測,1 條鏈路是 mha management 是通過直接檢測 mysql master節點, 2 其他鏈路是 mha managerment 通過ssh登入到其他 slave 的方式去檢測 mysql master 是否正常,這樣就能夠解決 mha managerment 和 mysql master 的網路分割槽問題,防止誤切換。
客戶端應用自動恢復
一般來說自帶 failover 的分散式系統系統都能夠自己恢復服務,像elasticsearch , etcd, 他們客戶端和叢集都能夠自動感知叢集節點的變化,客戶端連線的是一組叢集地址,看下邊示例,像 etcd 連線的服務端地址 Endpoints []string 是個陣列,這就保證了當某個節點 ip 失效,或者機器不能訪問,機器異常,機器負載的情況加, 都有其他節點進行處理,etcd連線如下:
cfg := client.Config {
Endpoints: []string{" http://127.0.0.1:2379 "},
Transport: client.DefaultTransport, // set timeout per request to fail fast when the target endpoint is unavailable
HeaderTimeoutPerRequest: time.Second,
}
然而像 mysql 預設的連線方式,應用 tomcat 或其他 client 連線資料庫的預設的方式是mysql 驅動,就沒法連線一個數組。所以我們的解決方案是要減少客戶端感知,減少邏輯變更,讓客戶端和原來一樣只需要連線一個 ip就好,這裡的 ip是 proxy ip, 這裡會有多種方式(這裡不考慮分片和其他高階的路由,只考慮對應用連線,proxy的高可用方式可以通過keepalived來配合做互備)
通過代理的方式對客戶端體驗最好,原理上是 proxy 解析了mysql協議,然後根據不同的庫,表,請求型別路由(讀寫分離)到後端合適的 mysql 伺服器,但是因為加了這樣一個 7 層的proxy解析, 所以效能會損失,一般在 20% 左右,上邊的各個 proxy 會有各自的優勢,和功能,詳情可以去看看相關對比,我們需要做的就是在 proxy 後端配置我們的服務應用組,配置讀寫分離,當 master異常,能夠切換。
4層 proxy 就不會解析出 mysql 7層 協議,只能解析4層,所以保證 mysql 後端埠通就可以了,就是檢測到後端master 不可用的時候,切換到 backup master,由於是 4層協議 所以不能配置自動讀寫分離,只能單獨配置 master 埠,slave 埠了(如果配置keepalived可以自定義有指令碼可以進行切換,自定義指令碼可以配置主從同步延時)
- 直接使用vip
- 指令碼配置vip
- keepalived配置vip
最後這個方式邏輯就是:
手動配置vip: 在 master 機器上配置虛擬vip,當mysql master異常,mha management 通過配置的 master_ip_online_change_script 的指令碼,當 master 異常的時候,利用該指令碼在新的 slave 上啟動新的 ip,這樣對客戶端來說也是無影響的,配置vip比較容易
關於配置流程[配置流程]( www.cnblogs.com/gomysql/p/3… ),這裡說的很清楚了,我就不詳細解析了。
我們生產環境實際上是使用maxscale,利用它來進行讀寫分離,他的 文件 特別全面,我們選用他的原因是他穩定高效,能無縫配合 mha,不需要 mha 配置任何 ip 切換之類的邏輯,當 mha 進行切換後,maxscale 會自動的進行感知系統中 servres 的角色,master切換它能感知到,對應用是完全無影響,如下圖:

自動識別roke
總結:
這裡解決的是 mysql 原官方社群版的高可用問題,利用 mha + maxscale 的方式,該方案能以最小的代價對現有系統進行變更,提高系統的可用性和穩定性。前面提到以前版本(5.7以前) mysql 對叢集化支援相對較弱,但是其實 mysql 也一直在發展,社群也開發出了很多方案,像PhxSQL,Percona XtraDB Cluster,MariaDB Galera Cluster,mysql 官方也開發出了使用 MySQL Group Replication的GA,來使用分散式協議來解決資料一致性問題了,非常期待未來越來越多的解決方案被提出,來更好的解決mysql高可用問題。