Nova大并发下的死锁问题 regexisart
如需转载,请标明原文出处以及作者
陈锐 RuiChen @kiwik
2014/1/22 21:41:11
问题
这两天遇到了一个PostgreSQL死锁
的问题,OpenStack环境部署使用的是PostgreSQL-8.3,当创建虚拟机请求达到一定的并发量的时候,log中出现了如下的问题:
2014-01-09 06:43:51.188 12358 TRACE nova.quota DBAPIError: (TransactionRollbackError) deadlock detected
2014-01-09 06:43:51.188 12358 TRACE nova.quota DETAIL: Process 32402 waits for ShareLock on transaction 2016703; blocked by process 32389.
2014-01-09 06:43:51.188 12358 TRACE nova.quota Process 32389 waits for ShareLock on transaction 2016704; blocked by process 32402.
2014-01-09 06:43:51.188 12358 TRACE nova.quota HINT: See server log for query details.
2014-01-09 06:43:51.188 12358 TRACE nova.quota 'SELECT quota_usages.created_at AS quota_usages_created_at, quota_usages.updated_at AS quota_usages_updated_at, quota_usages.deleted_at AS quota_usages_deleted_at, quota_usages.deleted AS quota_usages_deleted, quota_usages.id AS quota_usages_id, quota_usages.project_id AS quota_usages_project_id, quota_usages.user_id AS quota_usages_user_id, quota_usages.resource AS quota_usages_resource, quota_usages.in_use AS quota_usages_in_use, quota_usages.reserved AS quota_usages_reserved, quota_usages.until_refresh AS quota_usages_until_refresh \nFROM quota_usages \nWHERE quota_usages.deleted = %(deleted_1)s AND quota_usages.project_id = %(project_id_1)s AND (quota_usages.user_id = %(user_id_1)s OR quota_usages.user_id IS NULL) FOR UPDATE' {'project_id_1': u'8891d4478bbf48ad992f050cdf55e9b5', 'user_id_1': u'22b6a9fe91b349639ce39146274a25ba', 'deleted_1': 0}
抛出这个异常的位置,从日志中看,涉及创建虚拟机流程中的两个步骤quota_reserve
和reservation_commit
,是同一个私有方法抛出的_get_user_quota_usages
分析
从日志中看到是两个不同的transaction在等待对方释放ShareLock,从而发生了deadlock。
def _get_user_quota_usages(context, session, project_id, user_id):
# Broken out for testability
rows = model_query(context, models.QuotaUsage,
read_deleted="no",
session=session).\
filter_by(project_id=project_id).\
filter(or_(models.QuotaUsage.user_id == user_id,
models.QuotaUsage.user_id == None)).\
with_lockmode('update').\
all()
return dict((row.resource, row) for row in rows)
可以看到在_get_user_quota_usages函数中select语句增加的lockmode为update,最终转化的select为这样的格式select ... for update
,从postgresql的官方文档看select … for update相当于在select语句中增加行级排他锁,行为相当于update,delete操作。
quota_reserve
和reservation_commit
方法中涉及了两张表quota_usages
和reservations
,首先排除两张表互相锁的可能性,因为所有对这两张表的加锁都是按照先quota_usages
后reservations
的顺序,保证了不会死锁。
回到_get_user_quota_usages函数,其实最终只转化为一条select … for update的sql语句,如上面log里的select语句,单条sql会死锁么?答案是:“会!”,你没有听错,单条sql语句也会发生死锁,我也是被这个问题纠结了很久,有点违背常理的一个现象。
仔细分析一下select … for update,其实分为两个步骤,查询和锁定,先要通过查询确定记录的范围,然后在记录上加排他锁,防止其他事务更新,而且PostgreSQL是没有内存行级锁的,所以所有的操作都要在持久化存储上一行一行的做,也就查到一行,加锁一行。再有一点,Mysql和PostgreSQL都不保证每次的select查询结果是按照相同的顺序返回的,除非使用order by
子句。
综合以上两点:
- select不保证顺序
- 一行一行加锁
考虑如下两个事务A和B,在并发的情况下select … for update资源(1, 2),时间序列可能是这样的:A对2加排他锁,B对1加排他锁,A尝试对1加排他锁,等待B释放,同时B尝试对2加排他锁,等待A释放,达成死锁条件。
+------+--------+
| A | B |
+------+--------+
| 2 | 1 |
| 1 | 2 |
+------+--------+
为了验证这个猜想,我分别在Mysql和PostgreSQL上用select … for update做了实验,结果如下:
keystone=> select * from chenrui where id=2 for update;
ERROR: deadlock detected
DETAIL: Process 14179 waits for ShareLock on transaction 4427; blocked by process 17907.
Process 17907 waits for ShareLock on transaction 4397; blocked by process 14179.
mysql> select * from chenrui where id=1 for update;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
PostgreSQL上的错误和log中的错误吻合。
侦测到死锁之后PostgreSQL会中断其中一个事务的执行,从而打破死锁条件。从而保证一个事务可以commit,另一个事务rollback。
进一步
相同的压力下Mysql没有发生deadlock,而PostgreSQL发生了,为什么?
本人对数据库内部实现不是很懂。
推测两种可能性:
-
Mysql有内存行级锁,可以在内存中一次将所有select … for update结果加锁,未验证,如有知道的同学,请告知一下
-
Mysql的代码处理更好一点,防止死锁做的更好一些,在更大的压力下才会发生,个人偏重这个可能
而且在quota_reserve
方法上有@_retry_on_deadlock
的注解,保证了deadlock之后的重试。但是这里有个bug导致@_retry_on_deadlock不能处理PostgreSQL的deadlock。
补充
一些在实验中的发现:
Mysql的默认事务隔离级别是repeabled-read,而PostgreSQL是read-committed
会造成如下的区别,在Mysql中使用select查询到的是事务开始时的值,使用select … for update可以看到在事务执行过程中,其他事务修改的值,在事务中,通过select和select … for update查询到的结果可能不同。
PostgreSQL默认事务隔离级别是read-committed,所以每次查询都是查询时刻的最新值,select 和select … for update返回的结果一样。
mysql> select * from chenrui;
+------+--------+
| id | status |
+------+--------+
| 1 | 1 |
| 2 | 8 |
+------+--------+
2 rows in set (0.00 sec)
mysql> select * from chenrui for update;
+------+--------+
| id | status |
+------+--------+
| 1 | 1 |
| 2 | 9 |
+------+--------+
2 rows in set (0.00 sec)
参考文献:
http://stackoverflow.com/questions/10245560/deadlocks-in-postgresql-when-running-update