扩展Rails:41M请求/小时,8个数据库,disable_joins: true

Lobsters Hottest 新闻

摘要

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台

Hacker News Top

PlanetScale介绍了如何使用数据库分片将关系型数据库从单台服务器扩展到768台服务器,并解决诸如写入限制和资源争用等瓶颈问题。

从Rust到Ruby

Hacker News Top

开发人员描述使用LLM将一个15,000行的Rust Web应用转换为Ruby on Rails,发现Ruby版本明显更短,并评估了开发速度、安全性和可测试性方面的权衡。

每秒百万次请求的客户端负载均衡

Hacker News Top

Zalando 的工程团队描述了如何实现客户端负载均衡,以取代其高流量 Product Read API 的共享边缘负载均衡器。通过 Skipper 消除扇出瓶颈,从而降低延迟并提高可观测性。