如需转载,请标明原文出处以及作者

陈锐 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_reservereservation_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_reservereservation_commit方法中涉及了两张表quota_usagesreservations,首先排除两张表互相锁的可能性,因为所有对这两张表的加锁都是按照先quota_usagesreservations的顺序,保证了不会死锁。

回到_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/12825663/can-two-select-for-update-statements-on-the-same-table-cause-a-deadlock

http://stackoverflow.com/questions/10245560/deadlocks-in-postgresql-when-running-update



Published

22 January 2014

Category

OpenStack

Tags