扩展Rails:41M请求/小时,8个数据库,disable_joins: true
摘要
Aura Frames将其Ruby on Rails应用扩展至每小时4100万请求,通过拆分为8个主数据库并利用Active Record中的disable_joins: true特性。
<p><a href="https://lobste.rs/s/zijb20/scaling_rails_41m_req_hour_8_dbs_disable">评论</a></p>
查看缓存全文
缓存时间: 2026/06/24 22:02
# 在Aura Frames扩展Rails:拆分为8个主数据库并在App Store登顶
来源:https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails
- 使用Ruby on Rails构建 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#building-with-ruby-on-rails)
- 技术指标 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#technical-metrics)
- 开始使用多数据库 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#getting-started-with-multiple-databases)
- 从SQL连接查询到多个SELECT (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#from-sql-joins-to-multiple-selects)
- 新数据库配置 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#new-database-configuration)
- 拆分后:子查询表达式 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#post-split-subquery-expressions)
- 拆分后:EXISTS子句 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#post-split-exists-clauses)
- 拆分后:聚合和分组多个表 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#post-split-aggregating-and-grouping-multiple-tables)
- 拆分后:合并作用域 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#post-split-merging-scopes)
- 拆分后:References方法 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#post-split-references-method)
- 其他关联:has\_and\_belongs\_to\_many (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#other-associations-has_and_belongs_to_many)
- 扩展插入和更新 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#scaling-inserts-and-updates)
- 通过批处理扩展读取 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#scaling-reads-with-batching)
- 通过分页查询扩展读取 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#scaling-reads-with-paginated-queries)
- 频繁更新的计数器的计数器缓存维护 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#counter-cache-maintenance-for-frequently-updated-counters)
- 随机值与采样 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#random-values-and-sampling)
- 使用内存键值缓存存储 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#using-memory-key-value-cache-stores)
- 使用多数据库管理模式变更 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#managing-schema-changes-with-multiple-databases)
- 总结 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#wrap-up)
- 更新 (https://andyatkinson.com/how-aura-frames-scales-for-peak-load-ruby-on-rails#updates)
**📌 概述**
Ruby on Rails帮助扩展了数据库层,满足了数百万Aura Frames客户使用数字相框的需求。2025年底,团队增加了额外的主数据库,以扩展在圣诞节(公司一年中最繁忙的一天)的高峰写入和读取负载。Rails在同一个代码库内管理每个主数据库的查询和模式变更,现在有了多个主数据库的额外容量。总共有8个主数据库,每个服务器实例可以在高峰负载前进行垂直扩展。当负载恢复正常水平时,实例会按比例缩减以节省成本。团队利用了Ruby on Rails ORM(Active Record)对多数据库的原生支持以及`disable_joins: true`特性。`disable_joins`特性替代了SQL连接,发出多个SELECT语句,以在应用程序中从不同数据库组合数据。本文回顾了该计划的技术细节,以及一系列其他数据层扩展策略,最终在2025年圣诞节成功实现,并在美国、加拿大Apple App Store和Google Play Store中排名第一。
## 使用Ruby on Rails构建
Aura Frames平台自始至终(超过10年!)都使用Ruby on Rails构建。2025年圣诞节是公司和技术平台一年中最繁忙的一天,峰值API请求达到每小时4100万(约11,400请求/秒),峰值后台作业处理量达到每小时1180万(约3300作业/秒)。在数据库方面,数据库每秒事务处理总和为226K。关于Aura Frames公司和产品的介绍,以及Postgres方面的深入探讨,请参阅本系列的第一部分 (https://andyatkinson.com/postgresql-rds-scaling-aws-christmas-day-peak#postgres-scaling-challenges-and-solutions)。
**第一部分简要回顾**:除了Ruby on Rails,Aura Frames还使用PostgreSQL和AWS作为关键技术。由于PostgreSQL和Active Record的写操作不易水平扩展,数据库层常常成为瓶颈。团队在2024年圣诞节之前一直依赖垂直扩展单个主服务器实例。当时RDS可用的最大实例是48x系列(192 vCPU,1.5 TB RAM)。即使使用这样的超大实例,平台在2024年圣诞节高峰负载下仍存在可靠性问题,因此需要在2025年圣诞节之前重新设计以提高可靠性。
为了可靠地处理更高水平的高峰流量,团队决定通过使用多个主数据库引入应用层分片。考虑了几种替代方案。一个目标是尽可能多地利用现有代码,进行最小改动,并在应用层控制分片分布。另一个选择是是否在行级别进行传统分片,这将多实例间分布行,且数据库具有相同模式。幸运的是,Ruby on Rails经过超过15年的增强,已经能够支持拥有数十亿行和TB级数据的成熟、扩展平台的需求。
在深入解决方案细节之前,我们先看一下2025年圣诞节的一些技术指标,以帮助设置上下文。
## 技术指标
在圣诞节,Aura Frames平台的负载增加了4-5倍。以下是Rails开发者可能会感兴趣的一些HTTP和后台作业相关指标。
| 指标 | 峰值 |
| --- | --- |
| HTTP请求(CT下午1点,负载均衡器) | 4100万请求/小时 |
| 平均响应时间(CT上午10点至晚上9点) | 650毫秒 |
| Cloudfront全球请求 | 33,675,000请求/小时 |
| 图像处理EC2实例数 | 2990 |
| API EC2实例数 | 1849 |
| 后台作业处理速率 | 1180万作业/小时(约3300作业/秒) |
团队一个激动人心的进展是看到免费的iOS和Android Aura Frames应用在全天排名上升。圣诞节深夜,该应用在美国和加拿大App Store的所有免费应用中达到排名第一,超越了OpenAI(ChatGPT)和Meta(Meta AI)等大公司的应用!
[图片:Aura Frames在美国App Store排名第一的截图]
尽管Aura Frames的Ruby on Rails代码库在过去十年的演进中有很多有趣的历史,但本文将重点介绍从2025年中开始为圣诞节流量激增所做的更改,以及一些通用的数据层扩展策略。
## 开始使用多数据库
如前所述,Aura Frames平台从单个主应用数据库扩展到总共8个。为此,对Active Record查询层代码进行了大量重构。表查询不能跨越数据库边界,而由于一些大表被迁移到新数据库,查询将会中断。
开发环境使用Docker Postgres容器。为了在本地保持简单,所有8个数据库都运行在单个容器内,但作为独立的Postgres数据库分布。这意味着当查询跨越数据库边界时,它们仍然会“友好地”中断,使得通过单元测试和手动测试轻松定位问题。
这些更改的要点相当直接:找到中断的查询,解开连接查询或其他不兼容的SQL,并更改这些查询的连接以访问正确的数据库。然后,查询结果作为Ruby对象传递给其他数据库的查询作为输入。由于要处理数百个失败的测试,重构工作花费了很长时间,但进展容易衡量。最终,几周后,所有测试都通过了!这种设计的一个良好特性是,相同的查询更改(无需SQL连接)可以在现有的主数据库上执行,这意味着查询更改是向后兼容的,并且可以在单个主数据库上推出。
一些主要的更改包括评估所有Active Record关联(`has_many`、`belongs_to`等)以及任何子查询表达式或其他不兼容代码,并使用`disable_joins: true`,移除跨越边界的子查询表达式和表引用。
## 从SQL连接查询到多个SELECT
Ruby on Rails和Active Record中使这一切成为可能的关键部分是6.0版本发布的多数据库支持,以及7.0版本(2021年) (https://www.bigbinary.com/blog/rails-7-adds-disable-joins-for-associations) 为`has_many :through`和`has_one :through`关系添加的`disable_joins`功能,用于跨数据库查询。这些功能在发布之前可以通过自定义代码或第三方库(Ruby gems)实现,但Rails的原生支持是一个差异化优势。原生支持意味着更多的实际使用、错误修复、改进的文档以及长期的支持承诺。`disable_joins`作为一种一致的模式也帮助团队理解,实现了“一次学会,整个代码库复用”。
由于SELECT查询的增加(以及连接查询效率的损失),团队担心额外的读取查询量。幸运的是,团队已经有了负载测试工具,并能够通过负载测试验证额外的读取查询不会成为问题。尽管如此,随着时间的推移,我们根据慢查询日志或查询取消情况,将某些`disable_joins`关联的使用替换为更针对性的查询。这些查询有索引支持,选择最小字段,并通过批处理限制行范围。
下面是一个使用`Author`和`Post`模型的简单示例,说明`disable_joins: true`的工作原理:
- Author (`table_name: authors`)
- Post (`table_name: posts`)
- AuthorPost (`table_name: author_posts`)
Author模型有一个现有关联定义为:`has_many :posts, through: :author_posts`。要更改此关联,添加`disable_joins: true`选项,如下所示:
```ruby
class Author < ApplicationRecord
has_many :posts, through: :author_posts, disable_joins: true
end
```
在SQL中发生了什么?以前,要获取Author的帖子,我们会查询`author_posts`表,并根据作者的id连接`posts`表。现在,将有两个SELECT查询。一个通过`author_id`(需要索引的重要外键列)查询`author_posts`,获取帖子`id`值。然后第二个查询通过`id`(使用主键索引)查询`posts`表,获取第二个表的行。
```sql
select * from author_posts where author_id = '';
select * from posts where id IN (?);
```
Active Record处理查询更改,并以相同的方式向开发者呈现对象和集合。除了查询更改,还需要更改什么?
## 新数据库配置
尽管我们最初在单个主数据库架构上推出了查询更改,但这是为了进一步验证更改,而不需要立即部署新数据库。主要计划是使用独立的数据库服务器实例,将最大、最繁忙的表迁移到自己的实例,以增加容量并分散负载。为此,我们需要配置所有新数据库并从Rails连接它们。
首先需要为每个新数据库添加新的YML配置项(`config/database.yml`)。多数据库文档 (https://guides.rubyonrails.org/active_record_multiple_databases.html) 使用“animals”和`my_animals_db`作为第二个主数据库,我们在这里也使用相同的示例。此配置将存储Postgres连接字符串详细信息以及其他应用程序配置,例如是否使用迁移、模式转储路径等。
其次,之前继承自(OOP风格)`ApplicationRecord < ActiveRecord::Base`的Active Record类将获得一个新的父类。新父类将引入`my_animals_db`的新数据库配置,并包含“写入”和“读取”角色的概念,如下所示。新类是`AnimalsRecord`,它继承自`ApplicationRecord`。扩展这个新父类成为任何想要读写此新数据库的Active Record类的“接口”。
来自Rails文档的示例:
```ruby
class AnimalsRecord < ApplicationRecord
self.abstract_class = true
connects_to database: { writing: :animals, reading: :animals_replica }
end
```
模型/表继承自`AnimalsRecord`以使用该数据库。例如,一个`Dog`类:
```ruby
class Dog < AnimalsRecord
# 自动与animals数据库交互
end
```
最终,所有新数据库和基础设施(PgBouncer、配置变量)都已设置完成,可以切换过去。在Rails应用中,新数据库被命名得比较通用,因为它们并不对应特定的活动分组(如服务),我们只是需要一堆具有唯一名称的数据库。数据库的名称是父模型的一部分,现在是原始模型的父类。由于原始模型除了新父类之外没有变化,新数据库的细节被很好地封装了。
`disable_joins`用于`has_many :through`关系最终覆盖了查询数据库分离所需的大部分内容,但在此过程中还遇到了其他问题。它们是什么?
## 拆分后:子查询表达式
子查询表达式(又称“子查询”)中存在多个表,这些表需要在同一个数据库中才能使语句有效。当我们发现这些时,需要重新组织,以便可以查询包含该表的数据库。
## 拆分后:EXISTS子句
拆分后,当`users`和`posts`(示例模型)不在同一个数据库时,下面的SQL无法工作。
```sql
SELECT users.*
FROM users
WHERE EXISTS (
SELECT 1 FROM posts
WHERE posts.user_id = users.id
)
```
## 拆分后:聚合和分组多个表
拆分后,可能存在这样的SQL片段,当这些表被移动到单独的数据库时,需要更改此代码。SQL片段被包装在`Arel.sql('')`中。
```ruby
Users.select("users.*, COUNT(posts.id) AS posts_count")
```
## 拆分后:合并作用域
如果`User`和`Post`的表不在同一个数据库,就不能合并这样的作用域:
## 拆分后:References方法
`references` API 文档 (https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-references) 添加了一个SQL连接,与`includes()`一起使用以指定一个表。但是,如果该表不再位于同一个数据库中,这将无法工作。
## 其他关联:has\_and\_belongs\_to\_many
相似文章
让768台服务器看起来像1台
PlanetScale介绍了如何使用数据库分片将关系型数据库从单台服务器扩展到768台服务器,并解决诸如写入限制和资源争用等瓶颈问题。
从Rust到Ruby
开发人员描述使用LLM将一个15,000行的Rust Web应用转换为Ruby on Rails,发现Ruby版本明显更短,并评估了开发速度、安全性和可测试性方面的权衡。
@alphabatcher:54万行Rails代码是进入智能体时代的残酷方式 Garry's List 发布时包含:> 26.2万行应用代码 > …
对AI智能体时代大型Rails代码库的批评,提出转向基于技能的开发方式,使用智能体、Markdown技能和TypeScript实现确定性I/O。
@_avichawla: Cloudflare如何在不离开Postgres的情况下将查询时间缩短35倍:他们的Postgres表达到数十亿行,而每次时间范围查询开始变慢。
Cloudflare使用TimescaleDB(Tiger Cloud)在数十亿行的Postgres表上将查询性能提升了35倍,并借助Claude Code和Tiger CLI构建了一个实时地震仪表板。
每秒百万次请求的客户端负载均衡
Zalando 的工程团队描述了如何实现客户端负载均衡,以取代其高流量 Product Read API 的共享边缘负载均衡器。通过 Skipper 消除扇出瓶颈,从而降低延迟并提高可观测性。