GBase 8c 里一条 SQL 卡半天,我排查锁等待时通常先盯这几个地方
GBase 8c 锁等待问题完整排查与处理实践
我最近看 GBase 8c 这块资料时,越来越觉得锁等待特别容易被误判。现场里最常见的说法是"数据库卡住了"或者"这条 SQL 性能太差",但真正落到排查动作上,很多时候根子不是执行计划本身,而是事务没提交、DDL 和 DML 撞到一起、批量过程在某个 DN 上卡住、或者分布式场景里已经出现了跨节点等待。
GBase 8c 本身有 MVCC、常规锁、全局死锁解除这些机制,读写不冲突不代表所有操作都不会互相挡住,尤其是 DDL、批量更新、长事务、存储过程这几类场景,很容易把问题拖得很隐蔽。
我自己现在碰到"SQL 一直不返回"这类问题,通常不会上来就改参数,也不会先怀疑机器不够。更实际一点的做法是先把现场拆成三件事:
- 当前会话是不是在等锁。
- 阻塞源到底是谁,是活跃 SQL 还是
idle in transaction。 - 是单节点局部问题,还是已经变成分布式阻塞链。
如果这三个问题没先看清,后面不管是终止会话、调超时还是改程序提交策略,动作都很容易打偏。
我一般先把现场现象分成这几类
| 现场现象 | 我优先怀疑的点 | 第一动作 |
|---|---|---|
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);
我自己更愿意给团队定一个简单原则:
- 先确认这是不是业务关键会话。
- 先取消 active 查询,不要上来就 terminate。
- 如果是
idle in transaction且已经明确阻塞别人,再考虑 terminate。 - 终止后要复查锁是不是已经释放,别以为一 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 这种分布式场景。因为你只要把阻塞链找对了,后面的动作其实都不复杂;最怕的是根本没找到那个真正拿着锁不放的人。
参考资料
- 南大通用GBase 8c分布式问题排查之锁故障定位
https://www.gbase.cn/community/post/4964 - GBase 8c如何解除全局死锁
https://www.gbase.cn/community/post/2441 - 南大通用GBase 8c事务与锁之全局死锁解除
https://www.gbase.cn/community/post/3883 - 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 - GBase 8c V3.0.0数据类型——服务器信号函数
https://www.gbase.cn/community/post/2270
评论
热门帖子
- 12025-12-01浏览数:183633
- 22023-05-09浏览数:26328
- 42023-09-25浏览数:19993
- 52020-05-11浏览数:18705