Hotdry.
systems

Redis到SolidQueue迁移的工程权衡:队列持久化、内存管理与性能基准

深入分析从Redis迁移到SolidQueue的技术决策,聚焦队列持久化策略、内存管理差异、故障恢复机制与性能基准测试实现。

在 Rails 8 的发布中,一个显著的变化是 Redis 从标准技术栈中被移除。取而代之的是 SolidQueue、SolidCache 和 SolidCable 这一套基于关系数据库的解决方案。这一转变引发了开发者社区的广泛讨论:我们真的需要 Redis 吗?还是说,像 PostgreSQL 这样的 "无聊技术" 已经足够胜任队列处理的任务?

Redis 的真实成本:超越月度账单

Redis 作为内存键值存储,在过去十年中一直是 Rails 应用队列处理的首选。它的速度快、功能强大且稳定可靠。然而,Matt Kelly 在 SimpleThread 的文章中指出,Redis 的成本远不止月度托管费用。

运维复杂度是 Redis 的主要隐性成本。要使用 Redis,你必须:

  • 部署、版本管理、补丁更新和监控 Redis 服务器软件
  • 配置持久化策略:选择 RDB 快照、AOF 日志,还是两者兼用?
  • 设置和监控内存限制,建立驱逐策略

此外,还有基础设施和互操作性的持续负担:

  • 维护 Rails 与 Redis 之间的网络连接,包括防火墙规则
  • Redis 客户端认证
  • 构建和维护高可用 Redis 集群
  • 跨部署编排 Sidekiq 进程的生命周期

当作业出现问题时,你需要在 Redis 和 RDBMS 这两个语义完全不同的数据存储之间切换上下文,使用不同的查询语言和工具进行调试。更不用说需要维护两套独立的备份策略。

SolidQueue 的架构革新:基于 PostgreSQL SKIP LOCKED

Redis 与 PostgreSQL 是两种截然不同的数据存储。Redis 在很多方面被当作内存使用:原子性、易失性和极快的速度。那么 SolidQueue 是如何用 PostgreSQL 替代 Redis 的呢?

关键在于 PostgreSQL 9.5 引入的FOR UPDATE SKIP LOCKED子句。FOR UPDATE创建排他行锁,而SKIP LOCKED进一步跳过当前已被锁定的行。这一机制使得基于数据库的作业队列即使在规模较大时也能保持可行性。

当工作进程需要作业时,执行以下查询:

SELECT * FROM solid_queue_ready_executions
WHERE queue_name = 'default'
ORDER BY priority DESC, job_id ASC
LIMIT 1
FOR UPDATE SKIP LOCKED

空闲的工作进程总是能获取到下一个可用作业。这一数据库优化解决了早期数据库队列实现中的根本问题:锁争用。工作进程永远不会等待另一个进程,也永远不会被阻塞。多个工作进程可以同时查询,PostgreSQL 保证每个进程都能获取到唯一的作业。

SolidQueue 的架构围绕三个核心表构建:

  1. solid_queue_jobs:存储所有作业的元数据,如作业名称、Ruby 类以及记录作业开始和完成时间的时间戳
  2. solid_queue_scheduled_executions:等待预定时间的调度作业
  3. solid_queue_ready_executions:准备立即运行的作业队列

作业表可能会快速且稳定地更新(有大量的插入和删除操作),但 PostgreSQL 的 MVCC 设计通过其内置的自动清理进程可以很好地处理这种情况,无需特殊调优。

性能对比:何时选择 Redis,何时选择 SolidQueue

性能是迁移决策中的关键考量因素。根据实际测试和社区经验,我们可以得出以下指导原则:

选择 SolidQueue 的场景:

  • 处理速度低于 100 作业 / 秒
  • 作业延迟容忍度大于 100 毫秒
  • 希望简化基础设施栈
  • 预算有限,无法承担 Sidekiq 企业版费用

仍需要 Redis 的场景:

  • 持续处理数千作业 / 秒(不是峰值,而是持续负载)
  • 作业延迟低于 1 毫秒对业务至关重要(如实时竞价、高频交易)
  • 需要复杂的 pub/sub 模式跨多个服务
  • 需要密集的速率限制或计数器,受益于 Redis 的原子操作

作为参考,37signals 每天处理 2000 万个作业,大约每秒 230 个作业,全部在 PostgreSQL 上运行,无需 Redis。这个规模已经覆盖了绝大多数 Rails 应用的需求。

并发限制:从付费功能到免费特性

如果你在普通规模下使用 Rails,可能不知道 Sidekiq 也将并发限制作为付费功能提供在 Sidekiq 企业版中。如果你考虑使用 Sidekiq,仅并发限制这一功能就值得购买企业版。

但 SolidQueue 免费提供了这一功能,而且更多!只需在任何作业中添加limits_concurrency

class ProcessUserOnboardingJob < ApplicationJob
  limits_concurrency to: 1, 
    key: ->(user) { user.id }, 
    duration: 15.minutes
  
  def perform(user)
    # 复杂的入职工作流程
  end
end

limits_concurrency to: 1确保每个用户在任何时候只有一个ProcessUserOnboardingJob作业运行。

duration参数也很重要,它定义了 SolidQueue 保证并发限制的时间长度。如果作业崩溃,信号量最终会过期,防止因崩溃的工作进程从未释放锁而导致的死锁。

实现使用两个表:solid_queue_semaphores跟踪并发限制,solid_queue_blocked_executions保存等待信号量释放的作业。当作业完成时,它释放信号量并触发调度器解除下一个等待作业的阻塞。这种设计优雅、数据库原生,且无需外部协调。

迁移实施指南:从 Sidekiq 到 SolidQueue

迁移过程相对简单,但需要遵循正确的步骤:

步骤 1:更改 Rails 队列适配器

# config/environments/production.rb
config.active_job.queue_adapter = :solid_queue

步骤 2:安装 SolidQueue

$ bundle add solid_queue
$ rails solid_queue:install
$ rails db:migrate

步骤 3:替换 sidekiq-cron 调度

config/sidekiq.yml中的 cron 调度转换为config/recurring.yml。配置结构类似,但需要更新键名并将经典 cron 字符串转换为 Fugit 的首选自然语言格式。

步骤 4:更新 Procfile

web: bundle exec puma -C config/puma.rb
jobs: bundle exec rake solid_queue:start

步骤 5:清理旧栈

Redis 和 Sidekiq 现在已过时。可以从 Gemfile 中删除相应的 gem,运行 Bundler 从 Gemfile.lock 中移除依赖。

监控与管理:Mission Control Jobs

Sidekiq 的免费版 Web 用户界面尚可,Sidekiq Pro(949 美元 / 年)和 Sidekiq 企业版(起价 1699 美元 / 年)提供增强的仪表板。

Mission Control Jobs是免费、开源的,专门为 Rails 8 的 SolidQueue 生态系统设计:

# config/routes.rb
mount MissionControl::Jobs::Engine, at: "/jobs"

通过这一行代码,你现在拥有:

  • 所有队列的 "实时" 作业状态
  • 失败作业检查,包含完整堆栈跟踪
  • 重试和丢弃控制,支持批量操作
  • 调度作业时间线可视化
  • 定期作业管理
  • 队列特定指标和吞吐量图表

更好的是,Mission Control 可以检查你的数据库模式。当你检查失败作业时,可以看到其作业参数(就像 Sidekiq 一样),但你也可以使用大家都喜欢的查询语言 SQL 查询作业数据:

SELECT j.queue_name, COUNT(*) as failed_count
FROM solid_queue_failed_executions fe
JOIN solid_queue_jobs j ON j.id = fe.job_id
WHERE fe.created_at > NOW() - INTERVAL '1 hour'
GROUP BY j.queue_name;

常见陷阱与解决方案

单数据库设置(替代方案)

SolidQueue 建议使用单独的数据库连接,但如果你愿意,可以在一个数据库中运行所有内容。

  1. db/queue_schema.rb的内容复制到常规迁移中
  2. 删除db/queue_schema.rb
  3. 从环境配置中移除config.solid_queue.connects_to
  4. 运行rails db:migrate

这对于较小的应用程序来说效果很好,但代价是操作灵活性。Rails 团队推荐使用单独的连接方法。

轮询间隔

调度作业的默认轮询间隔为 1 秒,就绪作业为 0.2 秒。如果你从 Sidekiq 迁移过来,感觉作业 "变慢" 了,请检查你的期望。根据经验,SolidQueue 的默认值对大多数应用程序都适用。对于后台作业来说,亚秒级延迟通常并不重要。

ActionCable 和 Turbo Streams

如果你使用 ActionCable(或任何依赖它的东西,如 Turbo Streams),你还需要使用自己的数据库连接配置 SolidCable。

可扩展性分析

你可能会问那个永恒的问题:"它能扩展吗?"

答案是肯定的,它能扩展。但更好的问题是:"它是否足够满足我的扩展需求?"

根据 Nate Berkopec 2015 年的文章《将 Ruby 应用扩展到 1000 RPM》,可以使用以下公式:

所需应用实例数 = 请求速率(请求 / 秒)× 平均响应时间(秒)

让我们为一个典型应用进行计算。假设你的应用每分钟收到 100 个请求,平均响应时间为 200 毫秒。这大约是每秒 1.67 个请求。乘以 0.2 秒,得到 0.083 个应用实例所需。你需要一个应用实例的 8% 来处理负载。

工程决策框架

在决定是否从 Redis 迁移到 SolidQueue 时,建议使用以下决策框架:

  1. 吞吐量评估:测量当前和预期的作业处理速率
  2. 延迟要求分析:确定业务对作业延迟的容忍度
  3. 运维复杂度对比:评估当前 Redis 运维负担与简化栈的收益
  4. 成本效益分析:计算 Redis 托管成本与开发维护时间
  5. 风险缓解计划:制定回滚策略和监控方案

结论

Redis 和 Sidekiq 是经过精心设计的优秀工具,Rails 应用在过去十多年中从这一组合中获益匪浅。但对于大多数 Rails 应用来说,Redis 和 Sidekiq 解决了一个你并不存在的问题,代价却是你无法承受的。

SolidQueue 提供了一个简化基础设施、减轻运维负担的机会,让你能够专注于构建产品,而不是维护技术栈。正如 Matt Kelly 所指出的,除非你每天处理数百万个作业,否则 PostgreSQL 很可能已经足够满足你的需求。

迁移决策不应基于技术潮流,而应基于实际需求、运维复杂度和成本效益的理性分析。对于 95% 的 Rails 应用来说,SolidQueue 不仅足够好,而且可能是更好的选择。

资料来源

  • SimpleThread 文章《I Love You, Redis, But I'm Leaving You for SolidQueue》
  • Hacker News 关于 SolidQueue 性能的讨论
查看归档