MySQL高可用淺析:MySQL HA方案
對于多數(shù)應(yīng)用來說,MySQL都是作為最關(guān)鍵的數(shù)據(jù)存儲中心的,所以,如何讓MySQL提供HA服務(wù),是我們不得不面對的一個問題。當(dāng)master當(dāng)機的時候,我們?nèi)绾伪WC數(shù)據(jù)盡可能的不丟失,如何保證快速的獲知master當(dāng)機并進(jìn)行相應(yīng)的故障轉(zhuǎn)移處理,都是需要我們好好思考的。這里,筆者將結(jié)合這段時間做的MySQL proxy以及toolsets相關(guān)工作,說說我們現(xiàn)階段以及后續(xù)會在項目中采用的MySQL HA方案。
(題圖來自:comprendrechoisir.com)
Replication
要保證MySQL數(shù)據(jù)不丟失,replication是一個很好的解決方案,而MySQL也提供了一套強大的replication機制。只是我們需要知道,為了性能考量,replication是采用的asynchronous模式,也就是寫入的數(shù)據(jù)并不會同步更新到slave上面,如果這時候master當(dāng)機,我們?nèi)匀豢赡軙媾R數(shù)據(jù)丟失的風(fēng)險。
為了解決這個問題,我們可以使用semi-synchronous replication,semi-synchronous replication的原理很簡單,當(dāng)master處理完一個事務(wù),它會等待至少一個支持semi-synchronous的slave確認(rèn)收到了該事件并將其寫入relay-log之后,才會返回。這樣即使master當(dāng)機,最少也有一個slave獲取到了完整的數(shù)據(jù)。
但是,semi-synchronous并不是100%的保證數(shù)據(jù)不會丟失,如果master在完成事務(wù)并將其發(fā)送給slave的時候崩潰,仍然可能造成數(shù)據(jù)丟失。只是相比于傳統(tǒng)的異步復(fù)制,semi-synchronous replication能極大地提升數(shù)據(jù)安全。更為重要的是,它并不慢,MHA的作者都說他們在facebook的生產(chǎn)環(huán)境中使用了semi-synchronous(這里),所以我覺得真心沒必要擔(dān)心它的性能問題,除非你的業(yè)務(wù)量級已經(jīng)完全超越了facebook或者google。在這篇文章里面已經(jīng)提到,MySQL 5.7之后已經(jīng)使用了Loss-Less Semi-Synchronous replication,所以丟數(shù)據(jù)的概率已經(jīng)很小了。
如果真的想完全保證數(shù)據(jù)不會丟失,現(xiàn)階段一個比較好的辦法就是使用gelera,一個MySQL集群解決方案,它通過同時寫三份的策略來保證數(shù)據(jù)不會丟失。筆者沒有任何使用gelera的經(jīng)驗,只是知道業(yè)界已經(jīng)有公司將其用于生產(chǎn)環(huán)境中,性能應(yīng)該也不是問題。但gelera對MySQL代碼侵入性較強,可能對某些有代碼潔癖的同學(xué)來說不合適了:-)
我們還可以使用drbd來實現(xiàn)MySQL數(shù)據(jù)復(fù)制,MySQL官方文檔有一篇文檔有詳細(xì)介紹,但筆者并未采用這套方案,MHA的作者寫了一些采用drdb的問題,在這里,僅供參考。
在后續(xù)的項目中,筆者會優(yōu)先使用semi-synchronous replication的解決方案,如果數(shù)據(jù)真的非常重要,則會考慮使用gelera。
Monitor
前面我們說了使用replication機制來保證master當(dāng)機之后盡可能的數(shù)據(jù)不丟失,但是我們不能等到master當(dāng)了幾分鐘才知道出現(xiàn)問題了。所以一套好的監(jiān)控工具是必不可少的。
當(dāng)master當(dāng)?shù)糁螅琺onitor能快速的檢測到并做后續(xù)處理,譬如郵件通知管理員,或者通知守護程序快速進(jìn)行failover。
通常,對于一個服務(wù)的監(jiān)控,我們采用keepalived或者h(yuǎn)eartbeat的方式,這樣當(dāng)master當(dāng)機之后,我們能很方便的切換到備機上面。但他們?nèi)匀徊荒芎芗磿r的檢測到服務(wù)不可用。筆者的公司現(xiàn)階段使用的是keepalived的方式,但后續(xù)筆者更傾向于使用zookeeper來解決整個MySQL集群的monitor以及failover。
對于任何一個MySQL實例,我們都有一個對應(yīng)的agent程序,agent跟該MySQL實例放到同一臺機器上面,并且定時的對MySQL實例發(fā)送ping命令檢測其可用性,同時該agent通過ephemeral的方式掛載到zookeeper上面。這樣,我們可以就能知道MySQL是否當(dāng)機,主要有以下幾種情況:
- 機器當(dāng)機,這樣MySQL以及agent都會當(dāng)?shù)?,agent與zookeeper連接自然斷開
- MySQL當(dāng)?shù)簦琣gent發(fā)現(xiàn)ping不通,主動斷開與zookeeper的連接
- Agent當(dāng)?shù)?,但MySQL未當(dāng)
上面三種情況,我們都可以認(rèn)為MySQL機器出現(xiàn)了問題,并且zookeeper能夠立即感知。agent與zookeeper斷開了連接,zookeeper觸發(fā)相應(yīng)的children changed事件,監(jiān)控到該事件的管控服務(wù)就可以做相應(yīng)的處理。譬如如果是上面前兩種情況,管控服務(wù)就能自動進(jìn)行failover,但如果是第三種,則可能不做處理,等待機器上面crontab或者supersivord等相關(guān)服務(wù)自動重啟agent。
使用zookeeper的好處在于它能很方便的對整個集群進(jìn)行監(jiān)控,并能即時的獲取整個集群的變化信息并觸發(fā)相應(yīng)的事件通知感興趣的服務(wù),同時協(xié)調(diào)多個服務(wù)進(jìn)行相關(guān)處理。而這些是keepalived或者h(yuǎn)eartbeat做不到或者做起來太麻煩的。
使用zookeeper的問題在于部署起來較為復(fù)雜,同時如果進(jìn)行了failover,如何讓應(yīng)用程序獲取到最新的數(shù)據(jù)庫地址也是一個比較麻煩的問題。
對于部署問題,我們要保證一個MySQL搭配一個agent,幸好這年頭有了docker,所以真心很簡單。而對于第二個數(shù)據(jù)庫地址更改的問題,其實并不是使用了zookeeper才會有的,我們可以通知應(yīng)用動態(tài)更新配置信息,VIP,或者使用proxy來解決。
雖然zookeeper的好處很多,但如果你的業(yè)務(wù)不復(fù)雜,譬如只有一個master,一個slave,zookeeper可能并不是最好的選擇,沒準(zhǔn)keepalived就夠了。
Failover
通過monitor,我們可以很方便的進(jìn)行MySQL監(jiān)控,同時在MySQL當(dāng)機之后通知相應(yīng)的服務(wù)做failover處理,假設(shè)現(xiàn)在有這樣的一個MySQL集群,a為master,b,c為其slave,當(dāng)a當(dāng)?shù)糁?,我們需要做failover,那么我們選擇b,c中的哪一個作為新的master呢?
原則很簡單,哪一個slave擁有最近最多的原master數(shù)據(jù),就選哪一個作為新的master。我們可以通過show slave status
這個命令來獲知哪一個slave擁有最新的數(shù)據(jù)。我們只需要比較兩個關(guān)鍵字段Master_Log_File
以及Read_Master_Log_Pos
,這兩個值代表了slave讀取到master哪一個binlog文件的哪一個位置,binlog的索引值越大,同時pos越大,則那一個slave就是能被提升為master。這里我們不討論多個slave可能會被提升為master的情況。
在前面的例子中,假設(shè)b被提升為master了,我們需要將c重新指向新的master b來開始復(fù)制。我們通過CHANGE MASTER TO
來重新設(shè)置c的master,但是我們怎么知道要從b的binlog的哪一個文件,哪一個position開始復(fù)制呢?
GTID
為了解決這一個問題,MySQL 5.6之后引入了GTID的概念,即uuid:gid,uuid為MySQL server的uuid,是全局唯一的,而gid則是一個遞增的事務(wù)id,通過這兩個東西,我們就能唯一標(biāo)示一個記錄到binlog中的事務(wù)。使用GTID,我們就能非常方便的進(jìn)行failover的處理。
仍然是前面的例子,假設(shè)b此時讀取到的a最后一個GTID為3E11FA47-71CA-11E1-9E33-C80AA9429562:23
,而c的為3E11FA47-71CA-11E1-9E33-C80AA9429562:15
,當(dāng)c指向新的master b的時候,我們通過GTID就可以知道,只要在b中的binlog中找到GTID為3E11FA47-71CA-11E1-9E33-C80AA9429562:15
這個event,那么c就可以從它的下一個event的位置開始復(fù)制了。雖然查找binlog的方式仍然是順序查找,稍顯低效暴力,但比起我們自己去猜測哪一個filename和position,要方便太多了。
google很早也有了一個Global Transaction ID的補丁,不過只是使用的一個遞增的整形,LedisDB就借鑒了它的思路來實現(xiàn)failover,只不過google貌似現(xiàn)在也開始逐步遷移到MariaDB上面去了。
MariaDB的GTID實現(xiàn)跟MySQL 5.6是不一樣的,這點其實比較麻煩,對于我的MySQL工具集go-mysql來說,意味著要寫兩套不同的代碼來處理GTID的情況了。后續(xù)是否支持MariaDB再看情況吧。
Pseudo GTID
GTID雖然是一個好東西,但是僅限于MySQL 5.6+,當(dāng)前仍然有大部分的業(yè)務(wù)使用的是5.6之前的版本,筆者的公司就是5.5的,而這些數(shù)據(jù)庫至少長時間也不會升級到5.6的。所以我們?nèi)匀恍枰惶缀玫臋C制來選擇master binlog的filename以及position。
最初,筆者打算研究MHA的實現(xiàn),它采用的是首先復(fù)制relay log來補足缺失的event的方式,但筆者不怎么信任relay log,同時加之MHA采用的是perl,一個讓我完全看不懂的語言,所以放棄了繼續(xù)研究。
幸運的是,筆者遇到了orchestrator這個項目,這真的是一個非常神奇的項目,它采用了一種Pseudo GTID的方式,核心代碼就是這個
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上面創(chuàng)建了一個事件,每隔10s,就將一個uuid寫入到一個view里面,而這個是會記錄到binlog中的,雖然我們?nèi)匀徊荒芟馟TID那樣直接定位到一個event,但也能定位到一個10s的區(qū)間了,這樣我們就能在很小的一個區(qū)間里面對比兩個MySQL的binlog了。
繼續(xù)上面的例子,假設(shè)c最后一次出現(xiàn)uuid的位置為s1,我們在b里面找到該uuid,位置為s2,然后依次對比后續(xù)的event,如果不一致,則可能出現(xiàn)了問題,停止復(fù)制。當(dāng)遍歷到c最后一個binlog event之后,我們就能得到此時b下一個event對應(yīng)的filename以及position了,然后讓c指向這個位置開始復(fù)制。
使用Pseudo GTID需要slave打開log-slave-update
的選項,考慮到GTID也必須打開該選項,所以個人感覺完全可以接受。
后續(xù),筆者自己實現(xiàn)的failover工具,將會采用這種Pseudo GTID的方式實現(xiàn)。
在《MySQL High Availability》這本書中,作者使用了另一種GTID的做法,每次commit的時候,需要在一個表里面記錄gtid,然后就通過這個gtid來找到對應(yīng)的位置信息,只是這種方式需要業(yè)務(wù)MySQL客戶端的支持,筆者不很喜歡,就不采用了。
后記
MySQL HA一直是一個水比較深的領(lǐng)域,筆者僅僅列出了一些最近研究的東西,有些相關(guān)工具會盡量在go-mysql中實現(xiàn)。
更新
經(jīng)過一段時間的思考與研究,筆者又有了很多心得與收獲,設(shè)計的MySQL HA跟先前有了很多不一樣的地方。后來發(fā)現(xiàn),自己設(shè)計的這套HA方案,跟facebook這篇文章幾乎一樣,加之最近跟facebook的人聊天聽到他們也正在大力實施,所以感覺自己方向是對了。
新的HA,我會完全擁抱GTID,比較這玩意的出現(xiàn)就是為了解決原先replication那一堆問題的,所以我不會考慮非GTID的低版本MySQL了。幸運的是,我們項目已經(jīng)將MySQL全部升級到5.6,完全支持GTID了。
不同于fb那篇文章將mysqlbinlog改造支持semi-sync replication協(xié)議,我是將go-mysql的replication庫支持semi-sync replication協(xié)議,這樣就能實時的將MySQL的binlog同步到一臺機器上面。這可能就是我和fb方案的唯一區(qū)別了。
只同步binlog速度鐵定比原生slave要快,畢竟少了執(zhí)行binlog里面event的過程了,而另外真正的slaves,我們?nèi)匀皇褂米钤嫉耐椒绞?,不使用semi-sync replication。然后我們通過MHA監(jiān)控整個集群以及進(jìn)行故障轉(zhuǎn)移處理。
以前我總認(rèn)為MHA不好理解,但其實這是一個非常強大的工具,而且真正看perl,發(fā)現(xiàn)也還是看的懂得。MHA已經(jīng)被很多公司用于生產(chǎn)環(huán)境,經(jīng)受了檢驗,直接使用絕對比自己寫一個要劃算。所以后續(xù)我也不會考慮zookeeper,考慮自己寫agent了。