Solid Queue 1.6.0 Fiber Worker支持:Rails后台任务并发新选择

Solid Queue 1.6.0 带来的关键更新
Solid Queue 作为 Rails 生态中原生的数据库后台任务队列方案,自诞生以来就以「无需额外依赖 Redis 即可运行任务队列」的特性受到关注。与传统的 Sidekiq 基于 Redis 的 List 和 Sorted Set 数据结构实现任务存储与调度不同,Solid Queue 将任务持久化在关系型数据库中(PostgreSQL、MySQL、SQLite),通过 SELECT FOR UPDATE SKIP LOCKED 等数据库特性实现高效的任务分发。
SELECT FOR UPDATE SKIP LOCKED 是现代关系型数据库(PostgreSQL 9.5+、MySQL 8.0+)提供的一种行级锁定查询语法。在传统的 SELECT FOR UPDATE 中,如果目标行已被其他事务锁定,当前事务会阻塞等待锁释放。而 SKIP LOCKED 变体会跳过已被锁定的行,直接返回未锁定的行。这一特性天然适合实现工作队列模式:多个 Worker 可以同时从任务表中拉取任务,每个 Worker 只会获取到未被其他 Worker 锁定的任务行,从而实现无冲突的并发任务分发。相比 Redis 的 BRPOP 原子弹出操作,这种方式虽然有额外的 SQL 解析和事务开销,但提供了 ACID 级别的可靠性保证——即使 Worker 进程崩溃,数据库事务回滚后任务会自动回到可用状态,不会出现任务丢失。
在实际的工作队列实现中,SELECT FOR UPDATE SKIP LOCKED 通常与事务结合使用。Worker 开启事务后执行查询获取一批待处理任务行,将这些行的状态标记为"处理中",然后提交事务释放锁。任务执行完毕后再通过独立事务更新状态为"已完成"或"失败"。这种模式被称为"两阶段提交"工作队列模式。相比之下,如果在整个任务执行期间都持有行锁,长时间运行的任务会导致数据库连接和锁资源被长期占用,影响整体吞吐量。Solid Queue 的实现采用了更精巧的设计——它使用独立的 ready_executions 表作为调度队列,Worker 通过 SKIP LOCKED 从该表中领取任务后立即删除对应行并将其移入 claimed_executions 表,从而最小化锁持有时间。
Solid Queue 的内部表结构设计体现了对数据库队列模式的深入理解。除了 ready_executions 和 claimed_executions 表,完整的表结构还包括 solid_queue_jobs(存储任务的完整定义和参数)、solid_queue_scheduled_executions(延迟执行的任务)、solid_queue_blocked_executions(因并发限制被阻塞的任务)、solid_queue_recurring_executions(周期性任务的执行记录)等。这种多表分离设计避免了单一大表的锁争用问题,每个表专注于任务生命周期的特定阶段。Dispatcher 进程负责将到期的 scheduled_executions 移入 ready_executions,Worker 进程则从 ready_executions 领取任务。这种职责分离的架构使得每个组件都可以独立扩展。
虽然单次操作延迟略高于 Redis 的微秒级(通常在毫秒级),但胜在持久化天然可靠、事务一致性强,且无需维护额外的 Redis 实例。Sidekiq 基于 Redis 构建,利用 Redis 的 BRPOP 命令实现阻塞式任务弹出,延迟通常在微秒到亚毫秒级别。其线程池模型默认使用 10 个线程,每个线程独立从 Redis 队列中拉取任务。Redis 的单线程事件循环模型保证了队列操作的原子性,无需额外的锁机制。Sidekiq Pro/Enterprise 还提供了可靠性保证(reliable fetch)、批量任务、速率限制等高级特性。相比之下,Solid Queue 的优势在于:无需维护 Redis 实例、任务数据天然持久化且支持事务一致性、可以利用数据库的全文搜索和复杂查询能力对任务进行管理。其劣势主要在于单次操作延迟较高(毫秒级 vs 微秒级)以及在超高吞吐量场景下数据库可能成为瓶颈。
在最新发布的 1.6.0 版本中,团队引入了一项备受期待的能力——对 Fiber Worker 的支持。这一变化看似只是版本迭代中的一个功能点,实际上却触及了后台任务执行模型的底层设计,对高 I/O 密集型应用不能忽视。
对于长期使用 Sidekiq、Resque 等传统队列方案的开发者而言,Solid Queue 的定位是「简单、可靠、与 Rails 深度集成」。而 Fiber 支持的加入,则让它在并发性能维度上迈出了关键一步。

为什么 Fiber Worker 值得关注
从线程到 Fiber 的并发演进
在 Ruby 的并发模型中,长期存在 GVL(Global VM Lock,全局虚拟机锁)的限制,使得多线程在 CPU 密集型任务上无法真正并行。GVL 是 CRuby(MRI)解释器中的一项核心机制,它确保在任意时刻只有一个线程能够执行 Ruby 字节码。这一设计最初是为了简化内存管理和避免数据竞争,但代价是多线程程序无法在多核 CPU 上实现真正的并行计算。值得注意的是,GVL 在 I/O 操作期间会被释放——当线程进入系统调用等待网络响应或磁盘读写时,其他线程可以获得执行机会。
GVL 的存在有其深刻的历史原因。CRuby 的内存管理使用标记-清除垃圾回收器,对象的引用计数和内存分配操作如果没有全局锁保护,多线程环境下会产生复杂的竞态条件。Python 的 CPython 解释器面临类似问题(其 GIL 即 Global Interpreter Lock 与 Ruby 的 GVL 本质相同)。JRuby 和 TruffleRuby 等替代实现由于基于 JVM/GraalVM,天然支持真正的多线程并行,没有 GVL 限制。Ruby 核心团队目前正在推进 GVL 的细粒度化工作,Ruby 3.3 中已引入了实验性的 M:N 线程调度器,将 Ruby 线程映射到更少的原生线程上,这是迈向更高效并发的中间步骤。
Ruby 社区为突破这一限制做了多方面探索。Ruby 3.0 引入的 Ractor 是试图从根本上解决并行计算问题的新抽象——每个 Ractor 拥有独立的 GVL,Ractor 之间通过消息传递通信,从而实现真正的多核并行。然而 Ractor 目前仍处于实验阶段,其严格的对象隔离模型(禁止共享可变对象)与现有 Ruby 生态的大量 gem 存在兼容性挑战。因此在当前阶段,对于 I/O 密集型并发,Fiber 仍是最实用的选择;对于 CPU 密集型并行,多进程模型(如 fork)仍是生产环境的主流方案。
Ruby 中的 Fiber 最早在 Ruby 1.9(2007年)引入,最初仅作为半协程(semi-coroutine)使用,通过 Fiber.yield 和 Fiber#resume 手动控制执行流。这种手动调度模型虽然灵活但使用门槛较高。直到 Ruby 3.0(2020年)引入 Fiber Scheduler 接口,Fiber 才真正具备了自动调度的能力——阻塞 I/O 操作可以自动触发 Fiber 切换,开发者无需手动管理让出时机。Ruby 3.1 进一步改进了 Fiber 的内存分配策略,引入了惰性栈分配(lazy stack allocation),只有在 Fiber 实际执行时才分配栈内存。Ruby 3.2 和 3.3 持续优化了 Fiber 的上下文切换性能和调度器接口的完整性,使其在生产环境中的可靠性达到了可用水平。
因此对于I/O 密集型任务——例如调用外部 API、等待数据库响应、发送邮件、处理网络请求等——线程和 Fiber 都能在等待 I/O 的过程中让出执行权,从而提升整体吞吐量。
Fiber 相比线程的核心优势在于其极轻量。在 CRuby 中,每个线程默认分配约 1MB 的栈空间(可通过 RUBY_THREAD_VM_STACK_SIZE 调整),加上操作系统层面的线程控制块开销,创建 1000 个线程可能消耗 1GB 以上内存。而 Fiber 的初始栈大小仅约 4KB-8KB(Ruby 3.x 中会按需增长),创建 10000 个 Fiber 可能仅消耗数十 MB 内存。此外,线程切换涉及操作系统的上下文切换(内核态/用户态转换),而 Fiber 切换完全在用户态完成,开销低一到两个数量级。
Fiber 的轻量级特性源于其栈管理策略的精巧设计。操作系统线程的栈空间通常通过 mmap 分配虚拟内存,即使物理内存是按需分配的(通过页错误触发),但虚拟地址空间的占用和内核中线程控制块(TCB)的维护开销仍然显著。Fiber 的栈则完全在用户态管理,Ruby 3.1+ 的惰性栈分配意味着创建 Fiber 对象时几乎零内存开销,只有在首次 resume 时才分配初始栈帧。栈空间的增长采用分段策略而非连续内存,避免了预分配大块内存的浪费。此外,Fiber 没有对应的内核态数据结构,操作系统完全不感知其存在,因此不受操作系统线程数限制(Linux 默认的 /proc/sys/kernel/threads-max 通常限制在数万级别)。
这意味着在同样的内存预算下,Fiber Worker 理论上可以承载远比线程更多的并发任务单元。对于那些以等待外部服务响应为主的后台作业——例如同时等待数千个 HTTP 响应返回——这种模型能显著提升资源利用率。
Async 生态的成熟推动
Fiber 在 Ruby 中的实用化,很大程度上得益于近年来 async gem 及 Fiber Scheduler 接口的成熟。Ruby 3.0 引入的 Fiber Scheduler 是一个标准化的钩子接口,允许开发者或框架注册自定义的调度器来拦截阻塞 I/O 操作。当 Ruby 运行时检测到诸如 IO.read、Socket.connect、sleep 等阻塞调用时,会自动委托给注册的调度器,由调度器决定是否挂起当前 Fiber 并调度其他 Fiber 执行。
从技术实现层面看,Fiber Scheduler 接口定义了一组方法,包括 io_wait、io_read、io_write、kernel_sleep、block/unblock 等。当 Ruby VM 遇到可能阻塞的操作时,会检查当前线程是否注册了 Fiber Scheduler,如果有则调用对应的钩子方法而非直接执行阻塞系统调用。调度器实现者可以在这些钩子中将当前 Fiber 挂起,将底层文件描述符注册到事件循环(如 epoll/kqueue/io_uring),然后调度其他就绪的 Fiber 执行。当 I/O 就绪事件到达时,调度器再恢复对应的 Fiber。
io_uring 是 Linux 5.1(2019年)引入的革命性异步 I/O 接口,由 Jens Axboe 设计。与 epoll 需要为每次 I/O 操作执行系统调用不同,io_uring 通过用户态和内核态之间共享的环形缓冲区(submission queue 和 completion queue)实现批量 I/O 提交和完成通知,大幅减少了系统调用开销。在高并发场景下,io_uring 相比 epoll 可以减少 50% 以上的 CPU 开销。此外,io_uring 支持的操作范围远超传统的网络 I/O——包括文件读写、定时器、进程信号等都可以统一通过 io_uring 处理。随着 io-event gem 对 io_uring 后端的逐步完善,基于 Fiber 的并发模型将能够以更低的系统开销处理更大规模的并发连接,这意味着 Fiber 模型的性能上限还在持续提升。
io-event 是 async 生态的底层事件循环抽象层,它封装了不同操作系统提供的 I/O 多路复用机制。在 Linux 上优先使用 io_uring(如果内核版本支持),降级到 epoll;在 macOS/BSD 上使用 kqueue;在 Windows 上使用 IOCP(I/O Completion Ports)。这种多后端设计使得上层代码无需关心平台差异。epoll 使用 epoll_create/epoll_ctl/epoll_wait 三个系统调用管理文件描述符集合,采用事件驱动模型,只通知就绪的文件描述符,时间复杂度 O(1)。kqueue 是 BSD 系列操作系统的等价物,功能上甚至比 epoll 更丰富,支持监控文件系统变更、进程事件等。这些机制相比传统的 select/poll 系统调用(O(n) 复杂度)在大量并发连接时性能提升显著。
这一机制的关键在于对既有代码的透明性——现有的同步风格代码无需重写即可获得异步执行的效果。Samuel Williams 开发的 async gem 正是基于此接口构建,它提供了完整的事件循环和 Fiber 池管理,底层使用 io-event gem 对接 epoll(Linux)或 kqueue(macOS)等高效 I/O 多路复用机制。
Samuel Williams 主导的 async 生态系统不仅仅是一个 gem,而是一整套协作式并发框架。其核心组件包括:async(提供 Fiber 池和任务抽象)、io-event(底层事件循环,支持 select/poll/epoll/kqueue/io_uring 多种后端)、async-http(基于 Fiber 的 HTTP 客户端和服务器)、async-postgres/async-mysql(数据库驱动的异步适配)等。整个生态系统遵循一个设计原则:通过 Fiber Scheduler 接口实现透明的异步化,使现有同步代码无需修改即可受益。Falcon 是基于 async 构建的 Web 服务器,已在多个生产环境中证明了 Fiber 并发模型的可行性,其单进程即可处理数千并发连接。
Solid Queue 顺应这一趋势,在 1.6.0 中把 Fiber 纳入 Worker 的执行选项,正是 Ruby 并发基础设施逐步完善的自然结果。
Fiber Worker 的适用场景与权衡
何时应该选择 Fiber Worker
Fiber Worker 并非万能药,它有明确的适用边界:
- 高 I/O 等待型任务:如批量调用第三方 API、Webhook 分发、邮件通知、爬取远程数据等,这类任务大部分时间都在等待网络返回,Fiber 能高效复用等待时间。
- 高并发但单任务负载轻:当你需要同时处理成千上万个轻量任务时,Fiber 的低开销特性可以避免线程数量膨胀带来的内存压力。
何时应保持传统 Worker
相对地,以下情况仍建议使用进程或线程模型:
- CPU 密集型任务:如图像处理、复杂计算,Fiber 无法绕过 GVL 带来的并行限制,甚至可能因协作式调度而表现更差。由于 Fiber 是协作式调度(cooperative scheduling),一个 CPU 密集型 Fiber 在主动让出执行权之前会独占整个线程,导致其他 Fiber 饿死。相比之下,操作系统线程的抢占式调度(preemptive scheduling)至少能保证时间片公平分配。
深入理解两种调度模式的差异对于做出正确的架构决策至关重要。协作式调度要求每个执行单元主动让出控制权,其优势是切换时机可预测、无需加锁保护共享状态(因为切换只发生在明确的让出点),缺点是单个执行单元可能因忘记让出或陷入长计算而饿死其他单元。抢占式调度由操作系统通过定时器中断强制切换,保证公平性但引入了竞态条件的风险。在实践中,Fiber 的协作式模型在 I/O 密集场景下表现优异,因为每次 I/O 调用都是天然的让出点;但如果任务中混入了超过几十毫秒的纯计算段,就需要开发者手动插入 Fiber.yield 或将计算拆分为小块,否则会影响整个调度器的响应性。
- 依赖阻塞式 C 扩展的库:部分底层库(如某些数据库驱动的旧版本、图像处理库等)未适配 Fiber Scheduler 接口,其阻塞调用不会触发 Fiber 挂起,可能导致协作调度失效,反而阻塞整个 Fiber 调度器,使所有排队的 Fiber 都无法推进。
这也提示开发者,在升级到 1.6.0 后引入 Fiber Worker 时,需要对现有作业的性质进行分类评估,而非一刀切地全面切换。一个推荐的做法是将队列按任务类型拆分——I/O 密集型队列使用 Fiber Worker,CPU 密集型队列继续使用传统的线程或进程 Worker。
对 Rails 生态的意义
Solid Queue 是 Rails 8 时代「Solid Trifecta」(Solid Cache、Solid Queue、Solid Cable)战略的一部分,其核心理念是减少对外部基础设施的依赖,让开发者仅凭数据库即可运行完整的生产级应用。这一战略的完整图景包括:Solid Cache 用数据库替代 Redis/Memcached 作为缓存后端,利用现代 SSD 的高 IOPS 特性提供足够的缓存性能;Solid Queue 用数据库替代 Redis 作为任务队列后端;Solid Cable 则用数据库替代 Redis 作为 Action Cable 的 pubsub 后端。三者共同构成了一个仅依赖关系型数据库即可运行完整生产应用的技术栈,大幅降低了小团队的运维复杂度。37signals(Basecamp/HEY 的母公司)已在生产环境中大规模验证了这套方案的可行性。
Solid Trifecta 策略之所以在当下可行,与存储硬件的革命性进步密不可分。十年前 HDD 时代,随机 IOPS 仅有数百级别,用数据库做缓存或队列是不现实的。现代 NVMe SSD(如 Samsung PM9A3、Intel Optane)的随机读取 IOPS 达到 50万-100万,随机写入达到 10万-20万,延迟在 10-100 微秒级别。这使得数据库驱动的队列和缓存操作的延迟从 HDD 时代的 10+ms 降至 1ms 以下。特别是 Intel Optane(基于 3D XPoint 技术)提供了接近 DRAM 的延迟特性(约 10 微秒),虽然 Intel 已停产 Optane 产品线,但 CXL(Compute Express Link)技术正在推动新一代持久化内存方案的发展,进一步模糊了内存与存储的边界。
从性能基准角度看,37signals 公布的数据显示,HEY 邮件服务在生产环境中使用 Solid Queue 处理数百万级别的日任务量,峰值吞吐达到每秒数千任务。Solid Cache 则利用 NVMe SSD 的高 IOPS(通常 50万-100万 IOPS)特性,在缓存命中率和响应延迟方面接近 Redis 的表现(P99 延迟通常在个位数毫秒)。然而这套方案的最佳适用范围是中等规模应用——对于每秒需要处理数万任务、对亚毫秒延迟有严格要求的超大规模系统(如实时交易撮合),Redis 或专用消息队列(如 Kafka、RabbitMQ)仍是更合适的选择。Solid Trifecta 的价值在于覆盖了绝大多数 Web 应用的实际需求,同时将运维复杂度降到最低。
此次 Fiber 支持的加入,进一步缩小了 Solid Queue 与 Sidekiq 等成熟方案在并发性能上的差距。对于任务量在每秒数百到数千级别的应用,两者的性能差异在实际体验中几乎不可感知,而 Fiber Worker 的引入让 Solid Queue 在高并发 I/O 场景下的吞吐量获得了质的提升。
对于中小型团队和希望简化运维栈的项目而言,这意味着可以在不引入 Redis 的前提下,获得接近专业队列方案的并发处理能力。这种「够用且省心」的定位,正是 Rails 生态一贯推崇的开发哲学的延续——Convention over Configuration(约定优于配置)精神在基础设施层面的又一次实践。
补充一点,社区对其实际性能表现的深入验证还有待时间检验。但从技术方向上看,Fiber Worker 的引入无疑代表了 Ruby 后台任务处理演进的一个正确趋势。随着 Ruby 3.x 系列对 Fiber Scheduler 的持续优化,以及越来越多的 gem 适配非阻塞 I/O 接口,Fiber 模型的适用范围只会越来越广。
结语
Solid Queue 1.6.0 的 Fiber Worker 支持,是 Ruby 并发生态成熟与 Rails「去外部依赖」战略结合的产物。它为 I/O 密集型场景提供了一种更轻量、更高效的执行选择,但也要求开发者理解其适用边界——理解协作式调度与抢占式调度的本质差异,评估依赖库的 Fiber 兼容性,并据此做出合理的队列架构决策。对于正在评估后台任务方案的团队,这一更新值得纳入技术选型的考量之中。
核心要点
- Solid Queue 1.6.0 新增 Fiber Worker 支持,为 I/O 密集型后台任务提供轻量级高并发执行模型
- Fiber 的核心优势:内存占用仅为线程的百分之一级别(4-8KB vs 1MB),用户态切换开销极低,适合高并发 I/O 等待场景
- 明确的适用边界:I/O 密集型任务受益显著,CPU 密集型任务和依赖未适配 Fiber Scheduler 的 C 扩展库时应继续使用线程/进程模型
- 推荐的实践策略:按任务类型拆分队列,I/O 密集型队列使用 Fiber Worker,计算密集型队列使用传统 Worker
- Rails 生态战略意义:作为 Solid Trifecta 的一部分,进一步缩小与 Redis 方案的性能差距,降低中小团队运维复杂度
- 底层依赖:基于 Ruby 3.0+ 的 Fiber Scheduler 接口和 async 生态,受益于 epoll/kqueue/io_uring 等高效 I/O 多路复用机制
- 技术演进脉络:从 Ruby 1.9 的手动 Fiber 到 Ruby 3.0+ 的自动调度 Fiber Scheduler,经历了十余年的成熟过程,async 生态系统的完善是 Fiber Worker 得以实用化的关键基础设施
相关推荐

Cursor是什么?AI编程工具与传统IDE的核心区别
Cursor是什么?本文详解这款内置AI助手的编程工具,对比它与VS Code等传统IDE在代码补全、生成、重构、错误处理上的核心区别,并分析Cursor集成Claude、DeepSeek等大模型的特性及适用人群。

Coze扣子3.0入门指南:智能体与AI应用全景解析
Coze扣子3.0入门教程:解析字节跳动AI开发平台的智能体、AI应用、工作流与插件体系,涵盖单Agent与多Agent协作,并对比Coze与Dify的差异,帮助零基础用户快速搭建AI智能体。

DeepSeek Harness 环境搭建:Node.js 安装与配置全流程
零基础搭建 DeepSeek Harness 运行环境的完整教程,涵盖 Node.js 安装、Add to PATH 勾选、npm 全局目录与缓存目录迁移,以及系统环境变量配置全流程,附常见踩坑提示。