Mysql Galera集羣是迄今OpenStack服務最流行的Mysql部署方案,它基於Mysql/InnoDB,個人OpenStack部署方式從原來的主從複製轉換到Galera的多主模式。html
Galera雖然有不少好處,如任什麼時候刻任何節點均可讀可寫,無複製延遲,同步複製,行級複製,可是Galera存在一個問題,也能夠說是在實現 真正的多主可寫上的折衷權衡,也就是這個問題致使在代碼的數據庫層的操做須要棄用寫鎖,下面我說一下這個問題。node
這個問題是Mysql Galera集羣不支持跨節點對錶加鎖,也就是當OpenStack一個組件有兩個會話分佈在兩個Mysql節點上同時寫入一條數據,其中一個會話會遇到 死鎖的狀況,也就是獲得deadlock的錯誤,而且該狀況在高併發的時候發生機率很高,在社區Nova,Neutron該狀況的報告有不少。mysql
這個行爲實際上是Galera預期的結果,它是由樂觀鎖併發控制機制引發的,當發生多個事務進行寫操做的時候,樂觀鎖機制假設全部的修改都能 沒有衝突地完成。若是兩個事務同時修改同一個數據,先commit的事務會成功,另外一個會被拒絕,並從新開始運行整個事務。 在事務發生的起始節點,它能夠獲取到全部它須要的鎖,可是它不知道其餘節點的狀況,因此它採用樂觀鎖機制把事務(在Galera中叫writes et)廣播到全部其餘節點上,看在其餘節點上是否能提交成功。這個writeset會在每一個節點上進行驗證測試,來決定該writeset是否被接受, 若是檢驗失敗,這個writeset就會被拋棄,而後最開始的事務也會被回滾;若是檢驗成功,事務就被提交,writeset也被應用到其餘節點上。 這個過程以下圖所示:git
在Python的SQLAlchemy庫中,有一個「with_lockmode('update')」語句,這個表明SQL語句中的「SELECT ... FOR UPDATE」,在我參與過的計費項目和社區的一些項目的代碼中有大量的該結構,因爲寫鎖不能在集羣中同步,因此這個語句在Mysql集羣中就沒有獲得它應有的效果,也就是在語義上有問題,可是最後Galera會經過報deadlock錯誤,只讓一個commit成功,來保證Mysql集羣的ACID性。github
把請求發往一個節點,這個在HAProxy中就能夠配置,只設定一個節點爲master,其他節點爲backup,HAProxy會在master失效的時候 自動切換到某一個backup上,這個也
是不少解決方案目前使用的方法,HAProxy配置以下:sql
server xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx:3306 check server xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx:3306 check backup server xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx:3306 check backup
對OpenStack的全部Mysql操做作讀寫分離,寫操做只在master節點上,讀操做在全部節點上作負載均衡。OpenStack沒有原生支持,但 是有一個開源軟件可使用,maxscale。數據庫
上面的解決方法只是一些workaround,目前狀況下最終極的解決方法是使用lock-free的方法來對數據庫進行操做,也就是無鎖的方式,這就 須要對代碼進行修改,如今Nova,Neutron,Gnocchi等項目已經對其進行了修改。api
首先得有一個retry機制,也就是讓操做執行在一個循環中,一旦捕獲到deadlock的error就將操做從新進行,這個在OpenStack的oslo.db中已 經提供了相應的方法叫wrap_db_retry,是一個Python裝飾器,使用方法以下:session
from oslo_db import api as oslo_db_api @oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True, retry_on_request=True) def db_operations(): ...
而後在這個循環之中咱們使用叫作"Compare And Swap(CAS)"的無鎖方法來完成update操做,CAS是最早在CPU中使用的,CAS說白了就是先比較,再修改,在進行UPDATE操做以前,咱們先SELEC T出來一些數據,咱們叫作指望數據,在UPDATE的時候要去比對這些指望數據,若是指望數據有變化,說明有另外一個會話對該行進行了修改, 那麼咱們就不能繼續進行修改操做了,只能報錯,而後retry;若是沒變化,咱們就能夠將修改操做執行下去。該行爲體如今SQL語句中就是在 UPDATE的時候加上WHERE語句,如"UPDATE ... WHERE ..."。併發
給出一個計費項目中修改用戶等級的DB操做源碼:
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True, retry_on_request=True) def change_account_level(self, context, user_id, level, project_id=None): session = get_session() with session.begin(): # 在會話剛開始的時候,須要先SELECT出來該account的數據,也就是指望數據 account = session.query(sa_models.Account).\ filter_by(user_id=user_id).\ one()] # 在執行UPDATE操做的時候須要比對指望數據,user_id和level,若是它們變化了,那麼rows_update就會被賦值爲0 ,就會走入retry的邏輯 params = {'level': level} rows_update = session.query(sa_models.Account).\ filter_by(user_id=user_id).\ filter_by(level=account.level).\ update(params, synchronize_session='evaluate') # 修改失敗,報出RetryRequest的錯誤,使上面的裝飾器抓獲該錯誤,而後從新運行邏輯 if not rows_update: LOG.debug('The row was updated in a concurrent transaction, ' 'we will fetch another one') raise db_exc.RetryRequest(exception.AccountLevelUpdateFailed()) return self._row_to_db_account_model(account)
該問題在OpenStack郵件列表中有說過,雖然Galera是生成同步的,也就是寫入數據同步到整個集羣很是快,用時很是短,但既然是分佈式系 統,本質上仍是須要一些時間的,尤爲是在負載很大的時候,同步不及時會很嚴重。
因此Galera只是虛擬同步,不是直接同步,也就是會存在一些gap時間段,沒法讀到寫入的數據,Galera提供了一個配置項,叫作wsrep_sync_ wait,它的默認值是0,若是賦值爲1,就可以保證讀寫的一致性,可是會帶來延遲問題。