惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
Stack Overflow Blog
Stack Overflow Blog
人人都是产品经理
人人都是产品经理
The GitHub Blog
The GitHub Blog
Engineering at Meta
Engineering at Meta
Vercel News
Vercel News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Y
Y Combinator Blog
The Cloudflare Blog
Last Week in AI
Last Week in AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
Google DeepMind News
Google DeepMind News
D
DataBreaches.Net
博客园 - 司徒正美
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
L
LangChain Blog

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 理解Linux TunTap设备
一次服务升级时pg表DDL执行超时失败
zhengwenfeng · 2025-09-14 · via 郑文峰的博客

# 背景

在一次业务升级过程中,需要针对 Postgresql 使用 DDL 对某个表新增一个字段,升级过程中失败了,报错信息如下:

ERROR:  canceling statement due to lock tiemout

1

# 分析

DDL 在执行的时候需要拿到 ACCESS EXCLUSIVE 锁,而它是最强的表级锁,只有当表中没有任何活动的事务时才能拿到该锁。

查询 lock timeout 发现其设置为 2 分钟。

通过报错信息可以看到它获取锁失败了,所以我们大胆的猜测一下是有业务中有事务超过 2 分钟,从而导致 DDL 一直无法拿到锁。

通过下面的 SQL 来查询当前超过 2 分钟的活动查询,可以定位某条 SQL。

SELECT 
    pid,
    now() - query_start AS duration,
    state,
    query,
    application_name,
    client_addr,
    client_application_name,
    xact_start,
    query_start
FROM pg_stat_activity 
WHERE state = 'active'
  AND now() - query_start > interval '2 minutes'
  AND pid <> pg_backend_pid()  -- 排除当前查询自身
  AND datname = 'your_database_name'  -- 替换为你的数据库名
ORDER BY query_start;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最后找到了一条查询 SQL,查询时会拿 ACCESS SHARE 锁,所以其阻塞了 DDL 的执行。

这里做一个测试 postgresql 拿锁失败的测试。

使用 postgresql 客户端创建一个事务,对一个表进行查询,但是不关闭。

# begin;
BEGIN
# select * from apollo.task limit 1;
....

1
2
3
4

而后在另一个客户端针对该表中执行一个 DDL 语句,发现其超时了。

# ALTER TABLE apollo.task ADD COLUMN IF NOT EXISTS test int;
ERROR:  canceling statement due to lock timeout

1
2

最后在第一个客户端中结束事务

再在第二个客户端再次执行 DDL 语句,显而易见的成功了。

# ALTER TABLE apollo.task ADD COLUMN IF NOT EXISTS test2 int;
ALTER TABLE

1
2

# 原因

现在我们通过该条 SQL 从业务代码中去找到执行的地方。

最后发现是在代码中使用了一个已经关闭的session,导致session 持续被打开而没有关闭,代码如下:

from sqlalchemy.orm.session import sessionmaker
_Session = sessionmaker()

@contextmanager
def open_session():
    """
    通用psql的session上下文管理器
    """

    try:
        session = _Session()
        yield session
        session.commit()
    except OperationalError as e:
        logging.error(f"Postgresql connection error: {e}, track: {traceback.format_exc()}")
        reconnection()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

with open_session() as session:
    pass

task = TaskModel.get_by_id(session, task_id)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

可以看到有使用一个上下文管理器来管理session的创建与关闭,而最后一行在 with 的作用域外面使用了 session,但是该session是在 with 结束时是调用了 session.close() 方法的。

通过查看 sqlalchemy 官网查询,即使是 close() 依然可能再次被使用。

https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.close

17578168418231757816840915.png

也就是说,最后一行使用了 session 但是没有再次对其进行关闭,导致其持续活动,导致执行 DDL 无法获取到 ACCESS EXCLUSIVE 锁,最终超时失败。

# 结论

  1. 即使是被close()的对象不能再次被使用,有隐藏的风险。
  2. 使用完资源后,一定要记得关闭。