1
0
forked from erp-dev/erp
Files
erpnew/docs/2026-04-02-rds-pgbouncer-server-side-cursor-issue.md
2026-04-03 16:55:22 +08:00

162 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-04-02 RDS PgBouncer 命名游标问题记录
## 背景
生产环境在 Django Admin 页面访问 `SalesItem` 编辑页时出现如下错误:
```text
ProgrammingError at /admin/shipment/salesitem/1/change/
cursor "_django_curs_128437320623808_sync_1" already exists
```
本地开发/测试环境未复现该问题。
## 现象
- 出错页面:`/admin/shipment/salesitem/<id>/change/`
- 错误类型PostgreSQL `ProgrammingError`
- 关键错误信息:`cursor "... already exists"`
- 特征:属于数据库连接/游标状态错误,而不是表结构错误
## 排查结论
### 1. 不是 migrate 未执行导致
本次错误不符合迁移缺失的典型表现。
如果是 migration 未执行,通常会看到:
- 某字段不存在
- 某表不存在
- 约束不存在
- 类型不匹配
而本次是“命名游标已经存在”,属于运行时数据库连接状态问题。
### 2. 当前 `SalesItem` Admin 实现本身没有明显异常
`shipment` 模块中的 admin 配置较为简单,没有明显会主动触发复杂命名游标行为的自定义逻辑:
- 参考:[shipment/admin.py](/home/f/coding/flower/shipment/admin.py)
因此本问题更像是生产环境数据库接入方式引发,而不是该页面逻辑本身写坏。
### 3. 根因判断:阿里云 RDS 自带 PgBouncer/连接池 与 Django server-side cursors 兼容性问题
生产环境已确认使用“阿里云 RDS 自带代理/连接池”。
Django 在 PostgreSQL 下会在部分场景使用 server-side cursors尤其与 `QuerySet.iterator()` 相关)。
当应用经过连接池/代理访问数据库时,连接池不能稳定保证命名游标依赖的“同一物理连接上下文”,从而可能触发:
- 命名游标残留
- 游标状态错乱
- `cursor "... already exists"`
这类问题在连接池场景下比直连 PostgreSQL 更容易出现。
## 代码与配置侧观察
### 数据库配置
项目数据库配置位于:
- [flower/settings.py](/home/f/coding/flower/flower/settings.py)
原有配置特征:
- 使用 PostgreSQL
- 支持 `CONN_MAX_AGE`
- 未显式设置 `DISABLE_SERVER_SIDE_CURSORS`
### 项目内的连接池配置
仓库自带的 PgBouncer 配置位于:
- [deploy/pgbouncer/pgbouncer.ini](/home/f/coding/flower/deploy/pgbouncer/pgbouncer.ini)
仓库内自建 PgBouncer 使用的是 `session` 模式。
但生产环境此次问题来自阿里云 RDS 自带代理/连接池,实际行为应以云侧代理为准,不能简单等同于本仓库自带 PgBouncer 配置。
## 修复决策
考虑到:
- 生产环境不能放弃连接池
- 该系统存在较多大查询
- 生产已经出现与命名游标相关的实际错误
最终决定采用 Django 官方支持的兼容方案:
在数据库配置中加入:
```python
'DISABLE_SERVER_SIDE_CURSORS': True,
```
并保留:
```python
'CONN_MAX_AGE': env.int('CONN_MAX_AGE', default=0),
```
## 修复位置
已在以下位置加入配置:
- [flower/settings.py](/home/f/coding/flower/flower/settings.py)
配置位置为 `DATABASES['default']` 顶层,与 `ENGINE``HOST``PORT``CONN_MAX_AGE` 同级,不放在 `OPTIONS` 中。
## 采用该修复的原因
### 为什么不优先怀疑 migration
因为错误类型明显属于连接/游标状态,不属于 schema 不一致。
### 为什么不取消连接池
生产环境仍需要连接池承载系统中的大查询与整体数据库连接管理。
### 为什么选择禁用 server-side cursors
这是 Django 官方针对连接池/代理场景给出的标准兼容方向之一。
对当前项目而言,这一决策的风险低于继续保留命名游标并承受生产不稳定性。
## 可能的副作用
启用 `DISABLE_SERVER_SIDE_CURSORS=True` 后:
- 普通页面和常规 ORM 查询通常不会受功能性影响
- 依赖 `QuerySet.iterator()` 的超大结果集遍历,可能失去 server-side cursor 带来的部分性能/内存优势
项目中需要后续重点观察的批量遍历代码包括:
- [printing/tasks.py](/home/f/coding/flower/printing/tasks.py)
- [api_v1/external_product_image_backfill.py](/home/f/coding/flower/api_v1/external_product_image_backfill.py)
也就是说,该修复更可能带来“部分后台任务性能特征变化”,而不是“开发环境或生产环境无法正确运行”。
## 对开发环境的影响判断
开发环境同样可以启用 `DISABLE_SERVER_SIDE_CURSORS=True`,不会导致系统无法运行。
主要影响仍然是:
- 大结果集遍历时的性能和内存行为可能更保守
但这不属于严重副作用。
## 结论
本次问题定性为:
- **生产环境数据库连接池代理与 Django server-side cursors 的兼容性问题**
本次修复决策为:
- **保留连接池**
- **在 Django 数据库配置中启用 `DISABLE_SERVER_SIDE_CURSORS=True`**
该决策用于降低生产环境中命名游标相关错误的出现概率,并保持系统整体连接池架构不变。