對於多數應用來說,MySQL都是作為最關鍵的資料儲存中心的,所以,如何讓MySQL提供HA服務,是我們不得不面對的一個問題。當master當機的時候,我們如何保證資料儘可能的不丟失,如何保證快速的獲知master當機並進行相應的故障轉移處理,都是需要我們好好思考的。這裡,筆者將結合這段時間做的MySQL proxy以及toolsets相關工作,說說我們現階段以及後續會在專案中採用的MySQL HA方案。
Replication
要保證MySQL資料不丟失,replication是一個很好的解決方案,而MySQL也提供了一套強大的replication機制。只是我們需要知道,為了效能考量,replication是採用的asynchronous模式,也就是寫入的資料並不會同步更新到slave上面,如果這時候master當機,我們仍然可能會面臨資料丟失的風險。
為了解決這個問題,我們可以使用semi-synchronous replication,semi-synchronous replication的原理很簡單,當master處理完一個事務,它會等待至少一個支援semi-synchronous的slave確認收到了該事件並將其寫入relay-log之後,才會返回。這樣即使master當機,最少也有一個slave獲取到了完整的資料。
但是,semi-synchronous並不是100%的保證資料不會丟失,如果master在完成事務並將其傳送給slave的時候崩潰,仍然可能造成資料丟失。只是相比於傳統的非同步複製,semi-synchronous replication能極大地提升資料安全。更為重要的是,它並不慢,MHA的作者都說他們在facebook的生產環境中使用了semi-synchronous(這裡),所以我覺得真心沒必要擔心它的效能問題,除非你的業務量級已經完全超越了facebook或者google。在這篇文章裡面已經提到,MySQL 5.7之後已經使用了Loss-Less Semi-Synchronous replication,所以丟資料的概率已經很小了。
如果真的想完全保證資料不會丟失,現階段一個比較好的辦法就是使用gelera,一個MySQL叢集解決方案,它通過同時寫三份的策略來保證資料不會丟失。筆者沒有任何使用gelera的經驗,只是知道業界已經有公司將其用於生產環境中,效能應該也不是問題。但gelera對MySQL程式碼侵入性較強,可能對某些有程式碼潔癖的同學來說不合適了:-)
我們還可以使用drbd來實現MySQL資料複製,MySQL官方文件有一篇文件有詳細介紹,但筆者並未採用這套方案,MHA的作者寫了一些採用drdb的問題,在這裡,僅供參考。
在後續的專案中,筆者會優先使用semi-synchronous replication的解決方案,如果資料真的非常重要,則會考慮使用gelera。
Monitor
前面我們說了使用replication機制來保證master當機之後儘可能的資料不丟失,但是我們不能等到master當了幾分鐘才知道出現問題了。所以一套好的監控工具是必不可少的。
當master當掉之後,monitor能快速的檢測到並做後續處理,譬如郵件通知管理員,或者通知守護程式快速進行failover。
通常,對於一個服務的監控,我們採用keepalived或者heartbeat的方式,這樣當master當機之後,我們能很方便的切換到備機上面。但他們仍然不能很即時的檢測到服務不可用。筆者的公司現階段使用的是keepalived的方式,但後續筆者更傾向於使用zookeeper來解決整個MySQL叢集的monitor以及failover。
對於任何一個MySQL例項,我們都有一個對應的agent程式,agent跟該MySQL例項放到同一臺機器上面,並且定時的對MySQL例項傳送ping命令檢測其可用性,同時該agent通過ephemeral的方式掛載到zookeeper上面。這樣,我們可以就能知道MySQL是否當機,主要有以下幾種情況:
- 機器當機,這樣MySQL以及agent都會當掉,agent與zookeeper連線自然斷開
- MySQL當掉,agent發現ping不通,主動斷開與zookeeper的連線
- Agent當掉,但MySQL未當
上面三種情況,我們都可以認為MySQL機器出現了問題,並且zookeeper能夠立即感知。agent與zookeeper斷開了連線,zookeeper觸發相應的children changed事件,監控到該事件的管控服務就可以做相應的處理。譬如如果是上面前兩種情況,管控服務就能自動進行failover,但如果是第三種,則可能不做處理,等待機器上面crontab或者supersivord等相關服務自動重啟agent。
使用zookeeper的好處在於它能很方便的對整個叢集進行監控,並能即時的獲取整個叢集的變化資訊並觸發相應的事件通知感興趣的服務,同時協調多個服務進行相關處理。而這些是keepalived或者heartbeat做不到或者做起來太麻煩的。
使用zookeeper的問題在於部署起來較為複雜,同時如果進行了failover,如何讓應用程式獲取到最新的資料庫地址也是一個比較麻煩的問題。
對於部署問題,我們要保證一個MySQL搭配一個agent,幸好這年頭有了docker,所以真心很簡單。而對於第二個資料庫地址更改的問題,其實並不是使用了zookeeper才會有的,我們可以通知應用動態更新配置資訊,VIP,或者使用proxy來解決。
雖然zookeeper的好處很多,但如果你的業務不復雜,譬如只有一個master,一個slave,zookeeper可能並不是最好的選擇,沒準keepalived就夠了。
Failover
通過monitor,我們可以很方便的進行MySQL監控,同時在MySQL當機之後通知相應的服務做failover處理,假設現在有這樣的一個MySQL叢集,a為master,b,c為其slave,當a當掉之後,我們需要做failover,那麼我們選擇b,c中的哪一個作為新的master呢?
原則很簡單,哪一個slave擁有最近最多的原master資料,就選哪一個作為新的master。我們可以通過show slave status這個命令來獲知哪一個slave擁有最新的資料。我們只需要比較兩個關鍵欄位Master_Log_File以及Read_Master_Log_Pos,這兩個值代表了slave讀取到master哪一個binlog檔案的哪一個位置,binlog的索引值越大,同時pos越大,則那一個slave就是能被提升為master。這裡我們不討論多個slave可能會被提升為master的情況。
在前面的例子中,假設b被提升為master了,我們需要將c重新指向新的master b來開始複製。我們通過CHANGE MASTER TO來重新設定c的master,但是我們怎麼知道要從b的binlog的哪一個檔案,哪一個position開始複製呢?
GTID
為了解決這一個問題,MySQL 5.6之後引入了GTID的概念,即uuid:gid,uuid為MySQL server的uuid,是全域性唯一的,而gid則是一個遞增的事務id,通過這兩個東西,我們就能唯一標示一個記錄到binlog中的事務。使用GTID,我們就能非常方便的進行failover的處理。
仍然是前面的例子,假設b此時讀取到的a最後一個GTID為3E11FA47-71CA-11E1-9E33-C80AA9429562:23,而c的為3E11FA47-71CA-11E1-9E33-C80AA9429562:15,當c指向新的master b的時候,我們通過GTID就可以知道,只要在b中的binlog中找到GTID為3E11FA47-71CA-11E1-9E33-C80AA9429562:15這個event,那麼c就可以從它的下一個event的位置開始複製了。雖然查詢binlog的方式仍然是順序查詢,稍顯低效暴力,但比起我們自己去猜測哪一個filename和position,要方便太多了。
google很早也有了一個Global Transaction ID的補丁,不過只是使用的一個遞增的整形,LedisDB就借鑑了它的思路來實現failover,只不過google貌似現在也開始逐步遷移到MariaDB上面去了。
MariaDB的GTID實現跟MySQL 5.6是不一樣的,這點其實比較麻煩,對於我的MySQL工具集go-mysql來說,意味著要寫兩套不同的程式碼來處理GTID的情況了。後續是否支援MariaDB再看情況吧。
Pseudo GTID
GTID雖然是一個好東西,但是僅限於MySQL 5.6+,當前仍然有大部分的業務使用的是5.6之前的版本,筆者的公司就是5.5的,而這些資料庫至少長時間也不會升級到5.6的。所以我們仍然需要一套好的機制來選擇master binlog的filename以及position。
最初,筆者打算研究MHA的實現,它採用的是首先複製relay log來補足缺失的event的方式,但筆者不怎麼信任relay log,同時加之MHA採用的是perl,一個讓我完全看不懂的語言,所以放棄了繼續研究。
幸運的是,筆者遇到了orchestrator這個專案,這真的是一個非常神奇的專案,它採用了一種Pseudo GTID的方式,核心程式碼就是這個
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
create database if not exists meta; drop event if exists meta.create_pseudo_gtid_view_event; delimiter ;; create event if not exists meta.create_pseudo_gtid_view_event on schedule every 10 second starts current_timestamp on completion preserve enable do begin set @pseudo_gtid := uuid(); set @_create_statement := concat('create or replace view meta.pseudo_gtid_view as select '', @pseudo_gtid, '' as pseudo_gtid_unique_val from dual'); PREPARE st FROM @_create_statement; EXECUTE st; DEALLOCATE PREPARE st; end ;; delimiter ; set global event_scheduler := 1; |
它在MySQL上面建立了一個事件,每隔10s,就將一個uuid寫入到一個view裡面,而這個是會記錄到binlog中的,雖然我們仍然不能像GTID那樣直接定位到一個event,但也能定位到一個10s的區間了,這樣我們就能在很小的一個區間裡面對比兩個MySQL的binlog了。
繼續上面的例子,假設c最後一次出現uuid的位置為s1,我們在b裡面找到該uuid,位置為s2,然後依次對比後續的event,如果不一致,則可能出現了問題,停止複製。當遍歷到c最後一個binlog event之後,我們就能得到此時b下一個event對應的filename以及position了,然後讓c指向這個位置開始複製。
使用Pseudo GTID需要slave開啟log-slave-update
的選項,考慮到GTID也必須開啟該選項,所以個人感覺完全可以接受。
在《MySQL High Availability》這本書中,作者使用了另一種GTID的做法,每次commit的時候,需要在一個表裡面記錄gtid,然後就通過這個gtid來找到對應的位置資訊,只是這種方式需要業務MySQL客戶端的支援,筆者不很喜歡,就不採用了。
後記
MySQL HA一直是一個水比較深的領域,筆者僅僅列出了一些最近研究的東西,有些相關工具會盡量在go-mysql中實現。
更新
經過一段時間的思考與研究,筆者又有了很多心得與收穫,設計的MySQL HA跟先前有了很多不一樣的地方。後來發現,自己設計的這套HA方案,跟facebook這篇文章幾乎一樣,加之最近跟facebook的人聊天聽到他們也正在大力實施,所以感覺自己方向是對了。
新的HA,我會完全擁抱GTID,比較這玩意的出現就是為了解決原先replication那一堆問題的,所以我不會考慮非GTID的低版本MySQL了。幸運的是,我們專案已經將MySQL全部升級到5.6,完全支援GTID了。
不同於fb那篇文章將mysqlbinlog改造支援semi-sync replication協議,我是將go-mysql的replication庫支援semi-sync replication協議,這樣就能實時的將MySQL的binlog同步到一臺機器上面。這可能就是我和fb方案的唯一區別了。
只同步binlog速度鐵定比原生slave要快,畢竟少了執行binlog裡面event的過程了,而另外真正的slaves,我們仍然使用最原始的同步方式,不使用semi-sync replication。然後我們通過MHA監控整個叢集以及進行故障轉移處理。
以前我總認為MHA不好理解,但其實這是一個非常強大的工具,而且真正看perl,發現也還是看的懂得。MHA已經被很多公司用於生產環境,經受了檢驗,直接使用絕對比自己寫一個要划算。所以後續我也不會考慮zookeeper,考慮自己寫agent了。
不過,雖然設想的挺美好,但這套HA方案並沒有在專案中實施,主要原因在於筆者打算近期離職,如果現在貿然實施,後續出問題了就沒人維護了。:-)