GaussDB(DWS)中的分布式死锁问题实践

本文分享自华为云社区《GaussDB(DWS)中的分布式死锁问题实践》,作者: 他强由他强 。

1、什么是分布式死锁

分布式死锁是相对于单机死锁而言,一个事务块中的语句,可能会分散在集群里多个节点(CN/DN)执行,在不同节点上可能都会持有锁,当并发事务进行时可能会导致分布式(全局)死锁,如下图所示,会话SESSION1持有了DN1上的lock1资源后再去请求DN2上的lock2,会话SESSION2持有了DN2上的lock2资源后再去请求DN1上的lock1,两个会话形成互相等待。出现分布式死锁现象后,如果没有外部干预,通常是一方等待锁超时报错后,事务回滚清理持有锁资源,另一方可继续执行。

2、常见的分布式死锁场景

一般来说,分布式死锁的产生与在不同节点上的并发时序或持锁顺序有关,所以现网实际发生概率较低,分布式死锁通常都是RegularLock类型,下面是几种常见的分布式死锁场景,举例说明两个并发事务产生的分布式死锁:

1)锁升级

复制代码
# 集群两个CN,两个DN
create table mytable(a int, b int);
insert into mytable values(1,1),(2,2);

其中sessionA与sessionB由不同CN发起(sessionA:CN1,session2:CN2),执行时序如下:

|-----------------------------------------------------------|----------------------------------------------|
| session A(CN1) | session B(CN2) |
| begin; | begin; |
| select * from mytable; // CN1上拿1级表锁 | |
| | select * from mytable; // CN2上拿1级表锁 |
| truncate table mytable; // CN1上拿8级表锁 // CN2上拿8级表锁,waiting | |
| | truncate table mytable; // CN1上拿8级表锁,waiting |

可以看到sessionA里select会持有本地1级锁,truncate会持有8级锁,出现锁升级现象,导致sessionA在CN2上等锁,sessionB在CN1上等锁,形成相互等待。

2)行更新冲突

复制代码
# 集群两个CN,两个DN
create table mytable(a int, b int);
insert into mytable values(1,1),(2,2); 

行存表发生行更新冲突是比较常见的分布式死锁场景。因为表是round robin分布,所以行a = 1 与 a = 2数据可以保证分别分布在DN1和DN2节点。

一个事务在更新数据时需要在对应DN节点持有本xid事务锁的Exclusive锁,当发生行更新冲突时(写写冲突),一个事务需要阻塞等待另一个事务提交(等待获取对方事务锁ShareLock),形成相互等待时造成分布式死锁。

其中sessionA与sessionB可由相同或者不同CN发起,执行时序如下:

|----------------------------------------------------------------|----------------------------------------------------------------|
| session A(xid1) | session B(xid2) |
| begin; | begin; |
| update mytable set b = 1 where a = 1; // DN1上拿xid1的事务锁 | |
| | update mytable set b = 2 where a = 2; // DN2上拿xid2的事务锁 |
| update mytable set b = 1 where a = 2; // DN2上拿xid2的事务锁,waiting | |
| | update mytable set b = 2 where a = 1; // DN1上拿xid1的事务锁,waiting |

3)CU更新冲突

复制代码
# 集群两个CN,两个DN
create table mytable(a int, b int) with (orientation = column);
insert into mytable values(1,1),(2,2),(3,3),(4,4);

其中sessionA与sessionB可由相同或者不同CN发起,执行时序如下:

|----------------------------------------------------------------|----------------------------------------------------------------|
| session A(xid1) | session B(xid2) |
| begin; | begin; |
| update mytable set b = 1 where a = 1; // DN1上拿xid1的事务锁 | |
| | update mytable set b = 2 where a = 2; // DN2上拿xid2的事务锁 |
| update mytable set b = 1 where a = 3; // DN2上拿xid2的事务锁,waiting | |
| | update mytable set b = 2 where a = 4; // DN1上拿xid1的事务锁,waiting |

当出现更新冲突时,对于行存表来说是对一行数据加锁(如场景2所述),但对于列存表来说是对一个CU加锁。所以一个事务里的更新语句如果涉及到不同的CU,也会拿事务锁,可能就会产生分布式死锁。

我们可以通过如下语句观察ctid信息判断数据是否分布在同一个CU上,如下图:可以看到a = 1 与 a = 3分布在DN1上,且在同一个CU;a = 2 与 a = 4分布在DN2上,且在同一个CU。所以这也能解释为什么看上去列存表更新不同的"行数据"也会产生锁阻塞和分布式死锁现象。

4)单语句出现分布式死锁

前面几种场景都是事务块里涉及到多条SQL语句,可能会到不同节点上去交错拿锁导致的分布式死锁,但有时候某些单语句场景可能也会出现分布式死锁,如下:

|---------------------------------------------------------|---------------------------------------------------------|
| session A(xid1) | session B(xid2) |
| update/delete mytable set b = 1 where a = 1; // waiting | update/delete mytable set b = 2 where a = 2; // waiting |

此类问题与数据分布有关,如下场景都可能会导致这个现象:

1)若表是复制表,每个DN节点上都有数据,更新时会去所有DN并发执行

2)表是普通行存表或列存表,但有行数据(如a=1)同时分布在了多个DN节点上,如round robin分布下插入两条相同a=1的数据

复制代码
insert into mytable values(1,1);
insert into mytable values(1,2);

此场景需要具体去排查数据分布是否会造成此情况。
3、规避分布式死锁的方法

1)控制锁级别,减少锁升级

按照各类操作的锁级别建议规则使用,开发时不要盲目提高锁级别,造成可能发生的不必要的锁等待

2)控制锁粒度

合理控制锁使用范围,及时释放

3)控制拿锁顺序

尽量控制对资源操作的顺序,比如对各分区表的操作顺序,避免乱序造成的死锁。但全局各节点的拿锁情况或顺序一般无法提前预测,往往为了考虑提高性能,请求会在节点间并发执行,但我们可以在某个节点上控制并发互斥以规避分布式死锁问题,如操作某个表时先去FirstCN上请求持锁,持锁成功后再对其他CN和DN并行拿锁。GaussDB(DWS)内核的很多地方的设计中会有这种思想,如DDL语句,autoanalyze等。

4)主动设置较短的锁超时时间

一般用在非关键的用户路径操作上,如用户语句在runtime analyze子事务的流程里会主动设置锁超时时间为2秒,发生阻塞后可及时放锁,避免出现长时间锁等待,也能规避潜在的分布式死锁场景

4、如何排查系统是否产生了分布式死锁

本质上是发现集群中是否有全局的死锁环等待关系,内核中提供有许多视图可以辅助观察持锁等待情况,但需要注意的是,因为查询到的锁等待关系可能只是暂时的瞬间状态,只有持续存在的锁等待才会造成分布式死锁,需要判断锁是否稍后会主动释放(事务提交前),还是只能等到事务提交后释放。如何判断系统是否产生了分布式死锁,有以下方法:

1)查询pgxc_deadlock视图,会输出全局死锁环信息,如果信息为空,则代表无分布式死锁,但需要注意在某些复杂的场景可能会出现误报,即输出有死锁环信息,但可能并没有形成分布式死锁;

当有分布式死锁时,直到等待锁超时后,某一方事务会出现"Lock wait timeout...",打印具体的锁信息及锁语句,报错后释放锁,另一方解除阻塞。相关的锁超时参数是lockwait_timeout或update_lockwait_timeout。

2)在GaussDB(DWS)的8.3.0版本及以后,内核已经支持了自动化地分布式死锁检测,当检测到系统中存在分布式死锁等待关系后,会自动报错和挑选事务进行cancel,具体原理下一节中会详细介绍。

如下图所示,若用户出现"cancelled by global deadlock detector"报错,代表检测到分布式死锁并被查杀,此时可以去检测CN上(FirstCN或者CCN)上去找相关日志信息,会输出具体死锁和session查杀信息,需要注意用户语句执行CN和检测CN可能并不是一个,此时检测CN会向执行CN发起事务cancel。

5、分布式死锁检测原理

分布式死锁检测的目标原则是做到不误报,争取不漏报,尽量及时报。

我们使用了中心化的收集检测思想,如流程图所示:首先挑选一个CN作为检测CN(类似master角色),CN上新增后台线程启动GlobalDeadlockDetector模块,周期性向集群所有节点收集锁等待关系,计算等待者和持有者信息,然后构造全局有向图(WFG),依赖定义的规则对图的顶点和边进行消除,判断是否能消除完成。如果无法消除完成,则出现了死锁环,并进行二次DoubleCheck,如果两次的死锁环信息有交集,则报告死锁信息。当发现死锁后,按照事务时间戳挑选最年轻的事务(youngest)进行中断,并会对用户报错。

我们在设计上主要参考了Greenplum的思路,由于与GaussDB(DWS)架构和应用场景上的差异性,也针对做了一些改造和优化,主要包括:

1、检测节点的选择:在FirstCN或CCN上启动后台检测线程,依赖外部OM模块做高可用切换;

2、等待关系图节点的标识:由检测CN构造全局唯一global_session下发,格式为:timestamp.pid.node_name(timestamp为事务开始的时间戳,pid为执行CN上的线程号,node_name为执行CN名称);
3、虚实边关系定义:支持定义线程级别虚实边,过滤掉不必要的死锁误报;

  • 实边:锁等待关系的变化,需要等到持有者事务会话commit或abort
  • 虚边:锁等待关系的变化,不需要等到持有者事务会话commit或abort

4、死锁结果的合法性检查:增加DoubleCheck机制,提高检测结果准确性,结果以连续两次检测到的死锁环交集为准;

5、死锁消除:执行CN与检测CN可能不同,可能存在跨CN发起的事务中断;

6、与单机死锁检测算法互补:当分布式死锁检测算法如果发现检测到单机的死锁环路等待关系后,则忽略,与单机死锁检测算法处理不冲突;

分布式死锁检测相关参数:

  • enable_global_deadlock_detector:分布式死锁检测功能是否开启,默认off

  • global_deadlock_detector_period:分布式死锁检测周期,默认5秒

点击关注,第一时间了解华为云新鲜技术~

相关推荐
阿巴~阿巴~2 天前
死锁防范:四大条件与破解之道
linux·服务器·线程·线程安全·死锁
wok1578 天前
如何构建构高性能、高可用、可扩展的集群?
架构·集群
qqxhb18 天前
系统架构设计师备考第45天——软件架构演化评估方法和维护
分布式·缓存·系统架构·集群·cdn·单体·已知未知评估
ICollection23 天前
RabbitMQ的安装集群、仲裁队列配置
rabbitmq·集群·银河麒麟
cominglately1 个月前
记录一次生产环境数据库死锁的处理过程
java·死锁
huangyuchi.1 个月前
【Linux系统】线程安全与死锁问题
互斥锁·线程安全·linux系统·死锁·linux线程·linux锁·死锁条件
howard20051 个月前
在VMWare上搭建openEuler25.09集群
集群·安装·配置·openeuler 25.09
ICollection1 个月前
银河麒麟下Redis的安装和集群配置
redis·集群·银河麒麟·国防版
ICollection1 个月前
RabbitMQ的安装集群、镜像队列配置
消息队列·rabbitmq·集群
Ronin3052 个月前
【Linux系统】单例式线程池
linux·服务器·单例模式·线程池·线程安全·死锁