Ruby 关于后台作业处理的一点思考和行动

a112121788 · 2021年08月25日 · 最后由 hellorails 回复于 2021年08月26日 · 345 次阅读

Rails 开发中,如果遇到需要异步处理 (后台作业) 的任务,基本上都会用 Sidekiq 来解决这个问题。 但是 Sidkiq 并不是解决这个问题的唯一方案,比如还有 Delayed Job, Resque、AsyncAdapter 等。

后台作业是不是到 Sidekiq 这里就再无改进空间了,不一定。

在写 《Rails in Action》 时,谈到了后台作业调度相关的技术。

本来也以为没什么讲的,最多就是把 Sidekiq 源码分析一遍,感觉没什么意思(但是撸源码还是很重要的,只是源码分析不是那本书的重点)。

我希望能找到一个能替代 Sidekiq 的后台作业调度框架,《Rails in Action》的写作也就先暂停了。

根据 Rails API 文档,我找到如下 Active Job 支持的常见队列后端

Active Job 简介

Active Job 框架负责声明作业,这些作业可在各种队列后端中运行。

Active Job 主要作用是确保所有 Rails 应用都有作业基础设施。这样便可以在此基础上构建各种功能和其他 gem,而不用担心不同作业运行程序(如 Sidekiq 和 Resque)的 API 之间的差异。

Active Job 的主要作用在于指定一种规范,这样开发者切换队列后端也不用重写 Job/Worker,有了 Active Job 之后,我们还需要后台作业处理框架。 也即是上面的队列后端。

Active Job 支持的队列后端的功能差异

异步 队列 延迟 优先级 超时 重试
Backburner 支持 支持 支持 支持 Job 全局
Delayed Job 支持 支持 支持 Job 全局 全局
Que 支持 支持 支持 Job 不支持 Job
queue_classic 支持 支持 支持 不支持 不支持 不支持
Resque 支持 支持 支持 Queue 全局 支持
Sidekiq 支持 支持 支持 Queue 不支持 Job
Sneakers 支持 支持 不支持 Queue Queue 不支持
Sucker Punch 支持 支持 支持 不支持 不支持 不支持
Active Job Async 支持 支持 支持 不支持 不支持 不支持
Active Job Inline 不支持 支持 N/A N/A N/A N/A

作业队列和消息队列

最初,我也混淆了作业队列和消息队列。Sneakers 又加深了我的这种误解。不过他们确实不同,这里就不展开讲了。

作业调度框架都需要一个存储任务的队列服务,队列只要能满足先进先出即可。可选择的技术也很多,比如

  1. 数据库(MySQL、Postgres、MongoDB、SQLite、Redis)
  2. 消息队列 (RabbitMQ)
  3. 线程池

Backburner 选择了 beanstalkd, Delayed Job 选择了数据库(MySQL、Pg、MongoDB 都可以), Que 选择了 PostgreSQL 数据库。 queue_classic 选择了 PostgreSQL 数据库,Resque 选择了 Redis, Sidekiq 选择了 Redis,Sneakers 选择了 RabbitMQ, Sucker Punch 选择了 concurrent-ruby(线程池),Active Job Async Job 也选择了 concurrent-ruby(线程池)。 最后 Active Job Inline 相对简单,不支持异步,但是还是支持了队列。

先谈一谈 Resque 和 Sidekiq。Sidekiq 性能优于 Resque,主要在于技术选型,首先, Sidekiq 中 Job 是在线程中执行的,而 Job 在 Resque 中是在进程中执行,创建进程的开销远高于线程。 其次,两者都使用了 Redis 作为作业队列服务,Redis 实现简单消息队列的几种方案:

  1. 基于 List 的 LPUSH + (B) RPOP 的实现
  2. 基于 List 的 RPUSH + (B) LPOP 实现。
  3. PUB/SUB,订阅/发布模式
  4. 基于 Sorted-Set 的实现
  5. 基于 Stream 类型的实现

Sidekiq 选择的是 LPUSH + BRPOP,而 Resque 选择的是 RPUSH + LPOP。 Resque 其实再优化优化也能达到 Sidekiq 的性能。只是修改后也仅能达到 Sidekiq 的性能,并不能超越它,意义不大。

本来我想体验一下 Sneakers, Sneakers 的队列服务基于 RabbitMQ ,RabbitMQ 是一款使用 Erlang 语言开发的开源消息中间件。但是 Sneakers 并不支持延迟执行 Job。我猜测是由于 RabbitMQ 的缘故。先说结论,Sneakers 的表现现的并不优秀。

最后我发现了一个被忽略的方案——Backburner。从功能对比上,可以发现 Backburner 是全能型的,但为什么全能型的选手败给了——Sidekiq。

下面才是本文的重点

beanstalkd

我比较关心 Backburner 的队列服务的实现。看了下 Backburner 的源码, 发现其并没有独自实现消息服务、也没有使用数据库、线程池等常规方案。而是选择了 beanstalkd,一个没听过的东西。

这里要吐槽下 Backburner, 它的 README 中的 beanstalkd 的链接不对。 最后总算找到了—— https://beanstalkd.github.io/。 beanstalkd 的安装和运行非常简单。

接着建一个 Rails Demo 项目,体验 Backburner, 体验后也没啥发现 Backburner 有啥特别的,也没有 SideKiq 快。 Backburner 最新的 gem 发布时间仍然停留在 2018 年。

本着实用角度,先测试下性能。

默认配置性能对比

处理 10000 个 空白 Job(perform 方法什么都不做),主要对比入队用时和 Worker 处理用时。

Ruby 版本 入队用时 作业时间 消息接收吞吐量 处理吞吐量
2.7.4 Sidekiq 6.0.2 2 秒 5.3 秒 5000/秒 1869/秒
2.7.4 Resque 2.1.0 1.6 秒 110 秒 6250/秒 91/秒
2.7.4 Backburner 1.5.0 4.5 秒 26.3 秒 2222/秒 380/秒

以上数据是在同一环境下作的测试, 数据说明 Sidekiq 的处理效率最快,Resque 的入队效率最快。

而 Backburner 在入队操作上比 Sidekiq 和 Resque 都慢。在处理效率上比 Resque 好,当扔不急 Sidekiq 的一般。

不能就这样把 Backburner,确切地说是 beanstalkd 否定了。去翻了下 Sidekiq、Resque 和 Backburner 的源码。

通过源码发现,Sidekiq 的默认配置比较好, Resque 的默认配置较差,Sidekiq 默认用多线程执行 Job,Resque 默认采用进程模式执行 Job。

Backburner 的消息队列采用 beanstalkd。beanstalkd 是一个专注做作业队列的软件。在入队速度上还干不过 Redis 的 List 是有原因的

Redis 的 List 的元素插入和删除的时间复杂度为 O(1),而 beanstalkd 为了实现可以给统一队列中的 Job 设置不同的优先级, 并没有选择 queue 这种数据结构,而是选择了最小堆(这样优先级低的 Job 可以先执行),其插入和删除元素的复杂度为 O(log(n))。 相对来说,入队会慢一点,不过可接受。同时 beanstalkd 还支持 Delay Job。

beanstalkd 的消息协议非常简单。可以在多种编程语言中使用 beanstalkd。

比如你可以使用 Ruby 发送作业,然后使用 Go 处理作业。 Sidekiq 目前是不支持这样操作,以后大概率也不会。

我有一个想法

基于 beanstalkd ,做一个跨语言任务调度框架。 这时候我看到了 Sidekiq 作者的新项目 faktory。faktory 是一个支持多语言的作业调度框架。不过目前他依然选择 Redis 作为其底层作业队列服务。 但 Redis 目前不支持最小堆或最大堆,这也决定了 faktory 很难支持为 Job 单独设置优先级。

其实我可以给 Backburner 提 PR,但是我的想法不一定和 Backburner 作者的想法相同。

beanstalk_worker_ruby 是我基于 Backburner 开发的,我打算让它的使用体验更接近并超越 Sidekiq。

Resque VS Sidekiq VS Faktory VS beanstalk_worker_ruby

经过几天的改造,再做一次性能对比。

语言 版本 入队用时 作业时间 消息接收吞吐量 处理吞吐量
Ruby 2.7.4 Resque 2.1.0 2 秒 119/秒 5000/秒 84/秒
Ruby 2.7.4 Sidekiq 6.0.2 2 秒 7/秒 5000/秒 1428/秒
Ruby 2.7.4 Faktory 1.5.2 1.84 秒 7/秒 5000/秒 1428/秒
Go 1.16.6 Faktory 1.5.2 2.4 秒 2.7/秒 4166/秒 3703/秒
Ruby 2.7.4 beanstalk_worker_ruby 0.0.2 4.2 秒 13.50/秒 2380/秒 740/秒
Ruby 2.7.4 beanstalk_worker_ruby 0.0.3 4.48 秒 12.83/秒 2232/秒 780/秒
Node.js 14 beanstalk_worker_node 0.0.3 1.8 秒 4.76/秒 2100/秒 5555/秒
Go 1.16.5 beanstalk_worker_go 0.0.3 0.79 秒 2.28/秒 12658/秒 4385/秒

beanstalk_worker_ruby 与 Sidekiq 的差距缩小了很多。beanstalk_worker_go 甚至操过了 Faktory Go 。

BeanstalkMQ 相关项目

beanstalk-mq

beanstalkq 的改进版,计划提供 auth 支持。

beanstalk_worker_ruby

BeanstalkMQ 的 Ruby 客户端,基于 Backburner 二次开发。

beanstalk_worker_node

BeanstalkMQ 的 Node.js 客户端,基于 jackd 二次开发。

beanstalk_worker_go

BeanstalkMQ 的 Go 客户端,底层依赖于 go-beanstalk

beanstalk-mq-adminui

beanstalk-mq 的一个 Web 管理界面,基于 aurora 二次开发。

beanstalk_worker_view

beanstalk-mq 的 另一个 Web 管理界面,嵌入到 Rails 应用程序中,基于 beanstalkd_view二次开发。

BeanstalkMQ 目前版本: 0.0.3。

感兴趣的读者可以加笔者微信 (cGVuZ3BlbmctLXZpcA==),或在留言区交流作业调度相关的技术

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