Базы данных

Ошибка 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; каждый вариант имеет свои компромиссы по памяти и масштабируемости.


Содержание


Краткое решение (быстро)

  1. В settings.py добавьте в конфигурацию DATABASES для подключения через PgBouncer:
python
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,
 }
}
  1. Перезапустите приложение (и PgBouncer при изменении его конфига).
  2. Если у вас есть операции, которые ожидают потоковую выдачу очень большого набора строк, перепишите их на батчи/страничную выборку или выполняйте через отдельное прямое подключение к 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 и альтернативы

  1. Рекомендованный базовый шаг (прост и надёжен)
  • Установите DISABLE_SERVER_SIDE_CURSORS = True в DATABASES (см. пример выше). Это решает большинство случаев и — чаще всего — единственное, что нужно сделать при переходе на PgBouncer в режиме transaction. См. обсуждения и инструкции: блог о пуллинге соединений, stack overflow.

Плюсы: просто, безопасно для масштабирования. Минусы: Django будет читать результаты в память (client‑side), поэтому большие результирующие наборы могут потреблять много памяти.

  1. Если нужен 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 вообще: переписать обработку больших наборов на батч‑запросы/пагинацию (см. пример ниже).
  1. Обёртка транзакцией (краткое решение для ограниченных случаев)
  • Если вы можете удерживать транзакцию открытой на время итерации, то пgbouncer выделит тот же backend для всей транзакции, и курсор будет доступен. Например:
python
from django.db import transaction

with transaction.atomic():
 for row in MyModel.objects.filter(...).iterator():
 process(row)

Это работает, но опасно: длительные транзакции блокируют ресурсы и влияют на производительность базы.

  1. Батч‑итерация (рекомендуют для больших выборок)
  • Пример безопасной загрузки по кускам без server‑side cursor:
python
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:

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.


Источники


Заключение

Если коротко: при использовании PgBouncer в режиме transaction самый надёжный и простой путь — отключить серверные курсоры через DISABLE_SERVER_SIDE_CURSORS = True в Django и адаптировать тяжёлые выборки на батчи или прямые подключения. Альтернатива — использовать pool_mode = session или отдельные прямые соединения для задач, где server‑side cursors необходимы, но это снижает масштабируемость. Тестируйте конфигурацию (pool_size, max_client_conn, CONN_MAX_AGE) под вашу нагрузку и мониторьте pgbouncer/postgres, чтобы избежать неожиданных узких мест.

Авторы
Проверено модерацией
Модерация
Ошибка cursor does not exist PgBouncer Django PostgreSQL