返回首页

Async-background:Ruby 中无需 Redis 和 Postgres 的后台任务

Async-background — 为 Ruby 提供无需外部依赖的后台任务的 gem。解决资源管理和可靠性问题。适用于 Async 和 Falcon 上的应用。

如何在 Ruby 中运行无需 Redis 的后台任务?async-background 解决方案
Advertisement 728x90

# async-background:一个无需外部依赖的轻量级 Ruby 后台任务系统

Ruby 应用开发者常常需要处理后台任务,但像 Sidekiq 这样的现成解决方案都需要额外的基础设施。新 gem async-background 提供了一个替代方案,它基于 Async 运行,无需 Redis 或 Postgres,并支持细粒度资源控制。

现有解决方案的不足

Sidekiq 是 Ruby 中后台任务的常见选择,但它依赖 Redis。对于只有少量任务的小型项目,这太过头了:需要在 docker-compose 中多加一个容器,引入另一个故障点,还增加基础设施成本。而且,Sidekiq 不支持在 worker 之间灵活分配负载——一个耗时的夜间报告可能会拖慢用户邮件处理。

其他方案也有各自缺点:

Google AdInline article slot
  • whenever —— 生成系统 crontab,没有队列或应用集成。
  • rufus-scheduler —— 在独立线程中运行,在多进程的 Async 应用中会导致上下文切换和重复执行问题。
  • good_jobsolid_queue —— 绑定到 ActiveRecord 和 Postgres。这会带来风险:后台任务与事务负载竞争数据库资源,可能拖慢主应用。

因此,需要一个解决方案,它结合 cron 调度、间隔执行和动态队列,无需外部依赖,同时能控制资源使用。

核心设计原则

该 gem 的作者列出了六个核心原则:

  • 单一事件循环。 调度器与应用运行在同一个反应式循环中,无需独立线程。
  • 零基础设施。 队列存储在磁盘上(SQLite 的 WAL 模式或 tmp/ 中的文件),无需 Redis 或 Postgres。
  • 可选依赖。 额外功能(如指标)通过可选 gem 添加。
  • 无需协调器的多进程安全。 使用 Falcon(fork)时,确保任务只在一个进程中运行。
  • 持久化作为保障,速度作为独立机制。 可靠性来自 SQLite 存储,速度来自 UNIX socket 唤醒。
  • 托管 worker 利用率。 开发者可以通过环境变量和 YAML 配置将任务分配给特定 worker。

这些原则使得能够构建一个无缝集成到现有 Async 应用中的 gem,而无需额外基础设施。

Google AdInline article slot

架构特性

使用 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),因为调度与日历时间绑定。

Google AdInline article slot

代码实现如下:

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

Advertisement 728x90

继续阅读