一、事故背景
某 PostgreSQL 生产实例突然出现异常:
-
API 超时增加
-
数据库连接数持续上涨
-
新请求开始失败
-
最终接近
max_connections
怀疑数据库内部存在阻塞。
登录数据库准备查看对象状态:
\d+
结果发现:
\d+ 本身也卡住了。
这说明问题已经不仅是业务 SQL。
可能涉及:
-
DDL锁
-
catalog阻塞
-
长事务
-
系统表等待
开始排查。
二、第一步:查看数据库活动会话
首先查看当前数据库所有连接。
SELECT
pid,
usename,
wait_event_type,
wait_event,
state,
query
FROM pg_stat_activity
WHERE datname=current_database();
发现大量会话处于:
wait_event_type = Lock
wait_event = relation
state = active
例如:
SELECT ...
FROM biz_table
WHERE ...
UPDATE biz_table
SET ...
WHERE ...
这说明:
业务 SQL 不是慢,而是在等待表锁。
三、第二步:确认是否存在锁阻塞
使用 blocker / blocked 排查 SQL。
SELECT
blocked.pid AS blocked_pid,
blocked.usename AS blocked_user,
blocked.wait_event_type,
blocked.wait_event,
now()-blocked.query_start AS blocked_duration,
blocked.query AS blocked_query,
blocker.pid AS blocker_pid,
blocker.usename AS blocker_user,
blocker.state AS blocker_state,
now()-blocker.query_start AS blocker_duration,
blocker.query AS blocker_query
FROM pg_stat_activity blocked
JOIN pg_locks bl
ON bl.pid=blocked.pid
JOIN pg_locks kl
ON kl.locktype=bl.locktype
AND kl.database IS NOT DISTINCT FROM bl.database
AND kl.relation IS NOT DISTINCT FROM bl.relation
AND kl.page IS NOT DISTINCT FROM bl.page
AND kl.tuple IS NOT DISTINCT FROM bl.tuple
AND kl.virtualxid IS NOT DISTINCT FROM bl.virtualxid
AND kl.transactionid IS NOT DISTINCT FROM bl.transactionid
AND kl.classid IS NOT DISTINCT FROM bl.classid
AND kl.objid IS NOT DISTINCT FROM bl.objid
AND kl.objsubid IS NOT DISTINCT FROM bl.objsubid
AND kl.pid<>bl.pid
JOIN pg_stat_activity blocker
ON blocker.pid=kl.pid
WHERE
NOT bl.granted
AND kl.granted
ORDER BY blocked.query_start;
结果:
大量 blocked session 指向同一个 blocker。
四、第三步:找到真正的阻塞源
最终定位到唯一 blocker:
ALTER TABLE biz_table
ALTER COLUMN id_col SET NOT NULL,
ALTER COLUMN id_col
ADD GENERATED BY DEFAULT AS IDENTITY
(
MINVALUE 0
RESTART WITH 39252882
CACHE 1
),
ADD PRIMARY KEY(id_col,status_col);
至此基本确认:
生产环境执行了一次重型 DDL。
五、第四步:查看锁详情
进一步确认锁类型。
SELECT
a.pid,
a.usename,
l.mode,
l.granted,
a.state,
now()-a.query_start AS duration,
a.query
FROM pg_locks l
JOIN pg_stat_activity a
ON a.pid=l.pid
WHERE l.relation='biz_table'::regclass
ORDER BY l.granted,a.query_start;
重点关注:
AccessExclusiveLock
该锁会阻塞:
SELECT
INSERT
UPDATE
DELETE
也就是说:
整张表不可访问。
六、第五步:分析为什么 \d+ 会卡住
为了确认 \d+ 做了什么,打开 psql 隐藏 SQL 输出。
\set ECHO_HIDDEN on
然后再次执行:
\d+
可以看到其内部执行的是 catalog 查询:
SELECT ...
FROM pg_catalog.pg_class
JOIN pg_catalog.pg_namespace ...
LEFT JOIN pg_catalog.pg_description ...
由于存在 DDL 强锁竞争。
最终导致:
\d+ 也被锁等待
七、根因分析
看似简单的 DDL:
ALTER TABLE
SET NOT NULL
ADD IDENTITY
ADD PRIMARY KEY
实际上包含三个重操作。
1)SET NOT NULL
可能触发:
全表扫描验证
2)ADD GENERATED AS IDENTITY
修改:
sequence metadata
catalog metadata
3)ADD PRIMARY KEY
核心问题。
如果不存在现成索引。
PostgreSQL 会隐式执行:
CREATE UNIQUE INDEX ...
而且:
不是 CONCURRENTLY。
意味着:
ACCESS EXCLUSIVE LOCK
+
全表扫描
+
排序
+
索引构建
最终导致:
业务 SQL 堆积
连接数上涨
max_connections 接近耗尽
\d+ 卡死
八、现场恢复
首先取消 DDL。
优先尝试:
SELECT pg_cancel_backend(<pid>);
无法退出再使用:
SELECT pg_terminate_backend(<pid>);
阻塞解除后。
连接逐渐恢复。
九、正确的大表 DDL 最佳实践
原始写法:
ALTER TABLE ...
SET NOT NULL
ADD IDENTITY
ADD PRIMARY KEY
不适合生产大表。
推荐拆分。
Step1:在线验证 NOT NULL
添加未验证约束
ALTER TABLE biz_table
ADD CONSTRAINT biz_table_id_nn
CHECK(id_col IS NOT NULL)
NOT VALID;
特点:
| 项目 | 影响 |
|---|---|
| 在线 | YES |
| 全表扫描 | NO |
| 锁 | 短暂 ACCESS EXCLUSIVE |
后台验证
ALTER TABLE biz_table
VALIDATE CONSTRAINT biz_table_id_nn;
特点:
| 项目 | 影响 |
|---|---|
| 在线 | YES |
| 全表扫描 | YES |
| 阻塞DML | NO |
检查状态:
SELECT
conname,
convalidated
FROM pg_constraint
WHERE conname='biz_table_id_nn';
必须为:
t
Step2:在线创建唯一索引
不要直接 ADD PRIMARY KEY。
先执行:
CREATE UNIQUE INDEX CONCURRENTLY idx_biz_pk
ON biz_table(id_col,status_col);
特点:
| 项目 | 影响 |
|---|---|
| 在线 | YES |
| 阻塞DML | NO |
| 事务内执行 | NO |
检查索引状态:
SELECT
indexrelid::regclass,
indisvalid,
indisready
FROM pg_index
WHERE indexrelid='idx_biz_pk'::regclass;
Step3:短窗口切换
低峰执行。
ALTER TABLE biz_table
ALTER COLUMN id_col SET NOT NULL;
ALTER TABLE biz_table
ADD CONSTRAINT biz_table_pkey
PRIMARY KEY USING INDEX idx_biz_pk;
ALTER TABLE biz_table
ALTER COLUMN id_col
ADD GENERATED BY DEFAULT AS IDENTITY(...);
特点:
| 项目 | 影响 |
|---|---|
| 在线 | 部分 |
| 锁 | 短暂 ACCESS EXCLUSIVE |
| 推荐 | 低峰执行 |
十、总结
这次事故链路:
重型 ALTER TABLE
↓
ACCESS EXCLUSIVE LOCK
↓
relation wait 堆积
↓
业务查询阻塞
↓
连接数打满
↓
\d+ 卡死
经验总结:
不要直接:
ALTER TABLE ADD PRIMARY KEY
大表主键迁移:
先:
CREATE UNIQUE INDEX CONCURRENTLY
再:
PRIMARY KEY USING INDEX
大表 NOT NULL 迁移:
推荐:
CHECK NOT VALID
↓
VALIDATE
↓
SET NOT NULL
生产DDL原则
长耗时操作在线完成。
最终 schema 切换放入短窗口。