分布式DB锁问题排查方法 - 阿里云HybridDB for PostgreSQL最佳实践

本文涉及的产品
RDS PostgreSQL Serverless,0.5-4RCU 50GB 3个月
推荐场景:
对影评进行热评分析
云原生数据库 PolarDB 分布式版,标准版 2核8GB
云数据库 RDS SQL Server,基础系列 2核4GB
简介:

标签

PostgreSQL , Greenplum , 锁 , SEGMENT不一致 , gp_session_role=utility , gp_dist_random


背景

Greenplum(GPDB)是一个分布式数据库,分布式数据库的锁管理比单机更加复杂。例如在加锁时,需要对所有节点加锁(包括MASTER和所有的SEGMENT节点),在释放锁时,则需要释放所有节点的锁。

如果在释放过程中,MASTER的锁释放了,而SEGMENT锁没有释放,会造成什么问题呢?

不用说,会有很诡异的问题出现。例如某个会话锁了某一张表,但是会话退出时,主节点的锁释放了,SEGNEMT节点的锁没有释放。

那么用户在发起新的会话后,如果加载与之冲突的锁,当然要等待了。但是在MASTER节点你观察不到到底它在等待谁,你只能观察到它在等待。是不是很诡异呢?

锁查看方法

当你遇到堵塞时,可以另外开启一个会话查看是谁堵塞了谁?

《PostgreSQL 锁等待监控 珍藏级SQL - 谁堵塞了谁》

with      
t_wait as      
(      
  select a.mode,a.locktype,a.database,a.relation,a.page,a.tuple,a.classid,a.granted,     
  a.objid,a.objsubid,a.pid,a.transactionid,a.mppsessionid,a.mppiswriter,a.gp_segment_id,       
  b.procpid,b.sess_id,b.waiting_reason,b.current_query,b.xact_start,b.query_start,b.usename,b.datname,b.client_addr,b.client_port,b.application_name     
    from pg_locks a,pg_stat_activity b where a.mppsessionid=b.sess_id and not a.granted     
),     
t_run as     
(     
  select a.mode,a.locktype,a.database,a.relation,a.page,a.tuple,a.classid,a.granted,     
  a.objid,a.objsubid,a.pid,a.transactionid,a.mppsessionid,a.mppiswriter,a.gp_segment_id,       
  b.procpid,b.sess_id,b.waiting_reason,b.current_query,b.xact_start,b.query_start,b.usename,b.datname,b.client_addr,b.client_port,b.application_name     
    from pg_locks a,pg_stat_activity b where a.mppsessionid=b.sess_id and a.granted     
),     
t_overlap as     
(     
  select r.* from t_wait w join t_run r on     
  (     
    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.transactionid is not distinct from w.transactionid 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.mppsessionid <> w.mppsessionid     
  )      
),      
t_unionall as      
(      
  select r.* from t_overlap r      
  union all      
  select w.* from t_wait w      
)      
select locktype,datname,relation::regclass,page,tuple,textin(xidout(transactionid)),classid::regclass,objid,objsubid,     
string_agg(     
'Gp_Segment_Id: '||case when gp_segment_id is null then 'NULL' else gp_segment_id::text end||chr(10)||   
'MppIsWriter: '||case when mppiswriter is null then 'NULL' when mppiswriter is true then 'TRUE' else 'FALSE' end||chr(10)||   
'MppSessionId: '||case when mppsessionid is null then 'NULL' else mppsessionid::text end||chr(10)||   
'ProcPid: '||case when procpid is null then 'NULL' else procpid::text end||chr(10)||   
'Pid: '||case when pid is null then 'NULL' else pid::text end||chr(10)||     
'Lock_Granted: '||case when granted is null then 'NULL' when granted is true then 'TRUE' else 'FALSE' end||' , Mode: '||case when mode is null then 'NULL' else mode::text end||' , Waiting_Reason: '||case when waiting_reason is null then 'NULL' else waiting_reason::text end||chr(10)||     
'Username: '||case when usename is null then 'NULL' else usename::text end||' , Database: '||case when datname is null then 'NULL' else datname::text end||' , Client_Addr: '||case when client_addr is null then 'NULL' else client_addr::text end||' , Client_Port: '||case when client_port is null then 'NULL' else client_port::text end||' , Application_Name: '||case when application_name is null then 'NULL' else application_name::text end||chr(10)||      
'Xact_Start: '||case when xact_start is null then 'NULL' else xact_start::text end||' , Query_Start: '||case when query_start is null then 'NULL' else query_start::text end||' , Xact_Elapse: '||case when (now()-xact_start) is null then 'NULL' else (now()-xact_start)::text end||' , Query_Elapse: '||case when (now()-query_start) is null then 'NULL' else (now()-query_start)::text end||chr(10)||      
'SQL (Current SQL in Transaction): '||chr(10)||    
case when current_query is null then 'NULL' else current_query::text end,      
chr(10)||'--------'||chr(10)      
order by      
  (  case 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  ) desc,     
  (case when granted then 0 else 1 end)    
) as lock_conflict    
from t_unionall     
group by     
locktype,datname,relation::regclass,page,tuple,textin(xidout(transactionid)),classid::regclass,objid,objsubid ;    
AI 代码解读

但是本案例通过这个方法,你会发现,只有未赋予的等待,没有已赋予的灵异事件。原因是这个查询没有反馈SEGMENT上的锁等待。查询的是GPDB主节点的pg_locks。

正常情况下通过这种方法很容易排查问题,灵异事件需要特殊对待。

灵异锁等待事件排查手段一 - 通过(gp_dist_random)在主节点发起请求,在所有segment节点单独执行

Greenplum提供了一个函数接口gp_dist_random,当调用这个函数时,会下发到所有segment执行。

gp_dist_random函数的参数是对象名,换句话说说,会在所有segment查询这个对象。在select子句中可以输入一些函数调用,也会下发到SEGMENT节点执行。

我们在GPDB的源码中,可以看到大量gp_dist_random的使用。

进入排查阶段。

假设digoal.test这张表的truncate被堵塞了,通过前面的锁SQL,没有找到堵塞对象。所以我们需要通过gp_dist_random接口,去SEGMENT里面找找锁堵塞的原因。

1、到所有segment执行,找到堵塞digoal.test的QUERY。

SQL如下,发现有大量的copy to stdou的查询,看样子是用户断开了master节点的COPY操作,但是SEGMENT节点的COPY还在继续。并且这个事务是2天前发起的,期间还不知道锁了多少其他对象呢。它就是堵塞digoal.test的罪魁祸首。

digoal=# select gp_execution_dbid(),   -- 返回segment的dbid,对应gp_segment_configuration.dbid里可以得到SEGMENT。  
        inet_server_addr(),  -- 这个并不是segment IP,这个函数没有下推  
        inet_server_port(),  -- 这个并不是segment PORT,这个函数没有下推  
        *    
      from gp_dist_random('pg_stat_activity')   -- 查询pg_stat_activity视图  
      where procpid in   
        ( select pid from gp_dist_random('pg_locks')   -- 查询pg_locks视图,并找到锁digoal.test的PID  
	     where relation='digoal.test'::regclass  
	);   
  
结果如下:  
  
-[ RECORD 1 ]----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
datid            | 17159  
datname          | digoal  
procpid          | 39312  
sess_id          | 80714  
usesysid         | 10  
usename          | digoal_user  
current_query    | COPY digoal.test_1_prt_p20170819 (xxx,xxx,xxx....) TO stdout IGNORE EXTERNAL PARTITIONS;  
waiting          | f  
query_start      | 2017-08-22 12:32:14.674691+08  
backend_start    | 2017-08-20 22:06:03.1238+08  
client_addr      |   
client_port      | -1  
application_name |   
xact_start       | 2017-08-20 22:06:03.129544+08  
waiting_reason   |   
.........  
-[ RECORD 8 ]----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
datid            | 17159  
datname          | digoal  
procpid          | 52074  
sess_id          | 80714  
usesysid         | 10  
usename          | digoal_user  
current_query    | COPY dw.t_zhuanpan_1_prt_p20170214 (xxx,xxx,xxx....) TO stdout IGNORE EXTERNAL PARTITIONS;  
waiting          | f  
query_start      | 2017-08-22 12:30:03.907998+08  
backend_start    | 2017-08-20 22:06:03.134764+08  
client_addr      |   
client_port      | -1  
application_name |   
xact_start       | 2017-08-20 22:06:03.157087+08  
waiting_reason   |   
AI 代码解读

2、查看SEGMENT配置,通过dbid字段和gp_execution_dbid可以匹配到对应的SEGMENT。

digoal=# select * from gp_segment_configuration where role='p';  
 dbid | content | role | preferred_role | mode | status | port |       hostname       |       address        | replication_port | san_mounts   
------+---------+------+----------------+------+--------+------+----------------------+----------------------+------------------+------------  
    1 |      -1 | p    | p              | s    | u      | xxxx | xxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxx |                  |   
    2 |       0 | p    | p              | s    | u      | xxxx | xxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxx |             xxxx |   
    3 |       1 | p    | p              | s    | u      | xxxx | xxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxx |             xxxx |   
    4 |       2 | p    | p              | s    | u      | xxxx | xxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxx |             xxxx |   
    5 |       3 | p    | p              | s    | u      | xxxx | xxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxx |             xxxx |   
    6 |       4 | p    | p              | s    | u      | xxxx | xxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxx |             xxxx |   
    7 |       5 | p    | p              | s    | u      | xxxx | xxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxx |             xxxx |   
    8 |       6 | p    | p              | s    | u      | xxxx | xxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxx |             xxxx |   
    9 |       7 | p    | p              | s    | u      | xxxx | xxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxx |             xxxx |   
(9 rows)  
AI 代码解读

3、观察一下这些PID都锁了哪些对象。

select gp_execution_dbid(), pid, relation::regclass, locktype, mode, granted  
  from gp_dist_random('pg_locks')   
  where pid in   
    (select pid from gp_dist_random('pg_locks') where relation='digoal.test'::regclass);  
AI 代码解读

4、杀死这些SEGMENT上的PROCPID。

注意,数据会重分布到所有节点后再执行pg_terminate_backend,所以pid很可能被重分布到另一个SEGMENT,然后调用pg_terminate_backend,因为PID不是当前SEGMENT的PID,所以根本杀不掉。

所以手段一仅仅适合排查问题,不能解决问题。

select gp_execution_dbid() dbid,   
       pg_terminate_backend(procpid)  -- 杀杀杀,但是数据会重分布到所有节点执行,因此不是当前节点的procpid也会被pg_terminate_backend  
from gp_dist_random('pg_stat_activity')   
where procpid in   
  (select pid from gp_dist_random('pg_locks') where relation='digoal.test'::regclass and granted);  
  
WARNING:  PID 77961 is not a PostgreSQL server process  (seg21 slice3 xxxxxxxxx:25453 pid=128069)  
 dbid | pg_terminate_backend   
------+----------------------  
   23 | f  
(1 row)  
AI 代码解读

灵异锁等待事件排查手段二 - 使用(gp_session_role=utility)直接连接SEGMENT

segment节点是不能直接连接的,需要设置一个参数,就可以连接了。

手段一帮助我们找到了持锁的segment,手段二则登陆对应的主机,直连segment去terminate对应的process。

PGOPTIONS="-c gp_session_role=utility" psql -d dbname -h hostname -p port -U user  
AI 代码解读

直连到SEGMENT后,可以在这里看到持锁的SQL,这个SQL是COY TO STDOU,显然是MASTER中断这个SQL后,而segment没有中断造成的。属于GPDB本身的BUG,需要修复。

digoal=# \x  
Expanded display is on.  
digoal=# select array_agg(' usename: '||usename||' datname: '||datname||' current_query: '||coalesce(current_query,'')||' xact_start: '||coalesce(xact_start,'1970-01-01')||' backend_start: '||coalesce(backend_start,'1970-01-01')) from pg_stat_activity where procpid in (select pid from pg_locks where relation='digoal.test'::regclass);  
-[ RECORD 1 ]-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
array_agg | {" usename: digoal_user datname: digoal current_query: COPY digoal.test_1_prt_p20170213 (xxxx,xxxx......) TO stdout IGNORE EXTERNAL PARTITIONS; xact_start: 2017-08-20 22:06:03.157087+08 backend_start: 2017-08-20 22:06:03.134764+08"}  
  
  
postgres=# select usename,datname,xact_start,current_query from pg_stat_activity ;  
  usename  | datname  |          xact_start           |                                                                                             current_query                                                                              
                   
-----------+----------+-------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
-----------------  
 digoal_user | digoal      |                               | <IDLE>  
 digoal_user | digoal      | 2017-08-20 22:06:03.157087+08 | COPY digoal.test_1_prt_p20170203 (xxx,xxx,......) TO stdout IGNORE EXTE  
RNAL PARTITIONS;  
 digoal_user | digoal      |                               | <IDLE>  
 aurora    | postgres | 2017-08-22 11:59:43.652306+08 | select usename,datname,xact_start,current_query from pg_stat_activity ;  
 dw        | digoal      | 2017-08-22 11:59:37.1828+08   | INSERT INTO digoal.test\r                                                                                                                                                             
                   
                                                      : SELECT  * from xxx.xxx017082222115801;  
 dw        | digoal      |                               | <IDLE>  
(6 rows)  
AI 代码解读

杀掉所有segment的持锁会话后,恢复业务。

postgres=# select pg_terminate_backend(77961);  
 pg_terminate_backend   
----------------------  
 t  
(1 row)  
AI 代码解读

参考

《PostgreSQL 锁等待监控 珍藏级SQL - 谁堵塞了谁》

PGOPTIONS="-c gp_session_role=utility" psql -d dbname -h hostname -p port -U user  
AI 代码解读

https://www.postgresql.org/docs/8.2/static/runtime-config-developer.html

《Greenplum通过gp_dist_random('gp_id') 在所有节点调用某个函数》

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
打赏
0
0
0
0
20696
分享
相关文章
ICLR 2025 | EDiT:一种基于 Local SGD 策略的大模型高效分布式训练方法
蚂蚁 AI Infra 团队在深度学习最核心之一的训练框架方向上持续投入与创新,实现了提升资源利用率、加速训练、提升训练稳定性等目标。我们提出的 EDiT 方法,即为其中一项工作。
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
1月前
|
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
Redisson 的看门狗机制是解决分布式锁续期问题的核心功能。当通过 `lock()` 方法加锁且未指定租约时间时,默认启用 30 秒的看门狗超时时间。其原理是在获取锁后创建一个定时任务,每隔 1/3 超时时间(默认 10 秒)通过 Lua 脚本检查锁状态并延长过期时间。续期操作异步执行,确保业务线程不被阻塞,同时仅当前持有锁的线程可成功续期。锁释放时自动清理看门狗任务,避免资源浪费。学习源码后需注意:避免使用带超时参数的加锁方法、控制业务执行时间、及时释放锁以优化性能。相比手动循环续期,Redisson 的定时任务方式更高效且安全。
108 24
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
|
1月前
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。
89 11
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
|
4天前
|
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。
20 0
|
1月前
|
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
本文详细解析了Redisson可重入锁的加锁流程。首先从`RLock.lock()`方法入手,通过获取当前线程ID并调用`tryAcquire`尝试加锁。若加锁失败,则订阅锁释放通知并循环重试。核心逻辑由Lua脚本实现:检查锁是否存在,若不存在则创建并设置重入次数为1;若存在且为当前线程持有,则重入次数+1。否则返回锁的剩余过期时间。此过程展示了Redisson高效、可靠的分布式锁机制。
74 0
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
【📕分布式锁通关指南 01】从解决库存超卖开始加锁的初体验
本文通过电商场景中的库存超卖问题,深入探讨了JVM锁、MySQL悲观锁和乐观锁的实现及其局限性。首先介绍了单次访问下库存扣减逻辑的正常运行,但在高并发场景下出现了超卖问题。接着分析了JVM锁在多例模式、事务模式和集群模式下的失效情况,并提出了使用数据库锁机制(如悲观锁和乐观锁)来解决并发问题。 悲观锁通过`update`语句或`select for update`实现,能有效防止超卖,但存在锁范围过大、性能差等问题。乐观锁则通过版本号或时间戳实现,适合读多写少的场景,但也面临高并发写操作性能低和ABA问题。 最终,文章强调没有完美的方案,只有根据具体业务场景选择合适的锁机制。
100 12
【📕分布式锁通关指南 01】从解决库存超卖开始加锁的初体验
秒杀抢购场景下实战JVM级别锁与分布式锁
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。 为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
147 10
解决分布式系统演进过程中数据一致性问题的方法
【10月更文挑战第24天】解决分布式系统演进过程中数据一致性问题是一个复杂而又重要的任务。需要综合运用多种方法和技术,根据具体的系统需求和场景,选择合适的解决方案。同时,不断地进行优化和改进,以适应不断变化的分布式系统环境。
224 47
分布式读写锁的奥义:上古世代 ZooKeeper 的进击
本文作者将介绍女娲对社区 ZooKeeper 在分布式读写锁实践细节上的思考,希望帮助大家理解分布式读写锁背后的原理。
154 11

热门文章

最新文章

相关产品

  • 云原生数据库 PolarDB
  • 云数据库 RDS PostgreSQL 版
  • AI助理

    你好,我是AI助理

    可以解答问题、推荐解决方案等