使用 PlanetScale 构建多区域 Rails 应用程序
你已经花费了大量精力,使你的 Rails 应用程序尽可能地快速:每个查询都经过优化,视图被缓存,N+1 查询问题已被修复。
然而,唯一无法解决的问题就是服务器与用户之间的光速限制。
请查看以下表格,它展示了一个部署于美国东部的应用单次请求所增加的网络延迟。如果用户靠近服务器,运行效果良好,但离服务器越远,效果就越差。
位置 | 到美国东部应用的延迟 |
北加州 | 52ms |
巴黎 | 83ms |
法兰克福 | 92ms |
新加坡 | 214ms |
开普敦 | 231ms |
如果我们能够将 Rails 应用程序部署到每个用户所在的地方,那不是很棒吗?如今,通过 Fly.io、Heroku 和 Render 等提供商实现应用服务器的全球分布变得非常简单。业内人士称这种方式为部署到“边缘”。
即使如此,我们仍然有一个主要问题需要解决:数据库。数据库也需要支持跨区域部署。
如果我们的应用程序运行在新加坡,但数据库位于美国东部,那么每个数据库查询都会产生大约 200ms 的延迟。
使用 Rails 的多区域数据库
为了实现应用的全球部署,我们必须将数据与应用程序共同定位。
这意味着我们需要做两件事情:
- 在应用程序运行的区域设置数据库副本
- 使 Rails 应用程序能够从最近的副本读取数据
我们的最终目标是让 Rails 应用程序将所有读取操作发送到最近的数据库副本,同时确保所有写入操作仍然指向主数据库。
设置数据库副本
如果尚未注册 PlanetScale 账户,请先注册。启动一个新的数据库,并按照我们的 Rails 快速入门教程连接数据库。一旦数据库与 Rails 应用程序完成连接,就可以开始配置以支持多区域部署了。
在 PlanetScale 中,我们可以在全球范围内设置只读副本。导航到数据库的主分支,点击底部的“添加区域”以创建副本,选择要添加的区域并获取连接凭证。
PlanetScale 会在选择的区域设置一个副本,并在主区域写入数据时自动保持同步。
为 PlanetScale 数据库添加区域
只读数据库连接
现在只读区域已在 PlanetScale 上配置完毕,您需要在应用程序中设置一个新的只读连接到副本。
修改 database.yml
文件,使其同时包含主数据库和只读副本连接:
default: &default adapter: trilogy encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: socket: /tmp/mysql.sock development: primary: <<: *default database: multi_region_rails_development primary_replica: <<: *default database: multi_region_rails_development replica: true test: primary: <<: *default database: multi_region_rails_test primary_replica: <<: *default database: multi_region_rails_test replica: true
在 application_record.rb
文件中添加以下内容:
# app/models/application_record.rb class ApplicationRecord < ActiveRecord::Base primary_abstract_class connects_to database: { writing: :primary, reading: :primary_replica } end
一旦 Rails 识别了副本连接,您可以通过包裹查询代码的块 connected_to(role: :reading)
手动查询副本:
ActiveRecord::Base.connected_to(role: :reading) do books = Book.where(author: "Taylor") # 此块中的所有代码将连接到副本 end
自动连接切换
手动包裹每个读取查询太过繁琐。Rails 提供了一种更好的方式:自动连接切换。它允许 Rails 根据需要自动切换主数据库与副本连接。所有写操作将指向主数据库,而读操作将命中副本。
要设置此功能,运行以下命令:
bin/rails g active_record:multi_db
然后在 application.rb
文件中取消注释以下内容:
Rails.application.configure do config.active_record.database_selector = { delay: 2.seconds } config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session end
注意这一行:config.active_record.database_selector = { delay: 2.seconds }
。它是支持应用程序读取自身写入的关键设置。
复制延迟与读取自身写入
大多数 Rails 应用的网络请求为 GET 请求。这些请求从数据库读取数据。
POST/PUT/PATCH 和 DELETE 请求则更新应用数据。在使用多个数据库连接时,复制延迟(Replication Lag)是一个常见问题。
使用数据库副本时,主数据库的数据写入到副本总会有一段短暂延迟,这就是复制延迟。这种延迟会根据主数据库的繁忙程度而变化。
当用户向数据库写入数据后立即尝试从副本读取相同数据时,可能会因为数据尚未复制到副本而导致错误响应。
为了解决这个问题,Rails 有一个中间件,它会在每次写入后自动设置一个 2 秒的 cookie。该 cookie 存在期间,Rails 会将所有读操作指向主数据库,而不是副本。
连接最近的数据库副本
现在应用能够连接到副本,我们需要配置应用以选择性地连接到最近的副本,以享受低延迟的好处。
为此,我们需要根据 Rails 应用程序的部署位置指定要使用的凭证集。在这个示例中,我们将连接详细信息存储在 Rails 的凭证中:
<% region = ENV["APP_REGION"] region_replica_mapping = { "fra" => Rails.application.credentials.planetscale_fra, "gra" => Rails.application.credentials.planetscale_gra } db_replica_creds = region_replica_mapping[region] || Rails.application.credentials.planetscale %> production: primary: <<: *default username: <%= Rails.application.credentials.planetscale&.fetch(:username) %> password: <%= Rails.application.credentials.planetscale&.fetch(:password) %> database: <%= Rails.application.credentials.planetscale&.fetch(:database) %> host: <%= Rails.application.credentials.planetscale&.fetch(:host) %> ssl_mode: <%= Trilogy::SSL_VERIFY_IDENTITY %> primary_replica: <<: *default username: <%= db_replica_creds.fetch(:username) %> password: <%= db_replica_creds.fetch(:password) %> database: <%= db_replica_creds.fetch(:database) %> host: <%= db_replica_creds.fetch(:host) %> ssl_mode: <%= Trilogy::SSL_VERIFY_IDENTITY %> replica: true
完成后,我们的全球部署应用可以从全球部署的数据库读写数据。这会显著加快同区域用户的 GET 请求响应速度,同时所有写操作仍然指向主数据库。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接
本文链接:http://choupangxia.cn/2025/09/10/planetscale-rails-application/