Rails 最轻量级的 Rails 异步任务方案

huacnlee · 2020年05月12日 · 最后由 n5ken 回复于 2020年05月14日 · 6428 次阅读

SidekiqResque 这类 Background Job 服务当下已成为几乎所有 Rails 应用程序的标准配置。

但某些更微小的应用场景我们会发现单独部署一个 Sidekiq 进程有些复杂的,而且因为 Sidekiq 我们还得部署一个 Redis,我们往往只需要简单的异步而已,这个时候我们很想像 Go、Node.js 里面那样直接开一个异步动作处理或者自己搞一个消息订阅,这样部署可以和 Web 服务在同一个进程,无需复杂的额外部署。

难道 Rails 应用必须标配一个额外的后台异步服务么? 🙅🏻 不,我们有别的选择!

我分析发现,实际上 Rails ActiveJob 内置的 AsyncAdapter 是可以做到这样的。

如何使用

class AsyncJob < ActiveJob::Base
  # 为了让测试好验证,我们配置 test 环境用 inline 模式,其他时候用 AsyncAdapter
  self.queue_adapter = Rails.env.test? ? :inline : ActiveJob::QueueAdapters::AsyncAdapter.new(
    min_threads: 4,
    max_threads: 10 * Concurrent.processor_count)
end

class NotifyTopicNodeChangedJob < AsyncJob
  def perform
    # 轻量的异步逻辑
  end
end

需要注意点

关于 AsyncAdapter 的使用,Rails 官方 API 有提到

https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html

This is the default queue adapter. It's well-suited for dev/test since it doesn't need an external infrastructure, but it's a poor fit for production since it drops pending jobs on restart.

翻译谁都会,我说说这句我的理解,大概意思说这个本来是 ActiveJob 默认的 Adapter,它可以用在 dev/test 环境这样可以不需要额外的架构(异步服务),但不建议在生产环境用,因为重启会导致等待中 / 进行中任务丢失。

我的建议用于可以容错,能容忍微小数据丢失的场景,比如访问量 +1,重新统计数据(再次调用能修复),或者个别时候对数据不敏感的场景比如个人博客。

The adapter uses a Concurrent Ruby thread pool to schedule and execute jobs. Since jobs share a single thread pool, long-running jobs will block short-lived jobs. Fine for dev/test; bad for production.

这句意思说:此 Adapter 用 Concurrent 这个 Gem 利用 Ruby 的线程池方式来计划和执行异步任务,因为目前 ActiveJob::AsyncAdapter 的设计是所有异步任务都用的一个线程池(一个进程里面一个线程池),如果有重的异步任务,它们未结束之前会堵塞同一进程的其他任务,包括 HTTP 服务。

鉴于 Ruby GIL 的机制,上面的情况,同一个进程内,如果有重的耗费 CPU 的动作执行期间,可能会导致这段期间这个进程无法响应普通的 HTTP 请求,从而堵塞正常的 Web 服务。

在此根据我的理解,IO 类的异步任务,用于 AsyncAdapter 其实是可以的,在等待 IO 响应的期间,多线程能起作用,HTTP 服务可以继续处理,同时由于我们往往有多个 Puma worker,只要避免在 AsyncAdapter 的 Job 内出现那种非常耗费 CPU 的异步任务,对服务的影响是可以接受的。

总结

前面说那么多,我们到底能不能用 AsyncAdapter,什么场景用合适?

  • 非数据敏感场景,能接受个别任务丢失;
  • 数据不能丢,但再次调用,能重建;
  • 确保部署多个 Puma worker;
  • 个人博客,初创期的网站,需要节约内存的场景;
  • 低 CPU 开销的任务;
  • 比如采集动作、清理数据动作;

🌺本文只是告诉大家一个可选的方案,在资源足够的情况下,请用 Sidekiq 效果更佳!

其他选择

  • SuckerPunchAdapter - 它设计用来解决把异步任务泡在 Web Server 同一个进程内,我阅读了它的实现,在重启上比 AsyncAdapter 多一些优化,它会暂停重启一段时间,让异步任务有时间可以消化完。

挺好 saved my memory

2 楼 已删除

现在还有 GIL,不是 GVL?

搞 Ruby 的还在乎这么点内存? 😄

还是进程隔离好,ruby 多线程并不强。多进程也不是想象的那么耗内存。

鉴于 Ruby GIL 的机制,上面的情况,同一个进程内,如果有重的耗费 CPU 的动作执行期间,可能会导致这段期间这个进程无法响应普通的 HTTP 请求,从而堵塞正常的 Web 服务。

这个地方没太看明白,想请教一下。

无人知晓的 GIL 里说, Ruby 有一个 time thread,会给其他线程标记中断,被标记中断的线程,在执行完一个方法的时候,会调用 vm_call0_body 这个方法,如果有被中断,就会让给其他人执行。

我理解像 + 执行完,都会执行 vm_call0_body 这个方法。如果一个线程想一直占着 CPU 的话,除非是调用 C 方法。

不过也有一种情况,有多个后台任务在执行,打满线程池,就会堵塞正常的 Web 服务。

很好适合微小的应用场景,例如内部系统。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号