Ошибка cursor does not exist PgBouncer Django PostgreSQL
Как исправить ошибку 'cursor does not exist' при использовании PgBouncer с Django и PostgreSQL. Настройки DISABLE_SERVER_SIDE_CURSORS, pool_mode=transaction, оптимальная конфигурация для продакшена и альтернативы.
Как исправить ошибку ‘cursor does not exist’ при использовании PgBouncer с Django и PostgreSQL?
Использую PgBouncer в качестве пулера соединений. Получаю ошибку:
InvalidCursorName: cursor “_django_curs_140116399986368_sync_1” does not exist
Нагуглил решение: добавить в параметры подключения к БД параметр “DISABLE_SERVER_SIDE_CURSORS”: true.
Подскажите лучший вариант решения этой проблемы и оптимальные настройки для производства.
Ошибка “cursor … does not exist” при использовании PgBouncer с Django и PostgreSQL обычно возникает из‑за серверных (named) курсоров, которые живут только в конкретном соединении; в режиме PgBouncer pool_mode=transaction соединение может поменяться между транзакциями и Django пытается обратиться к несуществующему курсору. Быстрое и надёжное решение для большинства веб‑приложений — в настройках DATABASES установить DISABLE_SERVER_SIDE_CURSORS = True, либо использовать отдельное (прямое) соединение или режим session для операций, где нужен server‑side cursor; каждый вариант имеет свои компромиссы по памяти и масштабируемости.
Содержание
- Краткое решение (быстро)
- Почему возникает ошибка “cursor does not exist”
- Как исправить: DISABLE_SERVER_SIDE_CURSORS и альтернативы
- Оптимальные настройки PgBouncer и Django для продакшна
- Обходные пути: session pooling, прямые подключения, батчи
- Проверка и отладка (шаги)
- Источники
- Заключение
Краткое решение (быстро)
- В settings.py добавьте в конфигурацию DATABASES для подключения через PgBouncer:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'yourdb',
'USER': 'django',
'PASSWORD': 'secret',
'HOST': '127.0.0.1', # хост PgBouncer
'PORT': '6432', # порт PgBouncer
'DISABLE_SERVER_SIDE_CURSORS': True,
}
}
- Перезапустите приложение (и PgBouncer при изменении его конфига).
- Если у вас есть операции, которые ожидают потоковую выдачу очень большого набора строк, перепишите их на батчи/страничную выборку или выполняйте через отдельное прямое подключение к PostgreSQL.
Почему это работает: при DISABLE_SERVER_SIDE_CURSORS = True Django не создаёт серверные именованные курсоры (named cursors) и не полагается на их существование в конкретном back‑end соединении — проблема исчезает. Подробнее и доказательства можно найти в заметках сообщества и баг‑репортах Django StackOverflow — пример ошибки и issue в Django.
Почему возникает ошибка “cursor does not exist”
Коротко — named cursor (серверный курсор) принадлежит конкретному TCP‑соединению к PostgreSQL. Алгоритм такой:
- Django/psycopg2 создаёт именованный курсор, чтобы стримить запрос (например, при
QuerySet.iterator()). - PgBouncer в режиме
pool_mode = transactionвыдаёт клиенту (вашему Django процессу) backend‑соединение только на время транзакции. После окончания транзакции это backend‑соединение возвращается в пул. - Если Django затем пытается продолжить чтение из того же именованного курсора на другом backend‑соединении — курсор уже не существует →
InvalidCursorName: cursor "... does not exist".
Это описано в дискуссиях и тикетах Django (см. ticket #16614 и ticket #28062), а также подтверждается практическими советами и блогами (например, Django + PgBouncer на Heroku).
Как исправить: DISABLE_SERVER_SIDE_CURSORS и альтернативы
- Рекомендованный базовый шаг (прост и надёжен)
- Установите
DISABLE_SERVER_SIDE_CURSORS = Trueв DATABASES (см. пример выше). Это решает большинство случаев и — чаще всего — единственное, что нужно сделать при переходе на PgBouncer в режиме transaction. См. обсуждения и инструкции: блог о пуллинге соединений, stack overflow.
Плюсы: просто, безопасно для масштабирования. Минусы: Django будет читать результаты в память (client‑side), поэтому большие результирующие наборы могут потреблять много памяти.
- Если нужен streaming без загрузки в память
- Вариант A — переключить PgBouncer на
pool_mode = session. Тогда один клиент держит один backend‑сокет, и серверные курсоры будут работать. Но session‑режим разрушает преимущества пула (меньшая масштабируемость). - Вариант B — для задач, где реально нужно server‑side cursor, использовать прямое подключение к PostgreSQL (обход PgBouncer) или отдельный database alias в Django, который ссылается напрямую на Postgres (например,
DATABASES['direct'] = {... порт 5432 ...}), и через него выполнять только эти операции. - Вариант C — не использовать server‑side cursors вообще: переписать обработку больших наборов на батч‑запросы/пагинацию (см. пример ниже).
- Обёртка транзакцией (краткое решение для ограниченных случаев)
- Если вы можете удерживать транзакцию открытой на время итерации, то пgbouncer выделит тот же backend для всей транзакции, и курсор будет доступен. Например:
from django.db import transaction
with transaction.atomic():
for row in MyModel.objects.filter(...).iterator():
process(row)
Это работает, но опасно: длительные транзакции блокируют ресурсы и влияют на производительность базы.
- Батч‑итерация (рекомендуют для больших выборок)
- Пример безопасной загрузки по кускам без server‑side cursor:
def chunked_iter(queryset, chunk_size=1000):
last_pk = None
qs = queryset.order_by('pk')
while True:
chunk = qs if last_pk is None else qs.filter(pk__gt=last_pk)
batch = list(chunk[:chunk_size])
if not batch:
break
last_pk = batch[-1].pk
for obj in batch:
yield obj
Этот метод контролирует объём памяти и безопасен с PgBouncer в transaction режиме.
Оптимальные настройки PgBouncer и Django для продакшна
Общие рекомендации (точные числа — зависят от вашей инфраструктуры):
- pool_mode = transaction — лучший выбор для веб‑приложений, где важна масштабируемость.
- DISABLE_SERVER_SIDE_CURSORS = True — включите для всех подключений через PgBouncer (см. выше).
- max_client_conn — большой (например, 500–2000) чтобы pgbouncer принял много клиентских подключений; реальное значение зависит от ожиданий нагрузки.
- default_pool_size — количество backend‑соединений, выделяемых на пул для пользователя/базы. Рассчитывайте так:
default_pool_size ≈ floor((postgres_max_connections - reserved) / number_of_pools)
Пример: если postgres max_connections = 200, у вас 4 приложенческих пулов и нужно зарезервировать 20 соединений для админов/реплик — (200-20)/4 = 45. - reserve_pool_size = 5 (опционально) и reserve_pool_timeout = 5 — небольшой резерв для кратковременных всплесков.
- server_lifetime, server_idle_timeout — задайте (например, 3600 и 600 сек) чтобы снять “запекшиеся” соединения.
Пример pgbouncer.ini:
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
reserve_pool_size = 5
reserve_pool_timeout = 5
server_lifetime = 3600
server_idle_timeout = 600
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
CONN_MAX_AGE в Django: держите ненулевое значение (например 60–600 сек) чтобы уменьшить churn TCP соединений к PgBouncer; это не решит проблему курсоров, но уменьшит накладные расходы на переподключения. Подбирайте значение по нагрузке и тестам.
Полезные материалы по настройке и тонкостям — см. DZone article и разборы на dev.to (например, публикация о pitfall’ах).
Обходные пути: session pooling, прямые подключения, батчи
Когда использовать session pooling:
- Когда у вас есть операции, которые обязательно требуют server‑side cursor и вы готовы «платить» за это выделенными соединениями (например, аналитические джобы). Session pooling удерживает backend для клиента, курсор живёт.
Когда использовать прямое подключение:
- Для редких фоновых задач: создайте отдельный DATABASES[‘direct’] и выполняйте тяжелую выборку через него, не трогая основное подключение приложения.
Когда использовать батчи/пагинацию:
- Для наиболее масштабируемого и безопасного подхода при работе с большими объёмами данных в веб‑запросах.
Проверка и отладка (шаги)
- Воспроизведите ошибку локально через PgBouncer и напрямую к Postgres, сравните поведение.
- На pgbouncer выполните:
- psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c “SHOW POOLS;”
- psql … -c “SHOW CLIENTS;” / “SHOW SERVERS;”
- Проверьте логи pgbouncer (обычно /var/log/pgbouncer/ или куда настроено) и Django‑логи на InvalidCursorName.
- Включите SQL‑логирование Django: logger ‘django.db.backends’ — чтобы найти где используются
.iterator()или другие стриминговые вызовы. - Тестируйте влияние
DISABLE_SERVER_SIDE_CURSORS = Trueна память под нагрузкой: прогоните нагрузочный тест и замерьте потребление памяти.
Для практических кейсов и реальных отчётов об этой проблеме см. StackOverflow пример и обсуждение в багтрекере Django ticket #28062.
Источники
- https://stackoverflow.com/questions/62216837/django-view-causes-psycopg2-cursor-does-does-not-exist-error
- https://code.djangoproject.com/ticket/28062
- https://devpress.csdn.net/postgresql/6304d171c67703293080e167.html
- https://github.com/goauthentik/authentik/issues/6807
- https://code.djangoproject.com/ticket/16614
- https://saadmk11.github.io/blog/posts/django-postgresql-database-connection-pooling-with-pgbouncer/
- https://dzone.com/articles/advanced-postgres-connection-pooling-using-pgbounc
- https://dev.to/artemooon/django-pgbouncer-in-production-pitfalls-fixes-and-survival-tricks-3jib
- https://dev.to/stefanukena/scaling-django-postgres-with-pgbouncer-on-heroku-1lb5
- https://www.reddit.com/r/django/comments/1hgauvx/how_to_fix_postgresql_connection_cursor_errors/
Заключение
Если коротко: при использовании PgBouncer в режиме transaction самый надёжный и простой путь — отключить серверные курсоры через DISABLE_SERVER_SIDE_CURSORS = True в Django и адаптировать тяжёлые выборки на батчи или прямые подключения. Альтернатива — использовать pool_mode = session или отдельные прямые соединения для задач, где server‑side cursors необходимы, но это снижает масштабируемость. Тестируйте конфигурацию (pool_size, max_client_conn, CONN_MAX_AGE) под вашу нагрузку и мониторьте pgbouncer/postgres, чтобы избежать неожиданных узких мест.