Django视图引起Psycopg2游标存在/不存在错误

11
我运行一个Django网站,它有一个简单的ModelForm类型视图,但是会生成游标错误。在过去的两天里,这个视图被提交了几百次,其中大约8%的时间会生成错误。我只有在这个视图中才会出现这个问题,即使我有另一个非常相似的视图。令人沮丧的是,我还没有找出它的特殊之处。我在升级到Django 2.1/2之后才开始看到这些错误,但我认为它们可能已经存在,只是没有被发现。
完整的堆栈跟踪在这里: https://gist.github.com/jplehmann/ad8849572e569991bc26da87c81bb8f4 以下是一些来自查询日志[error] (internal users edit) OR (psycopg2 errors cursor)的示例,其中用户名已被删除以显示时间:
Jun 04 12:42:12 ballprice app/web.1: [ERROR] Internal Server Error: /users/a/edit  [log:228]
Jun 04 12:42:12 ballprice app/web.1: psycopg2.errors.InvalidCursorName: cursor "_django_curs_140401754175232_2" does not exist
Jun 04 12:42:12 ballprice app/web.1: psycopg2.errors.InvalidCursorName: cursor "_django_curs_140401754175232_2" does not exist
Jun 04 12:42:27 ballprice app/web.1: [ERROR] Internal Server Error: /users/a/edit  [log:228]
Jun 04 12:42:27 ballprice app/web.1: psycopg2.errors.InvalidCursorName: cursor "_django_curs_140401754175232_3" does not exist
Jun 04 12:57:51 ballprice app/web.3: [ERROR] Internal Server Error: /users/a/edit  [log:228]
Jun 04 12:57:51 ballprice app/web.3: psycopg2.errors.DuplicateCursor: cursor "_django_curs_140092205262592_2" already exists
Jun 04 12:57:51 ballprice app/web.3: psycopg2.errors.InvalidCursorName: cursor "_django_curs_140092205262592_2" does not exist
Jun 04 13:10:50 ballprice app/web.3: [ERROR] Internal Server Error: /users/b/edit  [log:228]
Jun 04 13:10:50 ballprice app/web.3: psycopg2.errors.DuplicateCursor: cursor "_django_curs_140092205262592_2" already exists
Jun 04 15:19:36 ballprice app/web.9: [ERROR] Internal Server Error: /users/c/edit  [log:228]
Jun 04 15:19:36 ballprice app/web.9: psycopg2.errors.InvalidCursorName: cursor "_django_curs_140515167295232_1" does not exist
Jun 04 17:28:22 ballprice app/web.5: [ERROR] Internal Server Error: /users/d/edit  [log:228]
Jun 04 17:28:22 ballprice app/web.5: psycopg2.errors.InvalidCursorName: cursor "_django_curs_140085445728000_2" does not exist
Jun 04 17:28:22 ballprice app/web.5: psycopg2.errors.InvalidCursorName: cursor "_django_curs_140085445728000_2" does not exist
Jun 04 22:49:15 ballprice app/web.1: [ERROR] Internal Server Error: /users/e/edit  [log:228]
Jun 04 22:49:15 ballprice app/web.1: psycopg2.errors.InvalidCursorName: cursor "_django_curs_139902341289728_2" does not exist
Jun 04 22:49:15 ballprice app/web.1: psycopg2.errors.InvalidCursorName: cursor "_django_curs_139902341289728_2" does not exist
Jun 04 23:43:26 ballprice app/web.1: [ERROR] Internal Server Error: /users/f/edit  [log:228]
Jun 04 23:43:26 ballprice app/web.1: psycopg2.errors.DuplicateCursor: cursor "_django_curs_139902341289728_2" already exists
Jun 05 02:49:22 ballprice app/web.1: [ERROR] Internal Server Error: /users/g/edit  [log:228]
Jun 05 02:49:22 ballprice app/web.1: psycopg2.errors.InvalidCursorName: cursor "_django_curs_140092373694208_1" does not exist
Jun 05 02:49:22 ballprice app/web.1: psycopg2.errors.InvalidCursorName: cursor "_django_curs_140092373694208_1" does not exist
Jun 05 02:49:41 ballprice app/web.1: [ERROR] Internal Server Error: /users/g/edit  [log:228]
Jun 05 02:49:41 ballprice app/web.1: psycopg2.errors.DuplicateCursor: cursor "_django_curs_140092373694208_1" already exists

然而,我无法重现此错误。我和一位用户交谈过后,他说他试了三次才保存成功。
你可以看到命名的光标被频繁地重复使用,相隔数分钟,我只能假定这是正常的。
版本:
- Python python-3.7.2 - Django==2.2.12 - psycopg2-binary==2.8.5
是什么导致了这个问题?
更新
我们确实使用PG bouncer,并禁用服务器端光标的建议是可靠的,似乎已经解决了问题。

1
你能分享一下视图代码吗?使用的是哪个版本的Postgres?同时,可以看一下这个链接:https://dev59.com/E1QK5IYBdhLWcg3wUOZ5,看看是否有所启发。 - Adrian Klaver
不确定是否适用,但请查看:https://docs.djangoproject.com/en/2.2/ref/databases/#server-side-cursors - Adrian Klaver
你能添加「最小化重现示例(Minimal, Reproducible Example)」吗? - JPG
这似乎是发生在模型选择迭代器中。另一个视图也有吗?您是否使用某种隔离来更新它们?查看代码和模型将真正有助于解决问题。 - user1600649
3个回答

23

你是否在使用pgBouncer或其他连接池机制?当数据库承载连接时,我通常会遇到这种问题(如果你有很多客户端,则完全可以并且建议使用连接池)。

https://docs.djangoproject.com/en/3.0/ref/databases/#transaction-pooling-and-server-side-cursors

在事务池模式下使用连接池器(例如PgBouncer)需要禁用该连接的服务器端游标。

服务器端游标仅限于某个连接,并在AUTOCOMMIT为True时保持打开状态。随后的事务可能尝试从服务器端游标获取更多结果。在事务池模式下,无法保证后续事务将使用相同的连接。如果使用了不同的连接,则会在事务引用服务器端游标时引发错误,因为只能在创建它们的连接中访问服务器端游标。

一种解决方法是通过将DISABLE_SERVER_SIDE_CURSORS设置为True,来为连接在DATABASES中禁用服务器端游标。

要在事务池模式下受益于服务器端游标,可以设置另一个连接到数据库,以执行使用服务器端游标的查询。此连接需要直接连接到数据库或连接到会话池模式的连接池器。

另一种选择是将每个使用服务器端游标的QuerySet包装在atomic()块中,因为它会在事务期间禁用自动提交。这样,服务器端游标只会在事务的持续时间内存在。

因此,如果这适用于您的连接,则您有以下选项:

禁用游标

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'DISABLE_SERVER_SIDE_CURSORS': True,
    }
}

将其包装到事务中

(不能保证可行,取决于您的连接池设置)

with transaction.atomic():
     qs = YourModel.objects.filter()
     for values in qs.values('id', 'x').iterator():
        pass

额外连接

如果您需要服务器端游标,您也可以使用额外的直接连接到数据库,然后将直接连接用于这些查询。

YourModel.objects.using('different_db_connection_id').filter().iterator()

2
通常情况下,当模型与数据库不兼容时,就会出现此错误。例如,如果您更改了一个模型并添加了一个字段,但没有进行迁移。
请确保检查您网站中的所有应用程序都在 INSTALLED_APP 中,否则此错误可能是因为迁移未应用于新的未声明的应用程序。 接下来,
python manage.py makemigrations && python manage.py migrate

1
光标问题与模型状态与数据库状态无关。如果模型不同步,您将无法走得这么远,而且它会始终发生而不是有时发生。这是一个时间/并发问题。 - user1600649
刚遇到了这个问题,意识到是因为我忘记为模型的更改创建迁移。创建迁移为我解决了这个错误。 - Henry Woody
@user1600649 的评论似乎不正确。我也遇到了迁移和数据库彼此一致的问题,但是这是由于模型被编辑过。 - mimo
这节省了我很多时间,我的案例被遗忘了迁移... - Ondra

1

以下提到的每种解决方案都有其自身的缺点。

  • 禁用游标: 我们将失去服务器端游标(分块结果集)的好处。
  • 包装到事务中: 这会增加事务的开销,并可能降低在使用大量.iterator()查询集的高流量站点上的查询执行吞吐量。
  • 额外的连接: 开发人员必须记住为.iterator()查询集使用单独的数据库。

因此,更好的方法是使用两个数据库设置。一个用于PgBouncer,另一个用于直接的数据库连接。(两个数据库设置应指向后端的同一个数据库)并根据事务状态路由.iterator()查询集以使用直接的数据库连接。

注意:我们应该保留DISABLE_SERVER_SIDE_CURSORS=False(对于两个数据库设置),因为当迭代查询集被包装在事务内时,PgBouncer支持服务器端游标。

DATABASE_URL: 'postgresql://django:xxx@localhost:7432/dbname'   # (pgbouncer connection)
DATABASE_URL_DIRECT: 'postgresql://django:xxx@localhost:6432/dbname' # (direct db connection)

在settings.py内部

USE_PGBOUNCER = True
if USE_PGBOUNCER
    if 'migrate' not in sys.argv:
        # django app proccess
        DATABASES = {
            'default': dj_database_url.parse(config['DATABASE_URL']), # (pgbouncer connection)
            'direct_db': dj_database_url.parse(config['DATABASE_URL_DIRECT'])  # (direct db connection)
        }
     else:
        # django migration proccess
        DATABASES = {
            'default': dj_database_url.parse(config['DATABASE_URL_DIRECT'])  # (direct db connection)
        }
else:
    # not using pgbouncer.
    DATABASES = {
        'default': dj_database_url.parse(config['DATABASE_URL'])   # (direct db connection)
    }

在初始化Django应用程序时(在AppConfig.ready()内部)
from functools import wraps

from django.apps import AppConfig
from django.conf import settings
from django.db import transaction
from django.db.models.query import ModelIterable, ValuesIterable, ValuesListIterable, \
    NamedValuesListIterable, FlatValuesListIterable


class CommonAppConfig(AppConfig):
    name = 'app_name'

    def ready(self):

        if settings.USE_PGBOUNCER:
            direct_db = 'direct_db'. # DATABASE setting
            ModelIterable.__iter__ = patch_iterator_class(using=direct_db)(ModelIterable.__iter__)
            ValuesIterable.__iter__ = patch_iterator_class(using=direct_db)(ValuesIterable.__iter__)
            ValuesListIterable.__iter__ = patch_iterator_class(using=direct_db)(ValuesListIterable.__iter__)
            NamedValuesListIterable.__iter__ = patch_iterator_class(using=direct_db)(NamedValuesListIterable.__iter__)
            FlatValuesListIterable.__iter__ = patch_iterator_class(using=direct_db)(FlatValuesListIterable.__iter__)


def patch_iterator_class(using):
    def decorator(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            cxn = transaction.get_connection()
            if not self.chunked_fetch or cxn.in_atomic_block:
                # We are already in db transaction so use the same db connection (default) using
                # which db transaction was started to execute iterator query.
                # Or
                # We are neither in db transaction nor it is a chunked_fetch so continue over same db connection
                return func(self, *args, **kwargs)
            # We are not in any db transaction and it is chunked_fetch so redirect iterator query to use
            # direct_db connection to avoid cursor not found exception.
            self.queryset = self.queryset.using(using)  # redirect query to use direct db connection.
            return func(self, *args, **kwargs)
        return wrapper
    return decorator

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接