多租户 Django 应用程序:根据请求更改数据库连接?

Posted

技术标签:

【中文标题】多租户 Django 应用程序:根据请求更改数据库连接?【英文标题】:Multi-tenant Django applications: altering database connection per request? 【发布时间】:2013-05-19 05:36:08 【问题描述】:

我正在寻找其他尝试使用数据库级隔离构建多租户 Django 应用程序的工作代码和想法。

更新/解决方案:我在一个新的开源项目中解决了这个问题:见django-db-multitenant

目标

我的目标是根据请求主机名或请求路径(例如,foo.example.com/ 将 Django 连接设置为使用数据库 foo , 而bar.example.com/ 使用数据库bar)。

先例

我知道一些现有的 Django 多租户解决方案:

    django-tenant-schemas:这非常接近我想要的:你以最高优先级安装它的中间件,它会向数据库发送一个SET search_path 命令。不幸的是,它是 Postgres 特有的,我被 mysql 困住了。 django-simple-multitenant:这里的策略是为所有模型添加一个“租户”外键,并调整所有应用程序业务逻辑以关闭它。基本上每一行都被(id, tenant_id)而不是(id)索引。我尝试过但不喜欢这种方法,原因有很多:它使应用程序更加复杂,可能导致难以发现的错误,而且它不提供数据库级别的隔离。 每个租户一个 app server, django settings file with appropriate db。又名穷人的多租户(实际上是富人的,考虑到它所涉及的资源)。我不想为每个租户启动一个新的应用服务器,为了可扩展性,我希望任何应用服务器都能够为任何客户端分派请求。

想法

到目前为止,我最好的想法是做类似django-tenant-schemas 的事情:在第一个中间件中,抓住django.db.connection 并摆弄数据库选择而不是架构。我还没有完全考虑到这对于池/持久连接意味着什么

我追求的另一个死胡同是特定于租户的表前缀:除了我需要它们是动态的之外,即使是全局表前缀在 Django 中也不容易实现(请参阅rejected ticket 5000 等)。

最后,Django multiple database support 允许您定义多个命名数据库,并根据实例类型和读/写模式在它们之间多路复用。没有帮助,因为没有工具可以根据每个请求选择数据库。

问题

有人做过类似的事情吗?如果有,您是如何实现的?

【问题讨论】:

正如最初写的那样,它不适合 Stack Overflow。我已经删除了令人震惊的部分。 感谢您添加赏金。 关于“令人震惊”的部分:没有问题,尽管我仍然对我所要求的“令人沮丧”的建议感兴趣,即使本质上是轶事,因为 IMO 这是一个设计/架构问题:处理多租户应用程序设计有多种根本不同的方法,因此第一手经验对于衡量可能不会立即显而易见的设计权衡很有价值。这是一个对 HN 有所帮助的讨论:news.ycombinator.com/item?id=4270003 这样的讨论不适合 Stack Overflow。因此我把它们删掉了。 这可能不是特定于您的情况,但您确实应该再次考虑最后一个选项。我在一家大型金融机构工作,当我们评估供应商时,应用程序层的共享内存空间对我们来说是一个巨大的问题。我理解您对可扩展性的担忧,但如果您使用 Puppet 或 Chef 之类的东西,您可以自动执行这些部署,只需向您的第一层 Web 服务器添加一个条目。由于内存和计算现在如此便宜,额外 Django 实例的少量额外资源对成本的影响最小。 【参考方案1】:

您可以自己创建一个简单的中间件,从您的子域或其他任何内容中确定数据库名称,然后针对每个请求在数据库游标上执行USE 语句。查看 django-tenants-schema 代码,这基本上就是它正在做的事情。它是 psycopg2 的子类,并发出等同于 USE 的 postgres,“set search_path XXX”。您也可以创建一个模型来管理和创建您的租户,但是您将重写大部分 django-tenants-schema。

在 MySQL 中切换模式(数据库名称)不应该有性能或资源损失。它只是为连接设置一个会话参数。

【讨论】:

同意,尽管这似乎是 OP 已经将其视为他的第一个想法。他指出“我还没有完全考虑到这在池/持久连接方面意味着什么” - 你能说明一下吗? 是的,这基本上就是我在“想法”下的第一段中所描述的内容。这可能是我要走的路线,至少作为第一次实验。我很想知道是否有人在实践中这样做过;看来我们都同意它与 postgres 模式方法没有太大不同。 另外:“不应该有性能或资源损失”——嗯,它不是免费的,但也许它是最便宜的选择;毕竟我们必须执行USE。当与持久数据库连接结合使用时,我需要某种 LRU 连接缓存,每个连接都绑定到设置时的特定租户。这是我挥手走过的部分,也是我很好奇是否有先例的领域。【参考方案2】:

我已经做了最接近第 1 点的类似操作,但没有使用中间件来设置默认连接,而是使用了 Django 数据库路由器。如果每个请求需要,这允许应用程序逻辑使用多个数据库。为每个查询选择合适的数据库取决于应用程序逻辑,这是这种方法的一大缺点。

通过此设置,所有数据库都列在settings.DATABASES 中,包括可能在客户之间共享的数据库。每个客户特定的模型都放置在具有特定应用标签的 Django 应用中。

例如。下面的类定义了一个存在于所有客户数据库中的模型。

class MyModel(Model):
    ....
    class Meta:
        app_label = 'customer_records'
        managed = False

一个数据库路由器被放置在settings.DATABASE_ROUTERS 链中以路由app_label 的数据库请求,类似这样(不是完整示例):

class AppLabelRouter(object):
    def get_customer_db(self, model):
        # Route models belonging to 'myapp' to the 'shared_db' database, irrespective
        # of customer.
        if model._meta.app_label == 'myapp':
            return 'shared_db'
        if model._meta.app_label == 'customer_records':
            customer_db = thread_local_data.current_customer_db()
            if customer_db is not None:
                return customer_db

            raise Exception("No customer database selected")
        return None

    def db_for_read(self, model, **hints):
        return self.get_customer_db(model, **hints)

    def db_for_write(self, model, **hints):
        return self.get_customer_db(model, **hints)

关于这个路由器的特殊部分是thread_local_data.current_customer_db() 调用。在使用路由器之前,调用者/应用程序必须已经在thread_local_data 中设置了当前客户数据库。为此,可以使用 Python 上下文管理器来推送/弹出当前客户数据库。

配置完所有这些后,应用程序代码如下所示,其中UseCustomerDatabase 是一个上下文管理器,用于将当前客户数据库名称推送/弹出到thread_local_data,以便thread_local_data.current_customer_db() 将返回正确的数据库名称当路由器最终被击中时:

class MyView(DetailView):
    def get_object(self):
        db_name = determine_customer_db_to_use(self.request) 
        with UseCustomerDatabase(db_name):
            return MyModel.object.get(pk=1)

这已经是一个相当复杂的设置了。它有效,但我会尝试总结一下我认为的优点和缺点:

优势

数据库选择很灵活。它允许在单个查询中使用多个数据库,客户特定数据库和共享数据库都可以在请求中使用。 数据库选择是明确的(不确定这是优点还是缺点)。如果您尝试运行查询客户数据库但应用程序没有选择一个,则会发生异常,指示编程错误。 使用数据库路由器允许不同的数据库存在于不同的主机上,而不是依赖于猜测所有数据库都可以通过单个连接访问的USE db; 语句。

缺点

设置起来很复杂,而且要使其发挥作用涉及很多层。 线程本地数据的需求和使用并不明确。 视图中充斥着数据库选择代码。这可以使用基于类的视图进行抽象,以根据请求参数自动选择数据库,就像中间件选择默认数据库一样。 选择数据库的上下文管理器必须以这样一种方式包装查询集,即在评估查询时上下文管理器仍然处于活动状态。

建议

如果您想要灵活的数据库访问,我建议使用 Django 的数据库路由器。使用中间件或视图 Mixin,它会根据请求参数自动设置用于连接的默认数据库。您可能不得不求助于线程本地数据来存储要使用的默认数据库,以便当路由器被击中时,它知道要路由到哪个数据库。这允许 Django 使用其现有的持久连接到数据库(如果需要,可以驻留在不同的主机上),并根据请求中设置的路由选择要使用的数据库。

这种方法的另一个优点是,如果需要,可以使用QuerySet using() 函数选择默认数据库以外的数据库来覆盖查询的数据库。

【讨论】:

感谢您富有洞察力的回答!我现在将其标记为答案,意识到没有单一的“正确”架构;您很好地概述了这种方法。 这是我最终实现的:github.com/mik3y/django-db-multitenant【参考方案3】:

为了记录,我选择实现我的第一个想法的变体:在早期请求中间件中发出USE <dbname>。我也用同样的方法设置了 CACHE 前缀。

我在一个小型生产站点上使用它,根据请求主机从 Redis 数据库中查找租户名称。到目前为止,我对结果非常满意。

我已经把它变成了一个(希望可以重复使用的)github项目:https://github.com/mik3y/django-db-multitenant

【讨论】:

以上是关于多租户 Django 应用程序:根据请求更改数据库连接?的主要内容,如果未能解决你的问题,请参考以下文章

多租户模式:使用 django rest 框架的动态 api 路由

django 上多租户应用程序的最佳架构

在使用弹簧数据存储库之前更改当前模式 - 多租户

在控制器中更改 ASP.NET Core 2.0 标识连接字符串

Grails:运行时改变dataSource url实现多租户数据库分离

如何将多租户 django 应用程序部署到 AWS?