Rails异步处理
了解Rails中一个简单的异步处理用例的实现,该用例中我集成了代码。
当我登录我的银行账户,想要一份我所有账户交易的报告,比如说,六个月或一年,网络应用程序说它收到了我的请求,并要求我稍后检查以获得PDF报告。一段时间后,我就可以下载该报告了。这是一个异步处理的例子。
在这篇文章中,我描述了Rails中一个简单的异步处理用例的实现。我有一个名为 "mahrasa "的示例应用程序,是Mahboob Rails 示例 应用程序的简称,我已将代码集成到其中。
用例
用户上传了一个CSV文件到应用程序。她收到一条消息说文件已经收到,正在处理中。一个链接被显示出来,用户可以在那里检查状态。在后端,文件被异步处理,处理后的状态在状态页面上被更新。
设计
Rails有许多宝石,可以实现异步处理。其中一些是delayed_job、Resque、sidekiq和delayed 。
我选择了delayed ,因为它是最新的孩子,并在其资源库页面上说明了高级功能,如下所示。
Delayed是一个多线程、SQL驱动的ActiveJob后端,在Betterment用于每天处理数百万个后台作业。它支持postgres、mysql和sqlite,并被设计为。
可靠性,具有共同交易的作业队列和保证至少一次的执行。
可扩展性,具有优化的拾取查询和并发作业执行功能
弹性,具有内置的重试机制、指数回退和失败作业保存功能
可维护性,具有强大的仪表、持续监控和基于优先级的警报功能
为什么选择
Delayed?
delayedgem是对delayed_job和delayed_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编码工作之前,我决定也检查一下其他的数据插入方法,并给它们计时,以了解它们的相对性能。这些选项是
- 逐行插入数据。
- 使用csvsql将文件复制到数据库中。
- 使用ActiveRecord-import批量插入行。
- 使用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
- 在另一个终端,启动
rakejobs。
$ 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。