我把 GBase 8c 锁等待和死锁排查重新捋了一遍:从阻塞链、全局事务到现场止血的一套实战方法
做数据库运维和问题排查的时候,我最近越来越明显地感觉到一个现象:很多“性能问题”表面看像 SQL 变慢,真正落到现场时,根因其实是锁等待、阻塞链或者分布式死锁。
这种问题麻烦的地方不在于原理难,而在于它特别容易误判。业务侧看到的是“更新卡住了”“truncate 一直不返回”“存储过程执行很久没结束”;开发侧第一反应常常是 SQL 写得不好;运维侧如果没有顺着会话、锁、全局事务号这条线往下看,很容易在 CPU、IO、执行计划上绕半天,最后才发现是前面有个会话一直没提交。
我最近看 GBase 8c 相关资料时,一个体会特别深:GBase 8c 这类分布式数据库,锁问题不能只按单机数据库的思路查。 因为它不仅有本地锁等待,还有跨 CN、DN 的全局锁等待和全局死锁。公开社区资料里提到,GBase 8c 在分布式场景下既要处理本地事务并发控制,也要处理跨节点的全局等待关系;锁问题排查通常会结合 pg_stat_activity、pg_locks、pg_class、pg_namespace 这几类系统视图来做。
从落地角度看,这个方向很适合写成一篇实战型长文。因为它和“慢 SQL 优化”不是一回事,和“统计信息/执行计划调优”也不是一回事。真正到了生产现场,锁冲突问题更像是一类并发故障:要先止血,再定位,再收口,再补规范。
一、我自己更倾向于先把锁问题分成三类,不然排查会越查越乱
很多人一看到业务报“卡住”,就直接去找最慢 SQL。我自己现在不太这么做了。因为锁问题如果不先分型,后面的排查动作会很散。
1. 普通锁等待:有前挡车,后面全跟着堵
这是最常见的一类。前面某个会话持有锁没有释放,后面的 DML、DDL 或者批量作业开始排队。
典型现象包括:
UPDATE、DELETE执行很久不返回TRUNCATE、ALTER TABLE一直等- 存储过程跑到某一步突然卡住
- 业务端连接没断,但请求长时间悬挂
这类问题往往先从阻塞源会话入手最有效。
2. 阻塞链:不是一个会话挡住你,而是一串会话互相卡住
现场里我见过不少这种情况。A 锁住对象,B 等 A,C 等 B,D 又等 C。最后业务看到的是很多 SQL 都“慢”,但真正的问题只在链头。
这个时候如果只看最后那个受害 SQL,很容易看偏。真正有价值的是先找到谁是最前面的 blocker。
3. 全局死锁:单节点思路不够了
GBase 8c 的公开资料里提到,死锁既可能发生在单个 CN 或单个 DN,也可能发生在多个 CN、DN 之间,形成全局死锁;而 GBase 8c 支持分布式场景下的全局死锁检测与解除机制。
这类问题和单机数据库不一样的地方在于:
你在一个节点上看到的是等待,但真正形成环的另一段,可能在别的 CN 或 DN 上。
二、我自己排查时,先看这几种现场信号,基本能很快判断是不是锁问题
真正落到现场时,我通常先看几个最直接的信号,而不是先跑一堆复杂脚本。
1. 会话状态里有没有 idle in transaction
这个状态我个人很关注。因为很多锁问题,不是 SQL 本身还在执行,而是语句已经执行完了,但事务没提交,锁还捏在手里。
社区公开案例里就提到,某些会话执行完 update 后处于 idle in transaction 状态,会导致后续 DML/DDL 持续等待。
先看活跃会话:
SELECT
pid,
usename,
application_name,
client_addr,
state,
wait_event_type,
wait_event,
xact_start,
query_start,
now() - query_start AS running_time,
query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;
这一步我通常会先盯这几列:
| 观察项 | 我更关注什么 | 典型含义 |
|---|---|---|
state | active / idle in transaction | 是否执行中,还是事务悬空 |
wait_event_type | Lock | 是否明确在等锁 |
xact_start | 事务开始时间是否过久 | 长事务风险 |
query_start | 当前 SQL 卡了多久 | 当前受害语句时长 |
2. 先看是不是表级强锁把业务挡住了
在实际环境里,TRUNCATE、部分 DDL、批量变更脚本更容易引发强锁等待。公开巡检资料里就给过一个比较直接的思路:从 pg_locks、pg_class、pg_namespace、pg_stat_activity 联查,重点看 AccessExclusiveLock 这类强锁。
我平时会先跑一个简化版:
SELECT
n.nspname AS schema_name,
c.relname AS table_name,
l.pid,
l.locktype,
l.mode,
l.granted,
a.usename,
a.state,
now() - a.state_change AS lock_duration,
a.query
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_locks l ON l.relation = c.oid
LEFT JOIN pg_stat_activity a ON a.pid = l.pid
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY lock_duration DESC NULLS LAST;
如果这里已经看到明显的长时间持锁会话,后面的问题大概率就有方向了。
三、我更喜欢顺着“阻塞链”查,而不是盯着最后卡住的 SQL
到现场以后,一个比较实用的思路是:
不要先问“谁在慢”,先问“谁把别人挡住了”。
1. 先把被阻塞会话和持锁会话串起来
可以先找没有拿到锁、仍在等待的会话:
SELECT
w.pid AS waiting_pid,
wa.usename AS waiting_user,
wa.client_addr AS waiting_client,
wa.state AS waiting_state,
wa.query AS waiting_sql,
w.locktype,
w.mode AS waiting_mode,
w.relation,
b.pid AS blocking_pid,
ba.usename AS blocking_user,
ba.state AS blocking_state,
ba.query AS blocking_sql,
b.mode AS blocking_mode
FROM pg_locks w
JOIN pg_stat_activity wa ON wa.pid = w.pid
JOIN pg_locks b
ON w.locktype = b.locktype
AND w.database IS NOT DISTINCT FROM b.database
AND w.relation IS NOT DISTINCT FROM b.relation
AND w.page IS NOT DISTINCT FROM b.page
AND w.tuple IS NOT DISTINCT FROM b.tuple
AND w.virtualxid IS NOT DISTINCT FROM b.virtualxid
AND w.transactionid IS NOT DISTINCT FROM b.transactionid
AND w.classid IS NOT DISTINCT FROM b.classid
AND w.objid IS NOT DISTINCT FROM b.objid
AND w.objsubid IS NOT DISTINCT FROM b.objsubid
AND w.pid <> b.pid
JOIN pg_stat_activity ba ON ba.pid = b.pid
WHERE NOT w.granted
AND b.granted;
这类 SQL 的价值不在于写得多复杂,而在于它能把受害者和阻塞者直接对应起来。
2. 真正处理时,优先级一般这样排
我自己更倾向于按下面这个顺序判断:
| 排查优先级 | 先看什么 | 为什么 |
|---|---|---|
| P1 | 是否有明确 blocker | 先找到链头,效率最高 |
| P2 | blocker 是否 idle in transaction | 最常见、最值得优先处理 |
| P3 | 锁住的是表还是行/事务级对象 | 决定影响范围 |
| P4 | 是否涉及 DDL 或批量脚本 | 这类问题扩散更快 |
| P5 | 是否跨 CN / DN | 决定要不要继续追全局事务号 |
很多时候,前面看起来像“几十条 SQL 同时变慢”,实际上只是一个长事务没提交。
四、GBase 8c 这类分布式场景里,我会特别留意 global_sessionid
这个点我最近整理资料时印象很深。因为它很符合 GBase 8c 的排障特点:从 CN 看到一个会话卡住,不代表你已经看到完整现场。
社区公开资料提到,在分布式模式下,可以先在 CN 上通过 pg_stat_activity 找到会话和语句,再到 pg_locks 里取 global_sessionid;然后再去 DN 上按这个全局事务号继续查对应的 PID 和正在执行的具体步骤。
1. 在 CN 上先定位业务会话
SELECT
pid,
usename,
client_addr,
state,
query_start,
query
FROM pg_stat_activity
WHERE query LIKE '%order_settle_proc%'
ORDER BY query_start;
2. 再查全局事务号
SELECT
pid,
global_sessionid,
locktype,
mode,
granted
FROM pg_locks
WHERE pid = 123456;
如果拿到类似这样的值:
0:818#0
我自己理解下来,这类标识在分布式排查里非常关键。因为后面去 DN 节点继续追,就不是按 CN 的 PID 查了,而是按这个全局事务号里的核心编号去关联。社区资料里也专门提到过这种取法。
3. 到 DN 上继续找真实执行点
SELECT
pid,
global_sessionid,
mode,
granted
FROM pg_locks
WHERE global_sessionid LIKE '%818%';
SELECT
pid,
state,
wait_event_type,
wait_event,
query
FROM pg_stat_activity
WHERE pid = 987654;
这个动作在批处理、复杂存储过程、跨节点写入冲突场景里特别有用。因为 CN 上很多时候你只能看到“这个过程还在跑”,但真正卡在哪一步,要到 DN 才能看清。
五、死锁问题别只想着“调参数”,先把形成环的业务模式找出来
1. GBase 8c 的全局死锁,和普通等待不是一回事
公开资料里提到,GBase 8c 支持全局死锁检测与自动解除,死锁既可能发生在单节点,也可能发生在多个节点之间;在分布式场景下,等待关系会沿节点之间传播,最终发现等待环后解除死锁。
我自己更倾向于这样理解:
锁等待是“前面有人挡路”,死锁是“大家互相挡路,谁也过不去”。
2. 一个很典型的双向更新场景
-- session 1
BEGIN;
UPDATE acct_balance SET amount = amount - 100 WHERE acct_id = 101;
-- session 2
BEGIN;
UPDATE acct_balance SET amount = amount - 50 WHERE acct_id = 202;
-- session 1
UPDATE acct_balance SET amount = amount + 100 WHERE acct_id = 202;
-- session 2
UPDATE acct_balance SET amount = amount + 50 WHERE acct_id = 101;
这类场景最典型的问题就是:
两个事务更新顺序不一致,最后互相等对方释放锁。
3. deadlock_timeout 该怎么看
公开社区文章给过一个示例,deadlock_timeout 可以查看和调整,用来影响死锁检测等待时机。
SHOW deadlock_timeout;
ALTER SYSTEM SET deadlock_timeout = 20;
但我个人不太倾向把它当成主要优化手段。因为真正落地时,这个参数更像是检测节奏的控制项,不是解决根因的钥匙。根因一般还是下面这些:
- 同一批业务对象更新顺序不一致
- 事务范围过大
- 一个事务里既做查询又做多步更新
- 批量任务和在线交易抢同一批热点行
六、从现场经验看,最容易引发锁故障的几种业务写法
1. 长事务不提交
最常见,也最容易被忽略。
BEGIN;
UPDATE order_hdr SET status = 'DONE' WHERE order_id = 9001001;
/* 中间应用层又去调别的接口,迟迟不提交 */
这种写法的问题不是 SQL 本身,而是事务边界拖得太长。
2. DDL 和在线 DML 混跑
比如白天业务高峰期直接跑:
TRUNCATE TABLE ods_trade_tmp;
ALTER TABLE fact_order ADD COLUMN ext_flag int;
这类操作很容易拿到更强的锁,影响面通常比普通 DML 大得多。
3. 存储过程里步骤过多、事务包得过大
这个问题在批量业务里特别常见。一个过程里连续更新多张表,还夹着中间计算和校验,最终锁持有时间被拉长。
4. 业务对象更新顺序不统一
这个是死锁高发点。比如有的服务先更新订单头再更新明细,另一个服务反过来,冲突出现只是时间问题。
下面这张表,是我比较常用的归纳方式:
| 场景 | 容易出现的问题 | 我更倾向的处理方式 |
|---|---|---|
| 长事务 | 锁长期不释放 | 缩短事务边界,尽早提交 |
| DDL 混跑 | 表级强锁放大影响 | 错峰执行,单独窗口变更 |
| 大存储过程 | 中间步骤难定位 | 拆分步骤,结合 DN 排查 |
| 更新顺序不一致 | 死锁、相互等待 | 统一对象访问顺序 |
七、现场止血时,我一般会把动作分成“取消、终止、复盘”三步
1. 先评估能不能取消
如果是仍在执行中的会话,可以先尝试较温和的方式:
SELECT pg_cancel_backend(140508376790784);
2. 不能取消,再考虑强制终止
如果确认该会话就是阻塞源,而且业务允许回滚,我才会继续往下做:
SELECT pg_terminate_backend(140508376790784);
社区公开排查文章里也给过类似处理思路:先 pg_cancel_backend,不行再 pg_terminate_backend。
3. 止血之后一定要补复盘
这个动作我个人特别在意。因为如果只是 kill 掉会话,下一次高峰还会再来。
我一般会复盘这几项:
| 复盘项 | 重点问题 |
|---|---|
| 阻塞源 SQL | 是 DDL、批量任务还是普通事务 |
| 会话来源 | 哪个应用、哪个客户端、哪个作业 |
| 事务时长 | 是否超过业务允许边界 |
| 对象热点 | 哪张表、哪批键值、哪个流程最常冲突 |
| 是否跨节点 | 要不要补 CN/DN 双侧排查脚本 |
八、我自己比较认可的一套 GBase 8c 锁故障排查顺序
真到了现场,我更倾向按这个顺序走:
# 1. 先看业务库当前非空闲会话
gsql -d appdb -p 15400 -r -c "
SELECT pid, usename, client_addr, state, wait_event_type, wait_event,
now() - query_start AS running_time, query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;
"
# 2. 看锁信息和对象对应关系
gsql -d appdb -p 15400 -r -c "
SELECT n.nspname, c.relname, l.pid, l.mode, l.granted
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_locks l ON l.relation = c.oid
ORDER BY l.granted, l.pid;
"
# 3. 如果是分布式过程或跨节点问题,继续查 global_sessionid
gsql -d appdb -p 15400 -r -c "
SELECT pid, global_sessionid, locktype, mode, granted
FROM pg_locks
WHERE pid = 123456;
"
如果查到锁来源确实在别的节点,我一般不会停留在当前 CN 上,而是直接顺着全局事务号继续下钻。因为这一步往往决定你是十分钟定位,还是两个小时还在猜。
九、最后收一下:锁问题真正难的,不是 SQL,而是并发秩序
我自己最近重新整理这一块时,一个很明显的感受是:
GBase 8c 的锁问题,本质上不是“某条 SQL 写坏了”,而是并发秩序失控了。
所以真正有效的治理,往往不是只靠一次应急 kill 会话,而是慢慢把下面这些习惯建立起来:
- 控制事务边界,避免长事务
- 统一热点对象的访问顺序
- 把 DDL 和在线写流量错开
- 对批处理、存储过程补充分步可观测性
- 遇到分布式阻塞时,别只停留在单节点视角
从处理顺序看,我自己更倾向于这样理解:
- 先止血,找到 blocker。
- 再下钻,看是不是跨节点全局事务。
- 最后补规范,避免同类锁冲突反复出现。
真正落到现场时,能把这三步走顺,锁等待、阻塞链、全局死锁这些问题,其实就没那么乱了。
参考资料
[1] 南大通用GBase 8c分布式问题排查之锁故障定位
https://www.gbase.cn/community/post/4964
[2] 南大通用GBase 8c事务与锁之全局死锁解除
https://www.gbase.cn/community/post/3883
[3] 南大通用GBase 8c分布式场景常见故障运维
https://www.gbase.cn/community/post/4987
[4] 南大通用GBase 8c数据库巡检指南:全方位保障数据库健康与性能
https://www.gbase.cn/community/post/5577
评论
热门帖子
- 12025-12-01浏览数:182333
- 22023-05-09浏览数:24584
- 42023-09-25浏览数:17886
- 52020-05-11浏览数:16902