首先声明:未经本人同意,请勿转载,谢谢!
本人使用自己编译的开源版本的greenplum数据库用于学习,版本为PostgreSQL 8.3.23 (Greenplum Database 4.3.99.00 build dev) on x86_64-unknown-linux-gnu, compiled by GCC gcc (GCC) 4.8.5 20150623
在使用的过程中遇到不少的问题,今天记录一下高并发的情况下,执行insert和vacuum操作造成的死锁,以及解决方案
一、问题描述:
在对ao分区表进行高并发测试的时候同时执行了vacuum的动作,发现会有死锁的问题产生,可以通过如下手段复现:
1)多个线程循环执行insert into t3 select * from XX(t3为本例中用于测试的分区表),为了提高问题复现的速度,可以仅对表中的一个分区进行操作
2)单个线程循环执行vacuum的动作
一段时间后会发现vacuum和insert都卡住,至此问题被复现出来。
二、结论:
该问题仅在AO分区表中会出现,产生的原因是由于数据库加锁的流程设计不合理导致(详情见分析过程)。
三、问题定位及解决方案
抛砖引玉,仅分享自己知道的一点点东西,如果有错误的地方还请指正,欢迎大家一起来讨论这个问题。
1)首先连接master执行:
select * from pg_stat_activity;
查到如下结果:
(图一)
发现有一个session是lock状态,为了弄清楚这里的lock的具体情况,则连接master和standby查看锁的具体情况,执行如下sql:
select a.locktype,b.relname,substring(c.current_query,1,50),c.xact_start,a.pid,a.mode,a.granted from pg_locks a,pg_class b,pg_stat_activity c where a.relation = b.oid and a.pid = c.procpid and relname like ‘t3%‘;
master:
(图二)
segment1:
(图三)
segment2:
(图四)
需要注意的是,这里图三和图四中在pg_stat_activity表中的current_query一列有<IDLE> in transaction,这里的值和我们平时看到的<IDLE>不是一回事,并不是一个空闲的连接,要弄清楚它的意义来看下官方文档:
(图五)
官方的文档里,对该值仅有这么一小段描述,这里并不能完全的表达出这个字段的意思,我们来看源码:
(图六)
源码里就可以看出这种状态说明该连接的事务是处于阻塞状态。
那么又是为什么会有锁等待的出现呢,因为同一时间,针对同一个对象,有些锁是不能够同时被不同的事务所持有的,如果一个事务持有了某个锁,另一个事务需要获取相同的锁或者是与这个锁冲突的锁,就会出现等待的情况,我们来看下锁之间的冲突情况:
(图七)
综合以上的所有信息,画出了如下的图:
(图七)
说明:
相同颜色的色块表示同一个连接(在物理机上,由ps -ef | grep $pid查到的con确定),红色的线表示等待关系。
由上图可以清晰的看出,在master上,con52等待con49持有的锁,而在segment2中,con49等待con52持有的锁,因此产生了死锁。
那么又是什么造成了这种锁的状态的形成?官方的描述中AccessShareLock仅仅是在对表数据不产生任何影响的查询语句才会申请:
(图八)
这里看图七的master节点,明明产生的是AccessShareLock,与官方的描述出现了不一致,对于这个疑惑,只有源码能告诉我们答案:
(图九)
这里有个if条件,这说明在处理ao分区表的时候,会在处理完成后会在master上加上AccessShareLock,结合图七和图九,也就解释了为什么在master会产生insert的锁等待vacuum的情况了,同时也回答了为什么结论中说该问题仅会出现在ao分区表中。
至此,产生这个问题的原因基本明晰了,那么遇到了这种情况该如何解决?
要知道不同的锁之间谁等待了谁,提供如下sql,很方便的就能知道等待关系:
create or replace function f_lock_level(i_mode text) returns integer as $$ begin RETURN (select case i_mode when ‘INVALID‘ then 0 when ‘AccessShareLock‘ then 1 when ‘RowShareLock‘ then 2 when ‘RowExclusiveLock‘ then 3 when ‘ShareUpdateExclusiveLock‘ then 4 when ‘ShareLock‘ then 5 when ‘ShareRowExclusiveLock‘ then 6 when ‘ExclusiveLock‘ then 7 when ‘AccessExclusiveLock‘ then 8 else 0 end ); end; $$ language plpgsql strict; --查询 with t_wait as (select a.mode,a.locktype,a.database,a.relation,a.page,a.tuple,a.classid,a.objid,a.objsubid, a.pid,a.virtualtransaction,a.virtualxid,a,transactionid,b.current_query,b.xact_start,b.query_start, b.usename,b.datname from pg_locks a,pg_stat_activity b where a.pid=b.procpid and not a.granted), t_run as (select a.mode,a.locktype,a.database,a.relation,a.page,a.tuple,a.classid,a.objid,a.objsubid, a.pid,a.virtualtransaction,a.virtualxid,a,transactionid,b.current_query,b.xact_start,b.query_start, b.usename,b.datname from pg_locks a,pg_stat_activity b where a.pid=b.procpid and a.granted) select r.locktype,r.mode r_mode,r.usename r_user,r.datname r_db,r.relation::regclass,r.pid r_pid, r.page r_page,r.tuple r_tuple,r.xact_start r_xact_start,r.query_start r_query_start, now()-r.query_start r_locktime,r.current_query r_query,w.mode w_mode,w.pid w_pid,w.page w_page, w.tuple w_tuple,w.xact_start w_xact_start,w.query_start w_query_start, now()-w.query_start w_locktime,w.current_query w_query from t_wait w,t_run r where r.locktype is not distinct from w.locktype and r.database is not distinct from w.database and r.relation is not distinct from w.relation and r.page is not distinct from w.page and r.tuple is not distinct from w.tuple and r.classid is not distinct from w.classid and r.objid is not distinct from w.objid and r.objsubid is not distinct from w.objsubid and r.transactionid is not distinct from w.transactionid and r.pid <> w.pid order by f_lock_level(w.mode)+f_lock_level(r.mode) desc,r.xact_start;
注意:上面的sql适用于文章开头部分greenplum内核版本,不同的版本会有些许差异,这个sql是我根据文章最后参考资料德哥给出的SQL自己修改的,如果您的环境中无法运行,请使用德哥给出的语句。
该sql会返回类似如下的结果(部分):
(图十)
每条结果以w_开头的结果表示正在等待的会话信息,以r_开头的代表正在运行的会话信息。
为了不影响正常的插入流程,可以找到vacuum语句的pid,使用select pg_terminate_backend($pid);语句终止vacuum的会话,业务即可继续进行。
那么问题是不是就此可以结束?其实并没有,我们来看官方文档:
官方指出,在发生死锁的情况下,会自动的回滚一个事务,保证另一个事务的正常运行,那么在上面所述的情况下,为什么没有发生事务的自动回滚?这是我一直没有想明白的问题,如果有相关的专业人士看到这篇文章请指点一二,解答我心中的疑问,感激不尽!
参考资料:
https://yq.aliyun.com/articles/86631
https://github.com/greenplum-db/gpdb/pull/425
https://github.com/greenplum-db/gpdb/issues/2837
在github中,有提出使用debug的方式复现死锁的问题,这是最快捷的方式,同时也给出了源码级别的修复建议,感兴趣的朋友可以自行阅读。
结语:
本人一直做的是java的开发工作,C并不是特别熟悉,要解决这个问题,根本上还得从源码入手,本人没有这个能力,如果有朋友知道如何修复这个问题,还请告知。同时本文如有表述不对的地方,还请各位指出,我会加强自身的学习,保证分享出来的东西是正确的。
另外,推荐一个好朋友的公众号,我觉得里面文章特别好,很多问题是真正讲清楚了,全都是他自己一点点整理出来的,我一直在读他的文章,受益良多。