Rails 8 使用 SolidQueue 替代 Redis 架构分析
一、背景与目标
1. 项目背景
A. 业务场景
Rails 8 从默认技术栈中移除了 Redis。新的功能组件——SolidQueue(任务队列)、SolidCache(缓存)、SolidCable(实时消息传递)——完全运行在应用程序现有的关系数据库服务上。对于大多数 Rails 应用,可以不再依赖 Redis。
B. 痛点分析
Redis 虽然是快速、可靠的基础设施,但它增加了系统复杂性:
- 需要单独部署、版本管理、补丁和监控服务器软件
- 需要配置持久化策略(RDB 快照、AOF 日志或两者)
- 需要设置内存限制和淘汰策略
- 需要维护 Rails 与 Redis 之间的网络连接和防火墙规则
- 需要配置 Redis 客户端认证
- 需要构建和维护高可用 Redis 集群
- 需要协调 Sidekiq 进程的生命周期管理
2. 设计目标
A. 功能目标
- 使用关系数据库替代 Redis 实现任务队列功能
- 支持任务调度、并发控制、重试机制
- 提供监控和管理界面
B. 非功能目标
- 降低运维复杂性
- 减少基础设施组件数量
- 保持足够的性能水平
二、Redis 运维成本分析
1. 直接成本
A. 服务器部署
- Redis 服务器软件安装与配置
- 版本升级与补丁管理
- 服务器资源监控
B. 持久化配置
- RDB 快照:内存快照持久化
- AOF 日志:追加文件日志
- 混合策略:结合两者优势
2. 间接成本
A. 网络与安全
- 防火墙规则配置
- Redis 客户端认证管理
- 高可用集群部署
B. 进程管理
- Sidekiq 进程编排
- 跨部署的生命周期管理
- 进程崩溃恢复
3. 调试复杂性
当任务出现问题时,需要同时调试 Redis 和 RDBMS 两个具有不同语义的数据存储,在不同的查询语言和工具之间切换上下文。还需要维护两套独立的备份策略。
三、SolidQueue 工作原理
1. 核心技术:SKIP LOCKED
PostgreSQL 9.5 增强了 SQL 的 `FOR UPDATE` 子句,添加了 `SKIP LOCKED` 选项。`FOR UPDATE` 创建排他行锁,`SKIP LOCKED` 进一步跳过当前被锁定的行。
这使得基于数据库的任务队列成为可能,即使在规模化场景下也能正常工作。
2. 任务获取流程
工作进程获取任务的查询:
```sql
SELECT * FROM solid_queue_ready_executions
WHERE queue_name = 'default'
ORDER BY priority DESC, job_id ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
```
自由的工作进程始终获取下一个可用任务。
这个数据库优化解决了早期数据库队列实现面临的根本问题:**锁竞争**。工作进程从不等待另一个进程,也从不阻塞。多个工作进程可以同时查询,PostgreSQL 保证每个进程获得唯一的任务。
3. 数据库表结构
SolidQueue 架构围绕三个核心表:
```mermaid
graph TD
subgraph SolidQueue 数据库表结构
A[solid_queue_jobs<br/>任务元数据表]
B[solid_queue_scheduled_executions<br/>定时任务表]
C[solid_queue_ready_executions<br/>就绪任务表]
end
D[新任务] -->|已调度| B
D -->|立即执行| C
B -->|到达执行时间| C
C -->|工作进程获取| E[执行中]
E -->|完成| F[删除执行记录]
E -->|失败| G[失败记录表]
```
<img src="https://static.op123.ren/static/d9/d9ea6cf58fd285b4.svg" alt="SolidQueue 数据库表结构" width="700">
A. solid_queue_jobs
存储所有任务元数据,包括任务名称、Ruby 类、开始和结束时间戳。默认情况下,每个排队请求都会记录在此表中,并在任务完成后永久保留。
B. solid_queue_scheduled_executions
定时任务等待直到其预定时间到达。
C. solid_queue_ready_executions
准备立即运行的任务排队在此,工作进程从中获取任务。
PostgreSQL 的 MVCC 设计通过内置的 autovacuum 进程很好地处理了任务表的快速和稳定 churn(大量插入和删除)。无需特殊调优。
4. 进程架构
```mermaid
graph LR
subgraph SolidQueue 进程组件
A[Supervisor<br/>监督进程]
B[Workers<br/>工作进程]
C[Dispatchers<br/>调度进程]
D[Schedulers<br/>定时任务进程]
end
A --> B
A --> C
A --> D
B -->|轮询 0.1-0.2s| C
D -->|每秒轮询| C
C --> B
```
<img src="https://static.op123.ren/static/c1/c1af6fa9474e338b.svg" alt="SolidQueue 进程架构" width="600">
A. Workers(工作进程)
轮询 solid_queue_ready_executions,间隔可配置(高优先级队列最快可达 0.1 秒)
B. Dispatchers(调度进程)
每秒轮询 solid_queue_scheduled_executions,将到期任务移至就绪表
C. Schedulers(定时任务进程)
按定义的时间表入队任务,管理周期性任务
D. Supervisor(监督进程)
监控所有进程,跟踪心跳并重启崩溃的进程
这些分离的关注点是 SolidQueue 最优雅的特性之一。每种进程类型在不同的表上操作,使用针对其工作负载优化的不同轮询间隔。
四、定时任务功能
1. 配置方式
SolidQueue 内置了 cron 风格的周期性任务支持,无需额外库。编辑 config/recurring.yml:
```yaml
# config/recurring.yml
production:
cleanup_old_sessions:
class: CleanupSessionsJob
schedule: every day at 2am
queue: maintenance
send_daily_digest:
class: DailyDigestJob
schedule: every day at 9am
queue: mailers
refresh_cache:
class: CacheWarmupJob
schedule: every hour
queue: default
```
2. 工作原理
```mermaid
sequenceDiagram
participant S as Scheduler
participant J as Job Queue
participant W as Worker
Note over S: 8:00 AM
S->>J: 入队 refresh_cache 任务
W->>J: 获取并执行任务
S->>S: 调度下次运行(9:00 AM)
Note over S: 9:00 AM
S->>J: 入队 refresh_cache 任务
W->>J: 获取并执行任务
S->>S: 调度下次运行(10:00 AM)
```
<img src="https://static.op123.ren/static/02/02509fa484a93fe2.svg" alt="定时任务调度流程" width="600">
A. 调度器运行时,找到到期的任务并入队执行
B. 同时,调度器还会为下一次出现的时间入队新任务
C. 这种模式具有崩溃恢复能力,因为调度是确定性的
五、并发控制功能
1. 并发限制声明
SolidQueue 免费提供了 Sidekiq Enterprise 付费版才有的并发限制功能:
```ruby
class ProcessUserOnboardingJob < ApplicationJob
limits_concurrency to: 1,
key: ->(user) { user.id },
duration: 15.minutes
def perform(user)
# 复杂的入职工作流
end
end
```
`limits_concurrency to: 1` 确保在任何时候每个用户只运行一个 ProcessUserOnboardingJob 任务。
2. 信号量实现
```mermaid
graph TD
A[新任务请求] --> B{检查信号量}
B -->|未达到限制| C[获取信号量]
B -->|已达到限制| D[加入阻塞队列]
C --> E[执行任务]
E --> F[释放信号量]
F --> G[触发调度器]
G --> D
D --> H[解除阻塞,准备执行]
```
<img src="https://static.op123.ren/static/f1/f106831493a715c1.svg" alt="并发控制流程" width="600">
A. solid_queue_semaphores
跟踪并发限制
B. solid_queue_blocked_executions
保存等待信号量释放的任务
当任务完成时,它释放信号量并触发调度器解除下一个等待任务的阻塞。
六、监控与管理:Mission Control
1. 功能对比
| 功能 | Sidekiq Pro/Enterprise | Mission Control Jobs |
|-----|----------------------|---------------------|
价格 | $949-$1699/年 | 免费 |
实时任务状态 | 支持 | 支持 |
失败任务检查 | 支持 | 支持,带完整堆栈 |
批量操作 | 支持 | 支持 |
定时任务可视化 | 支持 | 支持 |
SQL 查询 | 不支持 | 支持 |
2. 配置方式
```ruby
# config/routes.rb
mount MissionControl::Jobs::Engine, at: "/jobs"
```
3. SQL 集成优势
Mission Control 可以直接查询数据库:
```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;
```
SQL 是你已知的语言,在你已使用的工具中运行。无需外部解析,无需时间戳算术。
七、迁移路径:从 Sidekiq 到 SolidQueue
1. 步骤概览
```mermaid
graph LR
A[Sidekiq + Redis] --> B[更改队列适配器]
B --> C[安装 SolidQueue]
C --> D[转换定时任务配置]
D --> E[更新 Procfile]
E --> F[移除旧依赖]
```
<img src="https://static.op123.ren/static/89/89ddc87ed6c8a030.svg" alt="迁移步骤" width="600">
2. 详细步骤
A. 更改 Rails 队列适配器
```ruby
# config/environments/production.rb
config.active_job.queue_adapter = :solid_queue
```
B. 安装 SolidQueue
```bash
$ bundle add solid_queue
$ rails solid_queue:install
$ rails db:migrate
```
C. 转换定时任务配置
```yaml
# 旧配置:config/sidekiq.yml
:schedule:
cleanup_job:
cron: '0 2 * * *'
class: CleanupJob
# 新配置:config/recurring.yml
production:
cleanup_job:
class: CleanupJob
schedule: every day at 2am
```
D. 更新 Procfile
```yaml
web: bundle exec puma -C config/puma.rb
jobs: bundle exec rake solid_queue:start
```
E. 移除旧依赖
```ruby
# Gemfile - DELETE
# gem "redis"
# gem "sidekiq"
# gem "sidekiq-cron"
```
八、性能与扩展性分析
1. 性能基准
| 指标 | Redis + Sidekiq | SolidQueue |
|-----|----------------|-----------|
任务吞吐量 | ~1000s 任务/秒 | ~200-300 任务/秒 |
任务延迟 | <1ms | ~100ms || 适用范围 | 99.9% 的应用 | 95% 的应用 |
2. 扩展性评估公式
Nate Berkopec 的扩展性公式:
**所需应用实例数 = 请求率(请求/秒)× 平均响应时间(秒)**
示例计算:
- 应用每分钟处理 100 个请求
- 平均响应时间 200ms
- 即约 1.67 请求/秒
- 所需实例:1.67 × 0.2 = 0.083 个实例
实际案例:37signals 每天处理 2000 万个任务,约每秒 230 个任务,全部运行在 PostgreSQL 上,无需 Redis。
3. 使用场景判断
**适合使用 SolidQueue**:
- 任务处理少于 100 任务/秒
- 任务延迟容忍度大于 100ms
- 希望简化基础设施
**需要保留 Redis**:
- 持续处理数千任务/秒
- 任务延迟低于 1ms 至关重要
- 复杂的发布/订阅模式
- 需要密集的速率限制或计数器
九、实际部署指南
1. 创建 Rails 8 应用
```bash
$ rails new myapp --database=postgresql
$ cd myapp
```
Rails 8 自动配置 SolidQueue、SolidCache 和 SolidCable。
2. 配置队列数据库
```yaml
# config/database.yml
development:
primary: &primary_development
<<: *default
database: myapp_development
queue:
<<: *primary_development
database: myapp_queue_development
migrations_paths: db/queue_migrate
```
```ruby
# config/environments/development.rb
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }
```
3. 完整部署架构
```mermaid
graph TB
subgraph 应用层
Web[Web 服务器<br/>Puma]
Jobs[SolidQueue 工作进程]
end
subgraph 数据层
PG[(PostgreSQL)]
DB1[(主数据库)]
DB2[(队列数据库)]
end
subgraph 管理层
MC[Mission Control<br/>/jobs]
end
Web --> DB1
Jobs --> DB2
MC --> DB2
DB1 -.共享存储.-> PG
DB2 -.共享存储.-> PG
```
<img src="https://static.op123.ren/static/49/49ffcf3c04f4caa6.svg" alt="完整部署架构" width="700">
十、技术选型对比
1. Redis + Sidekiq vs SolidQueue
| 方面 | Redis + Sidekiq | SolidQueue |
|-----|----------------|-----------|
| 设置复杂度 | 需要单独服务 + 配置 | 已内置 |
| 查询语言 | Redis 命令 | SQL |
| 监控 | 独立仪表板 | 与应用相同 |
| 故障模式 | 6+ 种不同场景 | 2 种场景 |
| 任务吞吐量 | ~1000s 任务/秒 | ~200-300 任务/秒 |
| 适合应用比例 | 99.9% | 95% |
| 成本 | 需付费企业版功能 | 全部免费 |
2. 运维复杂度对比
```mermaid
graph LR
subgraph Redis + Sidekiq
R1[Redis 服务器]
R2[Sidekiq 进程]
R3[持久化配置]
R4[高可用集群]
R1 --> R2
R1 --> R3
R1 --> R4
end
subgraph SolidQueue
S1[PostgreSQL]
S2[SolidQueue 进程]
S1 --> S2
end
```
<img src="https://static.op123.ren/static/56/56baa8563d17b756.svg" alt="运维复杂度对比" width="700">
十一、常见问题与最佳实践
1. 单数据库配置(备选方案)
SolidQueue 推荐使用单独的数据库连接,但也可以运行在单一数据库中:
1. 将 db/queue_schema.rb 内容复制到常规迁移
2. 删除 db/queue_schema.rb
3. 从环境配置中移除 config.solid_queue.connects_to
4. 运行 rails db:migrate
2. 生产环境认证
不要忘记在生产环境中为 Mission Control 添加认证:
```ruby
# config/initializers/mission_control.rb
config.mission_control.jobs.base_controller_class = "AdminController"
```
3. 轮询间隔
默认轮询间隔:定时任务 1 秒,就绪任务 0.2 秒。从 Sidekiq 迁移时,如果感觉任务"变慢",需要调整期望。对于大多数应用,SolidQueue 的默认值工作良好。
十二、总结
Redis 和 Sidekiq 是经过精心设计的工程产品,十多年来 Rails 应用从中受益匪浅。但对于大多数 Rails 应用,Redis 和 Sidekiq 解决的是你并不存在的问题,代价是你无法承受的复杂性。
SolidQueue 的核心优势:
- 简化基础设施
- 减轻运维负担
- 保持足够性能
- 免费提供企业级功能
给 SolidQueue 一个尝试,让基础设施简化,运维负担减轻,专注于构建产品而不是维护技术栈。