Django持久化数据库连接

60

我正在使用Django和Apache和mod_wsgi以及PostgreSQL(全部在同一主机上),需要处理大量简单的动态页面请求(每秒数百个)。我面临的问题是Django没有持久性数据库连接并且在每个请求上重新连接(这需要近5毫秒)。

进行基准测试时,我发现使用持久连接可以处理接近500个请求/秒,而没有持久连接只能处理50个请求/秒。

有什么建议吗?如何修改Django以使用持久连接或加速Python到数据库的连接?

7个回答

36

3
这是在1.6分支上的(第一个)提交:https://github.com/django/django/commit/2ee21d9f0d9eaed0494f3b9cd4b5bc9beffffae5 - David Pope

27

尝试使用PgBouncer - 一款适用于PostgreSQL的轻量级连接池。

  • 在旋转连接时有几个不同的级别:
    • 会话池
    • 事务池
    • 语句池
  • 低内存需求(默认每个连接2k)。

1
与pgpool相同,它不会消除每个请求连接的巨大开销。连接打开代码是真正的瓶颈。 - HardQuestions
4
@Mike TK 首先,它与pgpool不同。Pgbouncer使用libevent异步管理连接,而pgpool则为每个连接分叉一个进程,就像Postgres本身一样(唯一的区别是pgpool保持进程活动)。从我的经验来看,使用pgbouncer(与不使用任何方法相比)可以明显加快速度。 - Vasil
5
这是他们在 Disqus 中使用的(截至目前,是使用用户最多的 Django 项目)。我认为这是经过火力测试的。 - tawmas

20
在 Django 的主干版本中,编辑 django/db/__init__.py 并注释掉以下行:
signals.request_finished.connect(close_connection)

这个信号处理程序使每个请求后它与数据库断开连接。我不知道这样做会有什么副作用,但在每次请求之后重新开始一个新的连接没有任何意义;正如您已经注意到的那样,这会破坏性能。

现在我正在使用它,但我还没有进行全面测试以查看是否会出现问题。

我不知道为什么所有人都认为这需要一个新的后端或特殊的连接池或其他复杂的解决方案。这似乎非常简单,尽管我不怀疑他们一开始就遇到了一些晦涩的陷阱--这应该更加明智地解决;对于高性能服务来说,每个请求的5毫秒的开销相当大,正如您已经注意到的那样。(对我来说需要150ms--我还没有弄清楚原因。)

编辑:另一个必要的更改是在django/middleware/transaction.py中;删除两个transaction.is_dirty()测试,并始终调用commit()或rollback()。否则,如果仅从数据库中读取数据,它将不会提交事务,这将保留应该关闭的锁定。


5
查看http://groups.google.com/group/django-users/browse_thread/thread/9b58de1380b1afd0,以获取更通用的实现方式django-postgres-persistent-db-connection.diff,但它仅适用于Postgresql。(并不是说原帖作者在关注这里,但对于其他任何发现此处的人...) - Glenn Maynard
1
人们建议使用新的后端的原因之一是代码可移植性;每次升级都意味着你必须再次编辑代码,或者如果你开始注释掉 Django 代码库,则必须保持分支同步。 - Burhan Khalid
1
我检查了Django 2.0的源代码,并发现Django只关闭已经超过其生命周期的连接。也许情况有所不同。您的示例代码已不存在,新代码是“signals.request_finished.connect(close_old_connections)”。 - dogewang

16
我创建了一个小的Django补丁,通过sqlalchemy池实现了MySQL和PostgreSQL的连接池。
这在http://grandcapital.net/的生产环境中已经长期完美运行。
在搜索了一下相关主题后编写了这个补丁。

非常感谢Igor,我使用了你的补丁,它完美地运行了。我在http://menendez.com/blog/mysql-connection-pooling-django-and-sqlalchemy/找到了另一个关于相同方法的参考,但是你的更简单易用。 - Fabio Ceconello

4

免责声明:我没有试过这个方法。

我认为您需要实现自定义数据库后端。网上有一些示例展示了如何使用连接池实现数据库后端。

对于您的情况,使用连接池可能是一个很好的解决方案,因为当连接返回到池中时,网络连接保持打开状态。

  • 这篇文章通过修补Django来实现这个功能(其中的一个评论指出最好在核心Django代码之外实现自定义后端)
  • 这篇文章是一个自定义db后端的实现

这两篇文章都使用MySQL - 或许您可以使用类似的技术来处理Postgresql。

编辑:

  • The Django Book提到了Postgresql连接池,使用pgpool教程)。
  • 有人发布了一个补丁,用于实现连接池的psycopg2后端。建议在自己的项目中创建现有后端的副本并对其进行修补。

我已经尝试了pgpool,但这并没有改善情况(每次仍然需要重新连接)。PgPool是为其他一些目的设计的(故障转移、复制等等)。 - HardQuestions

1
这是一个用于Django连接池的包: django-db-connection-pool
pip install django-db-connection-pool

您可以提供额外的选项以传递给SQLAlchemy的池创建,键名为POOL_OPTIONS:

DATABASES = {
    'default': {
        ...
        'POOL_OPTIONS' : {
            'POOL_SIZE': 10,
            'MAX_OVERFLOW': 10
        }
        ...
     }
 }

0

我创建了一个小的自定义 psycopg2 后端,使用全局变量实现持久连接。

通过这个方法,我能够将每秒请求数从 350 提高到 1600(在非常简单的页面上进行少量选择)

只需将其保存在名为 base.py 的文件中的任何目录中(例如 postgresql_psycopg2_persistent),并在设置中将 DATABASE_ENGINE 设置为 projectname.postgresql_psycopg2_persistent

注意!!!该代码不是线程安全的 - 由于结果不可预测,因此无法与 Python 线程一起使用,在 mod_wsgi 的情况下,请使用带有 threads=1 的 prefork 守护程序模式


# Custom DB backend postgresql_psycopg2 based
# implements persistent database connection using global variable

from django.db.backends.postgresql_psycopg2.base import DatabaseError, DatabaseWrapper as BaseDatabaseWrapper, \
    IntegrityError
from psycopg2 import OperationalError

connection = None

class DatabaseWrapper(BaseDatabaseWrapper):
    def _cursor(self, *args, **kwargs):
        global connection
        if connection is not None and self.connection is None:
            try: # Check if connection is alive
                connection.cursor().execute('SELECT 1')
            except OperationalError: # The connection is not working, need reconnect
                connection = None
            else:
                self.connection = connection
        cursor = super(DatabaseWrapper, self)._cursor(*args, **kwargs)
        if connection is None and self.connection is not None:
            connection = self.connection
        return cursor

    def close(self):
        if self.connection is not None:
            self.connection.commit()
            self.connection = None

这里有一个线程安全的版本,但是Python线程不使用多个核心,因此您不会像之前那个版本一样获得如此大的性能提升。您也可以将其与多进程版本一起使用。

# Custom DB backend postgresql_psycopg2 based
# implements persistent database connection using thread local storage
from threading import local

from django.db.backends.postgresql_psycopg2.base import DatabaseError, \
    DatabaseWrapper as BaseDatabaseWrapper, IntegrityError
from psycopg2 import OperationalError

threadlocal = local()

class DatabaseWrapper(BaseDatabaseWrapper):
    def _cursor(self, *args, **kwargs):
        if hasattr(threadlocal, 'connection') and threadlocal.connection is \
            not None and self.connection is None:
            try: # Check if connection is alive
                threadlocal.connection.cursor().execute('SELECT 1')
            except OperationalError: # The connection is not working, need reconnect
                threadlocal.connection = None
            else:
                self.connection = threadlocal.connection
        cursor = super(DatabaseWrapper, self)._cursor(*args, **kwargs)
        if (not hasattr(threadlocal, 'connection') or threadlocal.connection \
             is None) and self.connection is not None:
            threadlocal.connection = self.connection
        return cursor

    def close(self):
        if self.connection is not None:
            self.connection.commit()
            self.connection = None

3
请不要这样做。这完全是不安全的。请使用像pgpool这样的适当连接池。 - Alex Gaynor
Pgpool不会有所帮助,因为Django每次都需要重新连接。我知道这不是线程安全的代码(我有一个使用psycopg2.pool模块的线程安全版本,只是还没有发布),但我使用的是在Daemon模式下使用纯预分配而不是线程的Python,所以这里是安全的。我会添加注释的 - 谢谢。 - HardQuestions
使用本地的pgbouncer,每当Django连接时,pgbouncer将使用池中现有的连接。由于您将拥有到pgbouncer的本地主机连接,因此连接不会受到太大的惩罚。 - Jerzyk

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