GBase 8c
其他
文章
精选

GBase 8c 里一条 SQL 卡半天,我排查锁等待时通常先盯这几个地方

发表于2026-04-01 10:46:13266次浏览4个评论

GBase 8c 锁等待问题完整排查与处理实践

我最近看 GBase 8c 这块资料时,越来越觉得锁等待特别容易被误判。现场里最常见的说法是"数据库卡住了"或者"这条 SQL 性能太差",但真正落到排查动作上,很多时候根子不是执行计划本身,而是事务没提交、DDL 和 DML 撞到一起、批量过程在某个 DN 上卡住、或者分布式场景里已经出现了跨节点等待。

GBase 8c 本身有 MVCC、常规锁、全局死锁解除这些机制,读写不冲突不代表所有操作都不会互相挡住,尤其是 DDL、批量更新、长事务、存储过程这几类场景,很容易把问题拖得很隐蔽。

我自己现在碰到"SQL 一直不返回"这类问题,通常不会上来就改参数,也不会先怀疑机器不够。更实际一点的做法是先把现场拆成三件事:

  1. 当前会话是不是在等锁。
  2. 阻塞源到底是谁,是活跃 SQL 还是 idle in transaction。
  3. 是单节点局部问题,还是已经变成分布式阻塞链。

如果这三个问题没先看清,后面不管是终止会话、调超时还是改程序提交策略,动作都很容易打偏。

我一般先把现场现象分成这几类

现场现象 我优先怀疑的点 第一动作
UPDATE、DELETE 执行很久没返回 行级锁 / 表级锁冲突 看 pg_stat_activity、pg_locks
TRUNCATE、ALTER TABLE 一直挂起 DDL 被未提交事务挡住 先找持锁会话
存储过程跑着跑着不动了 某个 DN 上具体步骤被卡住 先查全局事务号,再到 DN 定位
会话状态不是 active,但别人都被堵住 idle in transaction 长事务 先看事务开始时间和最后一条 SQL
过一段时间直接报超时 lockwait_timeout 命中 查等待链,再判断要不要调超时

我最近整理下来觉得,这种拆法很适合现场。因为锁问题最怕"只盯当前报错 SQL",而不看前面那个真正拿着锁的人。

一、先判断是不是锁等待,不要先把锅甩给 SQL 性能

GBase 8c 的 pg_stat_activity 里能直接看到会话状态,waiting 字段可以反映后台当前是否在等待锁,state 里还能区分 active、idle、idle in transaction、idle in transaction (aborted) 这些状态。track_activities 默认就是开启的,关闭时很多排查视图会看不到有效内容,所以我现场里基本都会先确认它没被关掉。

我自己常用的第一组 SQL 很简单:

SELECT pid,
       usename,
       application_name,
       client_addr,
       xact_start,
       query_start,
       state_change,
       waiting,
       state,
       query
FROM pg_stat_activity
ORDER BY xact_start NULLS LAST, query_start NULLS LAST;

如果现场已经有人反馈"某张表一操作就卡",我通常会再缩一下范围:

SELECT pid,
       usename,
       state,
       waiting,
       xact_start,
       query_start,
       query
FROM pg_stat_activity
WHERE query LIKE '%orders_fact%'
ORDER BY query_start;

这个阶段我更关注两个点:

  • 有没有会话处在 idle in transaction 这个状态特别值得盯。因为它表面上看起来不像在执行 SQL,但事务其实没结束,锁资源还在占着。GBase 社区的锁故障定位案例里,就专门提到过这种"SQL 已经跑完,但事务没提交,结果把后面的 DML/DDL 都挡住"的情况。
  • 当前等待的是不是锁,不是别的资源 别一看到慢就默认是锁。有时候是 I/O、网络、并发队列,但 waiting=true 加上后续锁视图能对上,基本就能确定方向。

二、锁来源不要靠猜,直接从 pg_locks 往回找

我自己理解下来,GBase 8c 里排锁问题,最稳的还是 pg_stat_activity + pg_locks + pg_class + pg_namespace 这组组合。社区里给过一个比较实用的思路:如果某个表上的 DML/DDL 执行超时或者长时间不返回,可以直接按 schema 和表名去反查当前有哪些会话在持锁。

比如我现场里常写成这样:

SELECT a.pid,
       a.usename,
       a.application_name,
       a.client_addr,
       a.state,
       a.waiting,
       a.xact_start,
       a.query_start,
       l.mode,
       l.granted,
       n.nspname,
       c.relname,
       a.query
FROM pg_namespace n
JOIN pg_class c
  ON c.relnamespace = n.oid
JOIN pg_locks l
  ON l.relation = c.oid
JOIN pg_stat_activity a
  ON a.pid = l.pid
WHERE n.nspname = 'sales'
  AND c.relname = 'orders_fact'
ORDER BY l.granted DESC, a.xact_start;

这条 SQL 的好处是比较直接,适合"我已经知道是哪张表有问题"的场景。

如果是"我不知道谁堵谁",我更常用下面这种阻塞链查法。开发者手册里有一条现成思路:把等待会话和持锁会话按 pg_locks 关联起来,再把表名也带出来,这样能直接看到谁在等、谁在挡、挡的是哪张表。

SELECT w.query   AS waiting_query,
       w.pid     AS waiting_pid,
       w.usename AS waiting_user,
       l.query   AS locking_query,
       l.pid     AS locking_pid,
       l.usename AS locking_user,
       t.schemaname || '.' || t.relname AS table_name
FROM pg_stat_activity w
JOIN pg_locks l1
  ON w.pid = l1.pid
 AND NOT l1.granted
JOIN pg_locks l2
  ON l1.relation = l2.relation
 AND l2.granted
JOIN pg_stat_activity l
  ON l2.pid = l.pid
JOIN pg_stat_user_tables t
  ON l1.relation = t.relid
WHERE w.waiting;

这类查询在现场特别有价值,因为它不是只告诉你"有人在等",而是把阻塞源和对象一起带出来。真正落到处理顺序上,先找到源头会话,比一条条取消等待会话有效得多。

三、GBase 8c 分布式场景下,别只盯当前 CN

这个点我自己比较在意。单机数据库里的锁等待,很多时候在一个实例里就能闭环排掉;但 GBase 8c 是分布式架构,锁可能来自别的 CN 或 DN。社区的锁故障定位文章里就专门提醒过:分布式场景下,因为有多个 CN 和 DN,锁有可能由其他节点产生,所以排查时不能只看一个节点;而且如果是批量语句或者存储过程,还需要通过全局事务号继续往 DN 上追。

我最近整理下来比较顺手的一套动作是这样:

1) 先在 CN 找到当前会话

SELECT pid,
       usename,
       application_name,
       client_addr,
       query_start,
       state,
       query
FROM pg_stat_activity
WHERE query LIKE '%call proc_merge_orders%'
ORDER BY query_start DESC;

2) 拿这个 PID 去 pg_locks 找全局事务号

SELECT pid,
       sessionid,
       global_sessionid,
       mode,
       granted,
       locktag
FROM pg_locks
WHERE pid = 140512348217104;

社区资料里提到,在分布式模式下,global_sessionid 里会带节点信息,继续定位时要取其中的事务号部分,再到 DN 上去查。

3) 登录相关 DN,用全局事务号继续追

SELECT pid,
       sessionid,
       global_sessionid,
       mode,
       granted,
       locktag
FROM pg_locks
WHERE global_sessionid LIKE '%818%';

4) 再回到 DN 上看具体执行到了哪一步

SELECT pid,
       usename,
       state,
       waiting,
       xact_start,
       query_start,
       query
FROM pg_stat_activity
WHERE pid = 140512314560272;

这套动作对"存储过程很多步,不知道卡在哪一步"特别有用。因为 CN 上你看到的往往只是外层调用,真正卡住的那条 DML 很可能已经下发到某个 DN 上了。

四、我排锁问题时,通常会把下面几个状态单独拎出来看

状态 / 现象 我更倾向的判断 处理优先级
active 且 waiting=true 正在申请锁,但没拿到 先找阻塞源
idle in transaction 事务没提交,锁可能还占着 很高
idle in transaction (aborted) 事务里有语句失败,但会话没结束 很高
DDL 一直挂起 常见是被未提交 DML 或其他 DDL 挡住 很高
批处理 / 存储过程卡住 常见要跨 CN、DN 联动排查 中高

这里面我最怕的是 idle in transaction。因为业务侧经常会说"那条 SQL 明明执行完了",但数据库这边看,事务没提交就还是没完。GBase 8c 对 pg_stat_activity 的状态定义里也明确列出了这些状态差异,所以我现在看会话时,state 往往比 query 本身更有提示意义。

五、超时参数别乱拧,但要知道它们分别管什么

很多现场排到最后,会有人问一句:"要不要把锁等待时间改短一点?"

我自己的想法是,可以调,但得先分清这几个参数到底各管什么,不然很容易把症状压住,问题根源没解决。

根据开发者手册里的参数说明:

  • deadlock_timeout 默认值是 1s,它关系到死锁检测启动时机;如果 log_lock_waits=on,它也决定锁等待信息什么时候写日志。
  • lockwait_timeout 默认值是 20min,控制单个锁最长等待时间,超过就报错。
  • statement_timeout 控制语句总执行时长,和单纯锁等待不是一回事。

我更愿意这样理解:

参数 我更常把它用在什么场景 我对它的看法
deadlock_timeout 想更快发现锁等待/死锁迹象、配合日志分析 适合排障期观察
lockwait_timeout 不想让业务无限等锁 适合做保护边界
statement_timeout 防止单条 SQL 执行太久 别拿它代替锁治理

现场里如果只是临时排查,我一般先做会话级设置,不急着全局改:

SHOW deadlock_timeout;
SHOW lockwait_timeout;
SHOW statement_timeout;
SHOW log_lock_waits;

SET deadlock_timeout = '500ms';
SET lockwait_timeout = '60s';
SET statement_timeout = '10min';
SET log_lock_waits = ON;

这里我个人更倾向于把它当成"排障辅助"和"保护机制",而不是根治办法。真正的根因,通常还是长事务、提交边界不清晰、批量作业和在线业务没隔离好。

六、处理动作别太猛,先取消查询,再考虑终止会话

GBase 8c 提供了 pg_cancel_backend(pid) 和 pg_terminate_backend(pid)。社区文章和相关函数说明里提到,前者是取消当前查询,后者是直接终止会话;而且 pg_cancel_backend 更适合先做温和处理,pg_terminate_backend 适合确定可以中断、并且需要立即释放事务资源的场景。

我现场里一般按这个顺序来:

-- 先尝试取消当前查询
SELECT pg_cancel_backend(140512348217104);

-- 如果取消不了,且确认允许中断,再终止会话
SELECT pg_terminate_backend(140512348217104);

我自己更愿意给团队定一个简单原则:

  1. 先确认这是不是业务关键会话。
  2. 先取消 active 查询,不要上来就 terminate。
  3. 如果是 idle in transaction 且已经明确阻塞别人,再考虑 terminate。
  4. 终止后要复查锁是不是已经释放,别以为一 kill 就完了。

这个顺序看起来保守一点,但生产环境里我觉得更稳。因为锁问题往往不是"结束一个 PID 就彻底结束",有时只是把第一层等待打掉,后面还有第二层链路。

七、我更习惯的一套排查顺序

到现在为止,我自己用得最多的处理顺序差不多是下面这样:

第一步:看当前会话面

先扫 pg_stat_activity,判断是不是锁等待、是不是 idle in transaction、事务起始时间大概多久。

第二步:看对象面

如果知道是某张表卡住,就按表名去查 pg_locks,把持锁和等待锁的会话一起捞出来。

第三步:看阻塞链

用等待会话和持锁会话关联的 SQL 找出"谁在挡谁"。我现在很少只看等待方,因为真正要处理的是源头阻塞者。

第四步:判断是不是跨节点问题

如果是存储过程、批处理、分布式业务请求,就继续用 global_sessionid 往 DN 上追。

第五步:再决定是调参数还是动会话

短期排障可以调 deadlock_timeout、lockwait_timeout、log_lock_waits;必须马上恢复业务时,再用 pg_cancel_backend 或 pg_terminate_backend。

八、几个特别容易踩的坑,我一般会提前提醒

1) 把"查询慢"和"被锁住"混成一件事

有些 SQL 确实执行计划差,但锁等待的现场特征很明显:别人先拿着资源,你只能干等。这个时候去改索引、改 Hint,通常不在点上。

2) 以为 SQL 执行完了,锁就自动没了

如果事务没提交,锁就还在。idle in transaction 这类会话,现场里真的太常见了。

3) 只在一个 CN 上查

GBase 8c 是分布式架构,锁可能来自别的节点。只看当前连接节点,很容易误以为"查不到就是没有"。

4) 只会杀等待方,不处理阻塞源

等锁的会话杀掉一个,后面还会再来。真正要处理的是那个最先拿着锁又不释放的人。

5) 把超时参数当成根治方案

lockwait_timeout 调短了,只是让报错更早;statement_timeout 调短了,只是让失败更快。业务代码的提交边界、事务范围、批处理窗口、DDL 执行时机,这些不整理,锁问题还是会回来。

九、我现在更认可的落地建议

最后收一下。我最近重新整理 GBase 8c 事务和锁这块内容时,感觉最值得坚持的一点不是记住多少锁模式,而是排查顺序要稳定。锁冲突这类问题最怕靠经验拍脑袋,今天怀疑 SQL,明天怀疑机器,后天又去调全局参数,最后还是没把真正挡路的会话找出来。

从落地角度看,我更倾向于把这类问题当成一条完整链路来处理:

  • 先确认是不是锁等待;
  • 再找到谁持锁、谁在等;
  • 再确认是不是跨节点阻塞;
  • 最后才决定是取消查询、终止会话,还是调整超时和开发侧事务边界。

这个方法不花哨,但我自己觉得很适合 GBase 8c 这种分布式场景。因为你只要把阻塞链找对了,后面的动作其实都不复杂;最怕的是根本没找到那个真正拿着锁不放的人。

参考资料

  1. 南大通用GBase 8c分布式问题排查之锁故障定位
    https://www.gbase.cn/community/post/4964
  2. GBase 8c如何解除全局死锁
    https://www.gbase.cn/community/post/2441
  3. 南大通用GBase 8c事务与锁之全局死锁解除
    https://www.gbase.cn/community/post/3883
  4. GBase 8c 开发者手册
    https://cdn.gbase.cn/products/34/Pr3MGDrcOeeWwC9kdNEjM/GBase%208c%20V5_3.0.1_%E5%BC%80%E5%8F%91%E8%80%85%E6%89%8B%E5%86%8C_V1.1_20240119.pdf
  5. GBase 8c V3.0.0数据类型——服务器信号函数
    https://www.gbase.cn/community/post/2270

评论

登录后才可以发表评论
罗小胖发表于 6个月前
这是8c数据库,不是8a的
用户头像
山佳发表于 6个月前
111
GBase用户47954发表于 5个月前
感谢作者的精彩分享!
流泪猫猫头发表于 3个月前
很详细实用的文章