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,就可以保证读写的一致性,可是会带来延迟问题。