Sidekiq 、Resque 这类 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,什么场景用合适?
🌺本文只是告诉大家一个可选的方案,在资源足够的情况下,请用 Sidekiq 效果更佳!