数据库写操做弃用“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

相关文章
相关标签/搜索