Rails异步处理的详细指南

125 阅读5分钟

Rails异步处理

了解Rails中一个简单的异步处理用例的实现,该用例中我集成了代码。

当我登录我的银行账户,想要一份我所有账户交易的报告,比如说,六个月或一年,网络应用程序说它收到了我的请求,并要求我稍后检查以获得PDF报告。一段时间后,我就可以下载该报告了。这是一个异步处理的例子

在这篇文章中,我描述了Rails中一个简单的异步处理用例的实现。我有一个名为 "mahrasa "的示例应用程序,是Mahboob Rails 示例 应用程序的简称,我已将代码集成到其中。

用例

用户上传了一个CSV文件到应用程序。她收到一条消息说文件已经收到,正在处理中。一个链接被显示出来,用户可以在那里检查状态。在后端,文件被异步处理,处理后的状态在状态页面上被更新。

设计

Rails有许多宝石,可以实现异步处理。其中一些是delayed_jobResquesidekiqdelayed

我选择了delayed ,因为它是最新的孩子,并在其资源库页面上说明了高级功能,如下所示

Delayed 是一个多线程、SQL驱动的ActiveJob后端,在Betterment用于每天处理数百万个后台作业。

它支持postgres、mysql和sqlite,并被设计为。

  • 可靠性,具有共同交易的作业队列和保证至少一次的执行。

  • 可扩展性,具有优化的拾取查询和并发作业执行功能

  • 弹性,具有内置的重试机制、指数回退和失败作业保存功能

  • 可维护性,具有强大的仪表、持续监控和基于优先级的警报功能

为什么选择Delayed

delayed gem是对delayed_jobdelayed_job_active_record 的有针对性的分叉,将它们合并为一个库。它是为Betterment的各种操作需求而设计的,包括从Betterment的代码库中提取的许多功能,例如。

  • 多线程作业执行通过 concurrent-ruby

  • 一个高度优化的、基于SKIP LOCKED 的拾取查询(在postgres上)。

  • 通过一个新的monitor 进程,内置仪器和持续监控

  • 命名的优先级范围,默认为:interactive,:user_visible,:eventual, 和:reporting

  • 基于优先级的工作年龄、运行时间和尝试的警报阈值

  • 一个实验性的自动缩放指标,供水平自动缩放器使用(我们使用Kubernetes)。

  • 一个自定义的适配器,用Delayed 特定的行为扩展了ActiveJob

让我在这里补充一个免责声明,我还没有验证所有的说法,所以这篇文章并不是说你应该在你的应用程序中使用delayed

delayed 的安装步骤非常简单。

  • 在你的Gemfile中添加以下内容:
gem 'delayed'
  • 运行bundle install
  • 创建表delayed_jobs
$ rails generate delayed:migration rails db:migrate
  • 在config/application.rb中添加以下一行。
config.active_job.queue_adapter = :delayed 

用psql在PostgreSQL中插入数据是快得惊人的。然而,mahrasa使用SQLite3数据库。在这种情况下,相当于psql的是SQLite3本身。在用SQLite3编码工作之前,我决定也检查一下其他的数据插入方法,并给它们计时,以了解它们的相对性能。这些选项是

  1. 逐行插入数据。
  2. 使用csvsql将文件复制到数据库中。
  3. 使用ActiveRecord-import批量插入行。
  4. 使用SQLite3将文件复制到数据库中。

对于每个选项,我都写了一个应用程序作业,其细节如下。

ImportGdcJob

这个作业是选项1的实现。它在一个循环中读取输入的CSV文件,对于每一行,它在数据库中插入一行,调用模型Gdc的create 方法。

ImportGdcJob2

这个作业是选项2的实现。csvsql需要一个带有列名的标题行。我的CSV文件没有头行。因此,这个作业首先创建一个temp.csv文件,第一行有列名,然后附加整个输入CSV文件。然后,它运行工具 csvsql 将文件复制到数据库中。你可以在一个名为csvkit 的Python工具包中安装csvsql。

$ pip install csvkit

ImportGdcJob3

这个作业是选项3的实现。它通过调用模型类Gdc的input 方法,用ActiveRecord-import批量插入数据。

ImportGdcJob4

这个作业是选项4的实现。它执行一个系统语句来运行SQLite3,将一个shell脚本作为输入。shell脚本创建一个temp_table ,并将输入的CSV文件数据导入其中。然后它将数据从temp_table 插入到global_daily_cumulative 。这种通过temp_table 的数据路由处理了主列中的自动id生成,而SQLite3并没有处理这个问题。

运行和测试这些工作的程序如下:

  • 运行 "如何运行"部分中的步骤,直到创建表。
  • 在ImportGdcJob4.rb中,注释这一行。

Ruby

AsyncOperation.where(id: job.arguments.first[:id]).update(:status => "processed")
  • 在一个终端中,启动rake 工作。

Ruby

$ rake delayed:work
  • 在另一个终端,启动Rails控制台并调用作业的类名。
$ rails c 

> ImportGdcJob2.perform_later 

下面的屏幕截图显示了分别在终端1和终端2中运行ImportGdcJob2 的输出。

由于ImportGdcJob 是逐行插入数据库,我知道它的速度会非常慢,所以我只用1000行来运行它。执行时间是按照预期的顺序进行的。

工作时间(秒)
导入GdcJob

整合到Rails中

由于速度最快,第四种方案是首选方案。该作业在控制器中作为异步操作被调用,如下面的代码块所示:

def import
    # copy uploaded file app/jobs directory
    FileUtils.cp(File.new(params[:csvfile].tempfile),
                 "#{Rails.root}/app/jobs/global_daily_cumulative.csv")

    # insert async_operations row
    @filename = params[:csvfile].original_filename
    @ao       = AsyncOperation.new(:op_type => 'Import CSV',
                                   :filename => @filename,
                                   :status => :enqueued)

    # enqueue the job
    if @ao.save
        ImportGdcJob4.perform_later(id: @ao.id)
    end
    
    render :ack
end

视图渲染了一个链接来检查数据插入工作的状态。

如何运行

  • 克隆我的版本库:
$ git clone https://github.com/mh-github/mahrasa.git -b delayed1
  • 进入项目文件夹:
$ cd mahrasa
  • 确保你安装了Ruby 3.1.2并使用它:
$ rvm install 3.1.2
$ rvm use 3.1.2
  • 安装宝石:

Ruby

$ bundle install

Shell

$ bin/rails db:migrate RAILS_ENV=development
  • 创建表。在SQLite3提示符下或者在SQLite浏览器或DBeaver等数据库IDE中执行以下命令。

SQL

CREATE TABLE "global_daily_cumulative" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "date" TEXT, "place" TEXT, "confirmed" INTEGER, "deaths" INTEGER, "recovered" INTEGER);
  • 取消注释行。如果你在测试工作时注释了ImportGdc4.rb行,请取消注释。
  • 运行服务器。

外壳

$ rails s
  • 在另一个终端,启动rake jobs。
$ cd mahrasa
$ rake delayed:work
  • 在浏览器中访问应用程序:http://localhost:3000。
  • 点击链接 "Upload global_daily_cumulative.csv"。
  • 点击 "选择文件 "按钮。
  • 在文件资源管理器中,导航到mahrasa/test文件夹,选择文件global_daily_cumulative.csv
  • 你会看到一条消息,说文件已经收到,并给出一个链接,以检查工作的状态。如果你点击该链接,你将进入状态页面,了解作业的当前状态。

当作业被排队时查看。

在作业处理完毕后查看。

你可以在数据库中检查CSV文件的行数是否与表中的记录数相同:

sqlite> select count(*) from global_daily_cumulative;

158987

最后的思考

"延迟 "这个词有一个不幸的负面含义。当我第一次听到 "延迟作业 "这个词时,我认为这些作业是由于代码效率低下而导致的缓慢和低效作业,必须在服务器/数据库层面进行调整,甚至是代码审查。后来,我意识到它们实际上是什么。这些只是异步执行的对象,"延迟 "一词被用作形容词,因为它们使用了名为 "delayed_job "的库。

使用可用的宝石,将它们与你的样本工作负载进行计时。可能会发生这样的情况:这些之间的速度差异并不那么关键。对于真正的大批量处理,你可能要先去找RabbitMQ,最后再找Apache Kafka