數據庫寫操做棄用「SELECT ... FOR UPDATE」解決方案

問題闡述

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,就可以保證讀寫的一致性,可是會帶來延遲問題。

Appendix

  1. understanding reservations concurrency locking in nova

  2. investigating replication latency in percona xtradb cluster

  3. understanding multi node writing conflict metrics in percona xtradb cluster and galera

  4. an introduction to lock-free programming

  5. mysql multi master replication with galera

相關文章
相關標籤/搜索