Rails中是否可能有多个数据库连接池供切换?

13

一些背景

我多年来一直在使用Apartment gem运行一个多租户应用程序。现在最近,将数据库扩展到单独的主机中的需求出现了,数据库服务器无法再保持同步(读写都变得太多了)-是的,我已经将硬件扩大到了极限(专用硬件、64个核心、12个Nvm-e驱动器组成RAID 10、384Gb ram等)。

我正在考虑为每个租户(1个租户=1个数据库连接配置/池)执行此操作,因为这将是一种“简单”且高效的方法,可使容量增加number-of-tenants-倍,而不必进行大量的应用代码更改。

现在,我正在运行rails 4.2,很快就会升级到5.2。我可以看到rails 6添加了支持每个模型连接定义,但这并不是我所需要的,因为我的20个租户中每个租户都有完全相同的数据库模式。通常情况下,我会根据请求(在中间件中)或后台作业(sidekiq中间件)切换“数据库”,但目前这很简单,并由Apartment gem处理,因为它只是在Postgresql中设置search_path并不会真正更改实际连接。当切换到每个租户的托管策略时,我将需要在每个请求中完全切换连接。

问题:

  1. 我知道每个请求/后台作业都可以使用ActiveRecord::Base.establish_connection(config),但是据我所知,这会触发在Rails中完全新的数据库连接握手并生成新的数据库池 - 对吧?我猜这样做会对我的应用程序在每个请求上产生巨大的开销,从而影响性能。
  2. 因此,我想知道是否有人可以看到在rails中例如预先建立多个(总共20个)数据库连接/池的选项(例如在应用程序引导时),然后只需在每个请求之间切换这些池?以便数据库连接已经建立并准备好使用。
  3. 这一切只是一个不好的主意吗?我是否应该寻找其他方法?例如,1个应用实例=一个特定租户的特定连接。或者其他什么。

2
https://guides.rubyonrails.org/active_record_multiple_databases.html 我认为这可能会对你有所帮助。 - Oleksandr Holubenko
1
你可能会对 Rails 的 GitHub 仓库中的这个 PR 感兴趣(https://github.com/rails/rails/pull/38531),它最近在当前的 Rails master 分支中添加了你需要的功能。运行 Rails Edge 是一个选项,或者将该功能回溯到你当前的 Rails 版本中? - spickermann
ActiveRecord::Base.connected_to(shard: :shard_one) do ... end 的意思是池将被(重新)使用,而不是每次创建一个全新的连接? - Ben
3个回答

8
据我所了解,多租户应用有四种模式:
1. 单独模式/多个生产环境 每个实例或数据库实例完全托管不同的租户应用程序,并且租户之间没有任何共享。这是为一个租户服务一个实例应用和一个数据库。如果你有100个租户,开发将很容易,如果你只服务于一个租户,但对于devops来说将是一场噩梦。
2. 租户的物理隔离 所有租户使用一个应用实例,但是每个租户有一个专属的数据库。这就是你正在寻找的模式。你可以使用ActiveRecord::Base.establish_connection(config),也可以使用gem,或升级到Rails 6,如下面的答案所述。
3. 隔离模式/逻辑隔离 在隔离模式中,租户表或数据库组件被分组到一个逻辑模式或名称空间下,并与其他租户模式分离,但是模式托管在同一个数据库实例中。对于所有租户使用一个实例应用和一个数据库,就像你使用Apartment gem一样。
4. 部分隔离组件 在该模型中,具有公共功能的组件在租户之间共享,而具有唯一或不相关功能的组件则被隔离。在数据层面上,例如识别租户的数据被分组或保存在单个表中,而租户特定数据则在表或实例层面上被隔离。
关于(1),如果正确使用ActiveRecord::Base.establish_connection(config),则不需要每个请求都进行数据库握手。你可以查看这里这里的所有评论
关于(2),如果不想使用establish_connection,可以使用gem multiverse(适用于rails 4.2),或其他gems。或者,如其他人建议的那样,升级到Rails 6。
补充:Multiverse gem正在使用establish_connection。它将附加database.yml并创建一个基类,以便每个子类共享相同的连接/池。基本上,它减少了我们使用establish_connection的工作量。
关于(3)的答案:

如果您没有太多租户,并且您的应用程序非常复杂,我建议您使用专属模式模式。因此,您可以选择一个应用实例=一个特定连接到一个特定租户。您不必通过添加多个数据库连接使您的应用更加复杂。

但是,如果您有很多租户,我建议您根据业务流程使用租户的物理隔离或部分隔离组件。

无论哪种方式,您都必须更新/重写应用程序以符合新的架构。


我有几个关于1和2的问题。1:我不确定我理解你的引用。你的意思是说,我可以在不进行数据库握手/重新创建数据库池的情况下调用.establish_connection(config)吗?如果是这样,我不确定这两个链接如何解释?2:对于multiverse,这不是每个模型数据库切换而不是整个应用程序的整个数据库切换吗?我觉得他们的文档非常含糊。 - Niels Kristian
我认为我有误解。您介意详细说明这些句子吗?我知道每个请求/后台作业都可以执行ActiveRecord :: Base.establish_connection(config)- 但是,正如我所理解的那样,这会触发完全新的数据库连接握手,并在Rails中生成新的db池 它暗示一个请求创建一个db池? - KSD Putra
我的意思是:(1)我担心在每个请求中都需要调用 ActiveRecord::Base.establish_connection(config) 来在不同的数据库/国家之间切换时会产生性能/网络开销。 - Niels Kristian
你不必担心开销。现在,如果你使用单个数据库,你只需要一个连接池(你可以查看上面(1)中关于连接的链接)。如果你在模型中像这样使用establish_connection: class SecondTenantUser < ActiveRecord::Base; establish_connection(DB_SECOND_TENANT); end,并且假设你有5个模型,那么你会创建5个连接池到DB_SECOND_TENANT。每个连接池都被平等地对待。因此,你不是每个请求创建一个连接池,而是每个establish_connection创建一个连接池。 - KSD Putra
编辑:我犯了一些严重的“笔误”。不是一个数据库对应一个池。您可以为每个数据库设置最大池数。例如,如果您有2个数据库,则为每个数据库设置最大5个池,那么您可以同时拥有10个池:DB A有5个池,DB B也有5个池。但是您不能为DB A设置6个池,为DB B设置4个池。 - KSD Putra
显示剩余2条评论

4

就在几天前,Ruby on Rails的master分支上添加了水平分片功能。目前,这个功能尚未正式发布,但根据您应用程序的Rails版本,您可能希望考虑将Rails master 添加到您的Gemfile中:

gem "rails", github: "rails/rails", branch: "master"

有了这个新功能,你可以利用Rails的数据库连接池并根据条件切换数据库。

我还没有使用过这个新功能,但它似乎相当简单明了:

# in your config/database.yml
production:
  primary:
    database: my_database
    # other config: user, password, etc
  primary_tenant_1:
    database: tenant_1_database
    # other config: user, password, etc

# in your controller for example when updating a tenant
ActiveRecord::Base.connected_to(shard: "primary_tenant_#{tenant.database_shard_number}") do
  tenant.save
end

您没有提供有关如何确定租户编号或如何在应用程序中进行授权的详细信息。但是我建议您尽快在application_controller中使用around_action来确定租户编号。以下是一个起点示例:

around_filter :determine_database_connection

private

def determine_database_connection
  # assuming you have a method to determine the current_tenant and that tenant
  # has a method that returns the number of the shard to use or even the 
  # full shard identifier
  shard = current_tenant.database_shard # returns for example `:primary_tenant_1` 

  ActiveRecord::Base.connected_to(shard: shard) do
    yield
  end
end

在这种情况下,切换回默认连接会有相同的意义吗?https://github.com/influitive/apartment#middleware-considerations - Ben
1
一旦您离开 ActiveRecord::Base.connected_to ... do 块,它将再次使用默认连接。 - spickermann
@spickermann,我在阅读关于这个宝石的内容,它只适用于Rails6吗? - 7urkm3n
@7urkm3n 它已经包含在当前的 Rails master 分支中。 - spickermann
谢谢您的回答。在我能够奖励其中一个答案的赏金之前,我需要一些时间来实际测试建议是否是好的解决方案。 - Niels Kristian

3

谢谢,不过这似乎与我的使用情况相去甚远。这将意味着需要重写整个应用程序,以在各处使用此过程。 - Niels Kristian

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