Django/WSGI 应用程序中的持久数据库连接

Persistent DB Connection in Django/WSGI application

我想在 django 支持的 Web 应用程序中保持与第三方遗留数据库的持久连接。

我想保持 Web 应用程序和旧数据库之间的连接打开,因为创建新连接对于这个特殊的数据库来说非常慢。

它不像通常的连接池,因为我需要存储每个网络用户的连接。用户 "Foo" 需要在 Web 服务器和旧数据库之间建立自己的连接。

到目前为止,我使用 Apache 和 wsgi,但如果其他解决方案更适合,我可以更改。

到目前为止,我使用的是 django。在这里我也可以改变。但是痛苦会更大,因为已经有很多代码需要重新集成。

到目前为止,我使用 Python。我想 Node.js 会更适合这里,但改变的痛苦太大了。

当然需要某种超时。如果 N 分钟内没有来自用户 "Foo" 的 http 请求,则需要关闭持久连接。

如何解决?

更新

我称之为 DB 但它不是通过 settings.DATABASES 配置的数据库。这是一个奇怪的、遗留的、没有广泛传播的类数据库系统,我需要集成它。

如果此时有 50 人在线使用 Web 应用程序,那么我需要有 50 个持久连接。每个用户一个。

连接数据库的代码

我可以在每个请求中执行这一行:

strangedb_connection = strangedb.connect(request.user.username)

但是这个操作很慢。使用连接很快。

当然 strangedb_connection 不能序列化也不能存储在会话中:-)

据我所知,您已经排除了大多数(全部?)此类问题的常见解决方案:

  • 在字典中存储连接...需要 N 个工作人员并且不能保证哪个请求发送给哪个工作人员
  • 在缓存中存储数据...数据太多
  • 在缓存中存储连接信息...连接不可序列化

据我所知,实际上只有 1 'meta' 解决方案,使用@Gahbu 的字典建议,并保证给定 user 的请求发送给同一个工作人员。 IE。想出一种方法,每次都以相同的方式从 User 对象映射到给定的工人(也许用工人的数量散列他们的名字和 MOD?)。

如果当前活动的用户都映射到同一个工作人员,此解决方案将无法充分利用您的 N 个工作人员,但如果所有用户同时活动的可能性相同,那么工作 应该 平均分布。 (如果它们的可能性不完全相同,那么映射 可能 能够解释这一点)。

我能想到的两种可能的方法是:

1.编写自定义请求分配器

我不太熟悉 apache/wsgi 接口领域,但是......可以替换 Apache 服务器中的组件,该组件使用一些自定义逻辑将 HTTP 请求分派给工作人员,例如它总是分派到同一个进程。

2。 运行一个load-balancer/proxy在N个单线程worker前面

我不确定您是否可以在这里使用现成的软件包,但概念是:

  • 运行 实现此 'bind the User to an index' 逻辑的代理
  • 然后让代理将请求转发到您的 Apache/wsgi 网络服务器的 N 个副本之一,每个副本都有一个工作人员。

注意:我在这里遇到的第二个想法:https://github.com/benoitc/gunicorn/issues/183

总结

对于这两个选项,在现有应用程序中的实现都非常简单。您的应用程序只是更改为使用字典来存储持久连接(如果还没有则创建一个)。测试单个实例在开发中与在生产中相同。在生产中,实例本身 none 更明智,因为它们总是被问及相同的用户。

我喜欢这里的选项 2,原因如下:

  • 也许有一个现有的服务器包允许您定义这个代理技巧
  • 如果没有,创建一个自定义代理应用程序以放置在您当前应用程序的前面可能不会太难(特别是考虑到您在请求到达 strangedb 服务时(已经)受到的限制)

您可以使用 WSGIDaemonProcess 指令来拥有多个工作线程 线程 而不是拥有多个工作线程 进程 运行 在单个进程中。这样,所有线程都可以共享相同的数据库连接映射。

在你的 apache 配置中有这样的东西...

# mydomain.com.conf

<VirtualHost *:80>

    ServerName mydomain.com
    ServerAdmin webmaster@mydomain.com

    <Directory />
        Require all granted
    </Directory>

    WSGIDaemonProcess myapp processes=1 threads=50 python-path=/path/to/django/root display-name=%{GROUP}
    WSGIProcessGroup myapp
    WSGIScriptAlias / /path/to/django/root/myapp/wsgi.py

</VirtualHost>

...然后您可以在 Django 应用程序中使用像这样简单的东西...

# views.py

import thread
from django.http import HttpResponse

# A global variable to hold the connection mappings
DB_CONNECTIONS = {}

# Fake up this "strangedb" module
class strangedb(object):

    class connection(object):
        def query(self, *args):
            return 'Query results for %r' % args

    @classmethod
    def connect(cls, *args):
        return cls.connection()


# View for homepage
def home(request, username='bob'):

    # Remember thread ID
    thread_info = 'Thread ID = %r' % thread.get_ident()

    # Connect only if we're not already connected
    if username in DB_CONNECTIONS:
        strangedb_connection = DB_CONNECTIONS[username]
        db_info = 'We reused an existing connection for %r' % username
    else:
        strangedb_connection = strangedb.connect(username)
        DB_CONNECTIONS[username] = strangedb_connection
        db_info = 'We made a connection for %r' % username

    # Fake up some query
    results = strangedb_connection.query('SELECT * FROM my_table')

    # Fake up an HTTP response
    text = '%s\n%s\n%s\n' % (thread_info, db_info, results)
    return HttpResponse(text, content_type='text/plain')

...第一次命中时,会产生...

Thread ID = 140597557241600
We made a connection for 'bob'
Query results for 'SELECT * FROM my_table'

...然后,在第二个...

Thread ID = 140597145999104
We reused an existing connection for 'bob'
Query results for 'SELECT * FROM my_table'

显然,当不再需要数据库连接时,您需要添加一些东西来断开它们,但是如果没有关于您的应用程序应该如何工作的更多信息,很难知道最好的方法.

更新#1:关于I/O多路复用与多线程

I worked with threads twice in my live and each time it was a nightmare. A lot of time was wasted on debugging non reproducible problems. I think an event-driven and a non-blocking I/O architecture might be more solid.

使用 I/O 多路复用的解决方案可能更好,但会更复杂,并且还需要您的 "strangedb" 库来支持它,即它必须能够处理 EAGAIN/EWOULDBLOCK 并有能力在必要时重试系统调用。

Python 中的多线程远没有大多数其他语言危险,因为 Python 的 GIL,本质上,这使得所有 Python 字节码线程安全的。

实际上,当底层 C 代码使用 Py_BEGIN_ALLOW_THREADS 宏时,只有线程 运行 并发,而宏与其对应的 Py_END_ALLOW_THREADS 通常包裹在系统调用中,并且CPU密集型操作。

这样做的好处是在 Python 代码中几乎不可能发生线程冲突,尽管缺点是它不会总是在一个 CPU 上最佳地利用多个内核单机.

我建议上述解决方案的原因是它相对简单,并且需要最少的代码更改,但如果您可以详细说明 "strangedb" 库,可能会有更好的选择。拥有一个需要每个并发用户单独网络连接的数据库似乎很奇怪。

更新#2:关于多处理与多线程

...the GIL limitations around threading seem to be a bit of an issue. Isn't this one of the reasons why the trend is to use separate processes instead?

这很可能是 Python 的 multiprocessing module exists, i.e. to provide concurrent execution of Python bytecode across multiple CPU cores, although there is an undocumented ThreadPool class 在该模块中使用线程而不是进程的主要原因。

如果您确实需要在每个 CPU 内核上利用每个 CPU 周期,例如,"GIL limitations" 肯定会出现问题。如果您正在编写一款必须每秒渲染 60 帧的高清电脑游戏。

但是,大多数基于 Web 的服务可能会将大部分时间花在等待某事发生上,例如网络 I/O 或磁盘 I/O,Python 个线程将允许并发发生。

归根结底,这是性能和可维护性之间的权衡,考虑到硬件通常比开发人员的时间便宜得多,相比性能,更注重可维护性通常更具成本效益。

坦率地说,当您决定使用虚拟机语言(例如 Python)而不是编译成真实机器代码的语言(例如 C)时,您就已经在说您是准备牺牲一些性能来换取方便。

另请参阅 The C10K problem 以比较扩展基于 Web 的服务的技术。

一个简单的方法是让另一个 python 进程管理持久连接池(每个用户一个,并且可以在需要时超时)。然后另一个 python 进程和 django 可以与 zeromq 等快速通信。 interprocess communication in python

管理连接的工作守护进程

您的照片目前看起来像:

user  ----------->  webserver  <--------[1]-->  3rd party DB

connection [1] is expensive.

您可以通过以下方式解决此问题:

user ---->  webserver  <--->  task queue[1]  <--->  worker daemon  <--[2]-> 3rd party DB

[1] task queue can be redis, celery or rabbitmq.
[2] worker daemon keeps connection open.

工作守护进程将连接到第 3 方数据库并保持连接打开。这意味着每个请求都不必支付连接费用。任务队列将是进程间通信,将工作分派给守护进程并在第 3 方数据库中进行查询。网络服务器在处理方面应该尽可能轻,让工人做昂贵的任务。

使用 apache + modwsgi 预加载

您实际上可以 preload 并在第一个请求之前完成昂贵的连接。这是通过 WSGIImportScript configuration directive 完成的。我不记得有预加载+分叉配置是否意味着每个请求都已经打开连接并共享它;但由于您拥有大部分代码,这可能是一个简单的实验。

使用 uwsgi 预加载

uwsgi 也支持预加载。这是通过 the import directive.

完成的