对一个真实的 Rails 应用进行分片

2025 年 4 月 30 日
Lev Kokotov

互联网上有人说 Rails 应用程序无法扩展,这是错误的。可以根据 CPU 数量(和资金)启动尽可能多的 Docker 容器。真正需要的是更多的数据库,而这正是 PgDog 所擅长的。

如果之前来过这里,可以跳过介绍部分。对于其他人,PgDog 是一个可以分片数据库的 Postgres 代理,它可以解析 SQL 并自动根据查询参数确定查询应去的位置。

为了证明这确实有效,决定对使用人数达数百万的 Mastodon 应用程序进行数据库分片。这是一个分为两部分的系列的第一部分,在走向成功的道路上,会引入新功能,使 PgDog 成为数据库不可或缺的扩展引擎。

设置

首先需要在 Mastodon 和数据库之间部署 PgDog。如果是 Rails/Postgres 专家,可以直接跳到更有趣的部分(#分片键)。在本地进行此操作时,只需启动池化器并将config/database.yml中的端口更改为6432

development:
  primary:
    <<: *default
    port: 6432 # PgDog 运行的端口
pgdog.toml
[[databases]]
name = "mastodon_development"
host = "127.0.0.1"
port = 5432

现在 PgDog 位于 Mastodon 和 Postgres 之间,可以看到通过的每个查询。准备启动应用程序,但尚未进行分片,所有查询都将发送到一个数据库。

分片键

分片键是 Postgres 表中的一列,具体来说是该表中每行的该列值。如果知道该列(及其值),可以将表大致平均分成行数,并将它们放置在不同的数据库中。在搜索这些行(即SELECT查询)时,只需知道分片键的值,就可以找到存储这些行的数据库。
分片具有一堆表的关系数据库需要了解应用程序的工作方式,选择正确的分片键很重要,之后很难更改,选择不当可能会降低数据库性能。
理想情况下,应由熟悉应用程序的人找到合适的键,他们可以根据直觉找到瓶颈所在以及能均匀分割数据库的表和列。对于其他人(包括作者),架构可以提供帮助。在理论上,理想的表具有最多的外键引用,通过查看数据库架构可以找到合适的分片键。作者发现accounts表被 75 个其他表引用,占数据库中所有表的 36%,这是一个很好的起点。

传递关系

虽然accounts直接被 75 个其他表引用,但这些表本身可能在其他表中具有外键引用,由于这种关系是传递性的,通过连接可以找到这些表中的行。递归搜索后,发现有 123 个表直接或通过其他表引用accounts,占所有表的 60%,这表明选择的分片键不错。

试运行模式

在生产系统中,了解其工作方式的唯一真正方法是在生产环境中观察。为了找出应用程序是否在大多数查询中传递account_id,创建了“试运行”功能,启用后,通过 PgDog 的每个查询都将通过其内部路由器,即使数据库未分片,PgDog 也会假装进行分片并做出路由决策。可以通过在pgdog.toml中添加设置来启用:

[general]
dry_run = true

启用后,所有查询都将被路由和记录,路由决策最终被忽略,但可以在管理数据库中看到查询和 PgDog 做出的路由决策。内部路由器查询缓存非常高效,如果应用程序使用预处理语句,它们将被去重,只有一个带有占位符的语句实例被记录。如果应用程序不使用预处理语句或扩展协议,PgDog 将使用pg_query“规范化”查询,去除参数并替换为占位符,以确保查询缓存只存储唯一的查询并保持条目数量可管理。

生产指标

启用 Prometheus 端点以收集指标,以便了解是否选择了正确的分片键。导出的指标包括query_cache_hits(查询在缓存中找到的次数)、query_cache_misses(从未见过的查询次数)、query_cache_direct(路由到一个分片的查询次数)、query_cache_cross(必须命中所有分片的查询次数)和query_cache_size(缓存中的总查询数)。可以使用百分比总结这些指标,公式为success_rate = direct / (direct + cross) * 100,当该数字达到 95%时,就可以继续。

全局分片

每个应用程序都有“元数据”表,它们存储一些很少更新但经常访问的信息。对于无法分片的表(如terms_of_service),可以将其复制到所有分片,并使用轮询负载均衡算法将查询路由到任何分片,以均匀分布流量。添加omnisharded_tablespgdog.toml中即可实现:

[[omnisharded_tables]]
database = "mastodon_development"
tables = [
    "settings",
    "site_uploads",
    "ip_blocks",
    "terms_of_services",
]

添加后,查询缓存命中率达到 99.99%,52%的数据库查询被分片。

处理写入

在只读上下文中的跨分片查询不是世界末日,但写入查询如果路由错误可能会导致混乱。测试创建帖子时发现第一个查询没有分片键,第二个查询有但 PgDog 太晚才看到,需要在执行第二个查询之前做出路由决策。为此,使用SET命令传递分片键,还编写了一个 Ruby 宝石pgdog来简化操作。

结论

本周就到这里,还有一些未回答的问题,如如何生成主键、需要进行多少代码更改、能否自动化代码更改、如何维护全局分片表的数据完整性以及如何保护分片免受意外的跨分片写入等。

下一步

如果对此感兴趣,可以联系,PgDog 正在寻找早期采用者,预计 6 月发布v1.0版本,欢迎在 GitHub 上点赞。

阅读 14
0 条评论