# async-background:一个无需外部依赖的轻量级 Ruby 后台任务系统
Ruby 应用开发者常常需要处理后台任务,但像 Sidekiq 这样的现成解决方案都需要额外的基础设施。新 gem async-background 提供了一个替代方案,它基于 Async 运行,无需 Redis 或 Postgres,并支持细粒度资源控制。
现有解决方案的不足
Sidekiq 是 Ruby 中后台任务的常见选择,但它依赖 Redis。对于只有少量任务的小型项目,这太过头了:需要在 docker-compose 中多加一个容器,引入另一个故障点,还增加基础设施成本。而且,Sidekiq 不支持在 worker 之间灵活分配负载——一个耗时的夜间报告可能会拖慢用户邮件处理。
其他方案也有各自缺点:
whenever—— 生成系统 crontab,没有队列或应用集成。rufus-scheduler—— 在独立线程中运行,在多进程的 Async 应用中会导致上下文切换和重复执行问题。good_job和solid_queue—— 绑定到 ActiveRecord 和 Postgres。这会带来风险:后台任务与事务负载竞争数据库资源,可能拖慢主应用。
因此,需要一个解决方案,它结合 cron 调度、间隔执行和动态队列,无需外部依赖,同时能控制资源使用。
核心设计原则
该 gem 的作者列出了六个核心原则:
- 单一事件循环。 调度器与应用运行在同一个反应式循环中,无需独立线程。
- 零基础设施。 队列存储在磁盘上(SQLite 的 WAL 模式或
tmp/中的文件),无需 Redis 或 Postgres。 - 可选依赖。 额外功能(如指标)通过可选 gem 添加。
- 无需协调器的多进程安全。 使用 Falcon(fork)时,确保任务只在一个进程中运行。
- 持久化作为保障,速度作为独立机制。 可靠性来自 SQLite 存储,速度来自 UNIX socket 唤醒。
- 托管 worker 利用率。 开发者可以通过环境变量和 YAML 配置将任务分配给特定 worker。
这些原则使得能够构建一个无缝集成到现有 Async 应用中的 gem,而无需额外基础设施。
架构特性
使用 MinHeap 而非轮询 Tick
与使用 sleep 1 的循环(每秒唤醒检查所有任务)不同,这里使用二叉最小堆(MinHeap)。这实现了:
- 用
peek在 O(1) 时间内获取下一个任务。 - 运行任务后用
replace_top在 O(log n) 时间内更新堆。 - 精确休眠到下一个事件,节省 CPU 资源。
实现代码仅 74 行,无外部依赖。
双时间尺度
对于基于间隔的任务(例如 every: 60),使用单调时钟(CLOCK_MONOTONIC)来避免时间同步(NTP)问题。对于 cron 任务(例如 cron: "0 3 *"),使用壁钟时间(Time.now),因为调度与日历时间绑定。
代码实现如下:
next_run_at = if task_config[:interval]
now + jitter + task_config[:interval]
else
now_wall = Time.now
wall_wait = task_config[:cron].next_time(now_wall).to_f - now_wall.to_f
now + jitter + [wall_wait, MIN_SLEEP_TIME].max
end
通过 CRC32 分片
使用 Falcon(fork)时,任务必须只在一个进程中运行。确定性分片实现了这一点:为任务名称计算 Zlib.crc32(name) % total_workers + 1。如果匹配当前 worker 的索引,则加载该任务。
assigned = config['worker']&.to_i || ((Zlib.crc32(name) % total_workers) + 1)
next unless assigned == worker_index
这消除了分布式锁的需求。
重叠跳过
如果任务运行时间超过其间隔(例如,60 秒间隔的任务运行了 90 秒),新实例将被跳过。相反,当前任务会被记录、跳过,并重新调度到下一个时隙:
if entry.running
logger.warn('Async::Background') { "#{entry.name}: skipped, previous run still active" }
metrics.job_skipped(entry)
entry.reschedule(monotonic_now)
heap.replace_top(entry)
next
end
队列:SQLite + UNIX Socket
动态队列使用两种机制:
- SQLite —— 真相来源。任务持久化到磁盘以确保可靠性。
- UNIX socket —— 用于即时通知。将任务添加到数据库后,通过 socket 发送信号,使 worker 立即处理。
每 5 秒轮询作为信号丢失时的备份。
故障恢复
如果 worker 在任务中途崩溃,状态仍为 "running"。重启时,Queue::Store#recover 找到此类记录并将其返回队列。
关键要点
- 无外部服务依赖。 async-background 仅依赖文件系统,简化部署。
- 资源控制。 为不同任务类型(cron、队列)隔离 worker,防止资源争用。
- 可靠性。 得益于 SQLite 持久化和恢复机制,任务不会丢失。
- 灵活性。 YAML 和环境变量配置使任务分配易于调整。
- 性能。 最小堆和 UNIX socket 提供低延迟和高 CPU 效率。
— Editorial Team
暂无评论。