大数跨境
0
0

分布式数据库集群中的全局死锁与解决方案

分布式数据库集群中的全局死锁与解决方案 IvorySQL开源数据库社区
2025-05-23
0

概述

如今,支持分布式事务是许多用例的典型要求。然而,如果你计划使用 PostgreSQL 构建分布式数据库解决方案,全局死锁检测是一个关键的挑战性问题。关于全局死锁的讨论很多,但本文将为你提供一个逐步创建全局死锁的步骤,并分享一些基于个人经验的思考。

死锁

首先,死锁的基本概念是:进程 A 在持有锁 1 的同时试图获取锁 2,而进程 B 在持有锁 2 的同时试图获取锁 1。在这种情况下,进程 A 和进程 B 都无法继续执行,它们将无限期地相互等待。由于 PostgreSQL 允许用户事务以任意顺序请求锁,因此这种死锁可能发生。当发生这种死锁时,没有解决方案,唯一解决锁问题的方法是其中一个事务必须中止并释放锁。

为了解决死锁问题,PostgreSQL 内置了两个关键机制:

  • 通过锁等待队列和排序锁请求来尽量避免死锁;
  • 如果检测到死锁,要求事务中止。

有了这两个关键设计,单个 PostgreSQL 服务器内的死锁可以轻松解决。有关死锁的更多详细信息,可参考官方文档:src/backend/storage/lmgr/README[1]

在本文中,我们将这种死锁称为局部死锁,以区别于我们将要讨论的全局死锁。

PostgreSQL 能够检测本地死锁,原因在于其掌握所有锁的全局信息,并且可以轻松找到锁等待循环。在源代码中,PostgreSQL 定义了一个通用的 LOCKTAG 数据结构,让用户事务填充不同的锁信息。以下是 PostgreSQL 中 LOCKTAG 数据结构的定义:typedef struct LOCKTAG
{
    uint32 locktag_field1; /* a 32-bit ID field */
    uint32 locktag_field2; /* a 32-bit ID field */
    uint32 locktag_field3; /* a 32-bit ID field */
    uint16 locktag_field4; /* a 16-bit ID field */
    uint8 locktag_type; /* see enum LockTagType */
    uint8 locktag_lockmethodid; /* lockmethod indicator */
} LOCKTAG;

在 PostgreSQL 中,定义了大约 10 种微观锁来处理不同用例中的锁,详细信息可以通过以下关键字查找:#define SET_LOCKTAG_RELATION(locktag,dboid,reloid)
#define SET_LOCKTAG_RELATION_EXTEND(locktag,dboid,reloid)
#define SET_LOCKTAG_DATABASE_FROZEN_IDS(locktag,dboid)
#define SET_LOCKTAG_PAGE(locktag,dboid,reloid,blocknum)
#define SET_LOCKTAG_TUPLE(locktag,dboid,reloid,blocknum,offnum)
#define SET_LOCKTAG_TRANSACTION(locktag,xid)
#define SET_LOCKTAG_VIRTUALTRANSACTION(locktag,vxid)
#define SET_LOCKTAG_SPECULATIVE_INSERTION(locktag,xid,token)
#define SET_LOCKTAG_OBJECT(locktag,dboid,classoid,objoid,objsubid)
#define SET_LOCKTAG_ADVISORY(locktag,id1,id2,id3,id4)

在分布式 PostgreSQL 部署环境(通常包括一个或多个协调节点和多个数据节点)中,可能会发生全局死锁,如 Databases and Distributed Deadlocks: A FAQ[2] 中描述的情况。由协调节点触发并导致多个数据节点相互等待的死锁称为全局死锁或分布式死锁。在这种情况下,原始 PostgreSQL 无法解决这个问题,因为每个数据节点并不认为这种情况是死锁。

如何创建全局死锁

为了更好地理解全局死锁问题,我们可以按照以下步骤创建一个全局死锁。首先,你需要安装 postgres_fdw 扩展来设置一个简单的分布式数据库集群,运行以下命令。

设置简单的分布式 PostgreSQL 数据库集群

假设你已经安装了 PostgreSQL 或构建了自己的二进制文件,然后初始化四个 PostgreSQL 服务器,如下所示:initdb -D /tmp/pgdata_cn1 -U $USER
initdb -D /tmp/pgdata_cn2 -U $USER
initdb -D /tmp/pgdata_dn1 -U $USER
initdb -D /tmp/pgdata_dn2 -U $USER

对于每个 PostgreSQL 数据库,编辑配置文件以设置不同的端口和集群名称。在本例中,我们有两个协调节点分别运行在端口 50001 和 50002 上,两个数据节点监听在端口 60001 和 60002 上。vim /tmp/pgdata_dn1/postgresql.conf
  port = 60001
  cluster_name = 'dn1'
vim /tmp/pgdata_dn2/postgresql.conf
  port = 60002
  cluster_name = 'dn2'
vim /tmp/pgdata_cn1/postgresql.conf
  port = 50001
  cluster_name = 'cn1'
vim /tmp/pgdata_cn2/postgresql.conf
  port = 50002
  cluster_name = 'cn2'

启动所有 PostgreSQL 服务器:pg_ctl -D /tmp/pgdata_cn1 -l /tmp/logfile_cn1 start
pg_ctl -D /tmp/pgdata_cn2 -l /tmp/logfile_cn2 start
pg_ctl -D /tmp/pgdata_dn1 -l /tmp/logfile_dn1 start
pg_ctl -D /tmp/pgdata_dn2 -l /tmp/logfile_dn2 start

设置数据节点

运行以下命令在两个数据节点上创建表 t:psql -d postgres -U $USER -p 60001 -c "create table t(a int, b text);"
psql -d postgres -U $USER -p 60002 -c "create table t(a int, b text);"

设置协调节点

协调节点的设置稍复杂。我们使用 postgres_fdw 扩展创建一个简单的分布式 PostgreSQL 数据库集群,因此需要按照以下步骤设置 postgres_fdw 扩展、用户映射、外部服务器和表。

在协调节点 1 上设置扩展、外部服务器、用户映射和表:psql -d postgres -U $USER -p 50001 -c "create extension postgres_fdw;"

psql -d postgres -U $USER -p 50001 -c "create server s1 foreign data wrapper postgres_fdw options (dbname 'postgres', host '127.0.0.1', port '60001');"
psql -d postgres -U $USER -p 50001 -c "create server s2 foreign data wrapper postgres_fdw options (dbname 'postgres', host '127.0.0.1', port '60002');"

psql -d postgres -U $USER -p 50001 -c "create user mapping for $USER server s1 options( user '$USER');"
psql -d postgres -U $USER -p 50001 -c "create user mapping for $USER server s2 options( user '$USER');"

psql -d postgres -U $USER -p 50001 -c "create table t(a int, b text) partition by range(a);"
psql -d postgres -U $USER -p 50001 -c "create foreign table t_s1 partition of t for values from (1000) to (1999) server s1 options(schema_name 'public', table_name 't');"
psql -d postgres -U $USER -p 50001 -c "create foreign table t_s2 partition of t for values from (2000) to (2999) server s2 options(schema_name 'public', table_name 't');"

在协调节点 2 上设置扩展、外部服务器、用户映射和表:psql -d postgres -U $USER -p 50002 -c "create extension postgres_fdw;"

psql -d postgres -U $USER -p 50002 -c "create server s1 foreign data wrapper postgres_fdw options (dbname 'postgres', host '127.0.0.1', port '60001');"
psql -d postgres -U $USER -p 50002 -c "create server s2 foreign data wrapper postgres_fdw options (dbname 'postgres', host '127.0.0.1', port '60002');"

psql -d postgres -U $USER -p 50002 -c "create user mapping for $USER server s1 options( user '$USER');"
psql -d postgres -U $USER -p 50002 -c "create user mapping for $USER server s2 options( user '$USER');"

psql -d postgres -U $USER -p 50002 -c "create table t(a int, b text) partition by range(a);"
psql -d postgres -U $USER -p 50002 -c "create foreign table t_s1 partition of t for values from (1000) to (1999) server s1 options(schema_name 'public', table_name 't');"
psql -d postgres -U $USER -p 50002 -c "create foreign table t_s2 partition of t for values from (2000) to (2999) server s2 options(schema_name 'public', table_name 't');"

创建全局死锁

现在,设置好这个简单的分布式 PostgreSQL 数据库集群后,你可以在两个不同的 psql 会话/控制台中运行以下命令来创建全局死锁。

首先,在每个数据节点上插入一个元组:psql -d postgres -U $USER -p 50001 -c "insert into t values(1001, 'session-1');"
psql -d postgres -U $USER -p 50002 -c "insert into t values(2001, 'session-2');"

其次,启动两个不同的 psql 控制台,并按照以下标示的 (x) 序列更新元组:psql -d postgres -U $USER -p 50001
begin;
update t set b = 'session-11' where a = 1001;     (1)

update t set b = 'session-11' where a = 2001;     (4)

psql -d postgres -U $USER -p 50002
begin;
update t set b = 'session-22' where a = 2001;     (2)

update t set b = 'session-22' where a = 1001;     (3)

在执行更新查询 (4) 后,你将遇到以下等待情况:postgres=# begin;
BEGIN
postgres=*# update t set b = 'session-11' where a = 1001;
UPDATE 1
postgres=*# update t set b = 'session-11' where a = 2001;


postgres=# begin;
BEGIN
postgres=*# update t set b = 'session-22' where a = 2001;
UPDATE 1
postgres=*#
postgres=*# update t set b = 'session-22' where a = 1001;

如果你使用以下命令检查 postgres 进程,你会发现来自两个不同数据节点的两个独立 postgres 进程处于 UPDATE 等待状态,并且这种状态将持续下去:david:postgres$ ps -ef |grep postgres |grep waiting
david     2811  2768  0 11:15 ?        00:00:00 postgres: dn1: david postgres 127.0.0.1(45454) UPDATE waiting
david     2812  2740  0 11:15 ?        00:00:00 postgres: dn2: david postgres 127.0.0.1(55040) UPDATE waiting

当上述全局死锁发生时,可以通过调试工具 gdb 附加到任一进程,详细检查其等待状态。如果你深入研究源代码,你会发现这个全局死锁实际上与 SET_LOCKTAG_TRANSACTION 有关:#define SET_LOCKTAG_TRANSACTION(locktag,xid) \
((locktag).locktag_field1 = (xid), \
(locktag).locktag_field2 = 0, \
(locktag).locktag_field3 = 0, \
(locktag).locktag_field4 = 0, \
(locktag).locktag_type = LOCKTAG_TRANSACTION, \
(locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD)

如何解决问题

关于全局死锁检测有很多讨论,例如前面提到的 Databases and Distributed Deadlocks: A FAQ[3],其中提供了一些建议,如 Wait-Die 或 Wound-Wait 策略。另一个详细的讨论是 PostgreSQL and Deadlock Detection Spanning Multiple Databases[4],建议使用全局等待图(Global Wait-for-Graph)来检测死锁。

另一种方法是,与其避免问题或使用等待图查找循环,我们也可以考虑使用一个独立的程序,甚至是一个简单的数据库来帮助检查是否存在全局死锁。有时,问题难以解决是因为难以获取所有信息。我们能够轻松看到由一个或多个协调节点引起的死锁,是因为我们退后一步,从整体视角看待问题。

IvorySQL 2025 生态大会暨 PostgreSQL 高峰论坛 预告

2025 年 6 月 27 日,IvorySQL 2025 生态大会暨 PostgreSQL 高峰论坛将在济南盛大开幕,已确认多位全球 PostgreSQL 大咖参与,包括:

  • Bruce Momjian:PG 全球开发小组联合创始人和核心团队成员
  • Cédric Villemain:法国,Data Bene 创始人&CEO
  • Chris Travers:印度尼西亚,PG 和基础设施专家
  • Ivan Blinkov:俄罗斯,YDB 副总裁
  • Michael Meskes:德国,Meskes 董事长(原 Credativ 创始人)
  • Yurii Rashkovskii:加拿大,Omnigres 公司创始人
  • Álvaro Hernández:西班牙,OnGres 创始人

更多重量级嘉宾正在确认中!

无论你是数据库开发者、架构师还是技术爱好者,这都是与全球顶尖专家面对面交流的绝佳机会。6 月 27 日,相约济南 HOW 2025,共同探索 PostgreSQL 的无限可能!

目前,议题征集正在火热进行中!无论您是 PostgreSQL 内核开发者、DBA,还是 PostgreSQL 领域的技术专家,都欢迎来分享您宝贵的技术和经验。

  • 官网:https://howconf.cn/
  • 议题提交直达:https://jsj.top/f/Tr5eXn

引用链接

[1] 

src/backend/storage/lmgr/README: https://github.com/postgres/postgres/blob/master/src/backend/storage/lmgr/README

[2] 

Databases and Distributed Deadlocks: A FAQ: https://www.citusdata.com/blog/2017/08/31/databases-and-distributed-deadlocks-a-faq

[3] 

Databases and Distributed Deadlocks: A FAQ: https://www.citusdata.com/blog/2017/08/31/databases-and-distributed-deadlocks-a-faq

[4] 

PostgreSQL and Deadlock Detection Spanning Multiple Databases: https://www.enterprisedb.com/blog/postgresql-and-deadlock-detection-spanning-multiple-databases

关注公众号,了解更多社区动态

推荐阅读 -

PGConf.dev 2025 圆满落幕!HOW 2025 生态大会即将启航!
全球 PG 大咖相聚泉城!HOW 2025 生态大会邀您提交议题
IvorySQL-WASM:免安装的数据库探索之旅
深入解析 PostgreSQL 外部数据封装器(FDW)的 SELECT 查询执行机制
技术无界·共创未来|PG 全球开发者大会(PGConf.Dev)亮点前瞻
探索表访问方法功能:顺序扫描分析
- 关于 IvorySQL -
IvorySQL 是由瀚高股份主导研发的一款开源的兼容 Oracle 的 PostgreSQL。IvorySQL 与 PostgreSQL 国际社区紧密合作,保持与最新 PG 版本内核同步,为用户提供便捷的升级体验。基于双 Parser 架构设计,100% 与原生 PostgreSQL 兼容,支持丰富的 PostgreSQL 周边工具和扩展,并根据用户需求提供定制化工具。同时,IvorySQL 提供更全面灵活的 Oracle 兼容功能,具备高度的 SQL 和 PL/SQL 兼容性能够为企业构建更加高效、稳定和灵活的数据库解决方案。
官网:https://www.ivorysql.org
GitHub(欢迎点击 star 收藏哦):https://github.com/IvorySQL/IvorySQL
社群:微信搜索“ivorysql_official” 添加小助理进群


【声明】内容源于网络
0
0
IvorySQL开源数据库社区
IvorySQL 是由瀚高开发,基于 PostgreSQL 的一款具备强大 Oracle 兼容能力的开源数据库。紧跟 PG 社区,快速进行版本迭代,保持与最新版本 PG 数据库内核同步,并支持丰富的 PG 周边工具和扩展。
内容 278
粉丝 0
IvorySQL开源数据库社区 IvorySQL 是由瀚高开发,基于 PostgreSQL 的一款具备强大 Oracle 兼容能力的开源数据库。紧跟 PG 社区,快速进行版本迭代,保持与最新版本 PG 数据库内核同步,并支持丰富的 PG 周边工具和扩展。
总阅读6
粉丝0
内容278