(2021年2月)提升 Ruby on Rails 导出 CSV 效率(耗时1小时变成耗时4分钟)
这篇文章讲什么?
讲我最近在优化一个 Ruby on Rails 写的网站里的 csv 导出。
优化前:导出1000+行的 csv 需要1个小时。
优化后:导出8000+行的 csv 需要4分钟。
这篇文章对谁有用?有什么用?
对谁有用?正在折腾 csv 导出的 Rails 程序员
有什么用?看我是如何提升效率的
说明
由于是公司内部的系统,
因此没法贴截图。也没法贴很具体的代码。只能写文字。只能贴一两行业务无关的其他代码。
功能描述(从用户使用的视角)
- 用户通过各种筛选条件,筛选出他所需要的条目(可能是300条,可能是1000条,可能更多)
- 用户点击右上角下拉菜单的"导出为 csv",
- 此时页面上方横幅文字提示 "已开始导出,完成后会通过短信通知"
- 用户在10分钟/30分钟/1小时左右(时间不定,取决于具体导出多少条)收到短信
- 用户进入同一个页面里 "导出为 csv" 下方的那一条 "导出历史"
- 用户在 "导出历史" 页面可以看到刚刚那条导出任务,点击右侧的 "下载"
- 下载得到一个 csv 文件
整个流程完成。
实现方法(从程序的角度)
- 用户点击"导出为 csv"会添加一个后台任务(background job)
- 后台任务里生成 csv 文件,然后把文件上传到 AWS S3 存着
- 把文件地址保存到数据库里,发短信通知用户,程序就此结束。
2月也放假了懒得写那么多细节,直接来重点
-
我们之前用的后台队列是 que,为了解决这个问题换成了 sidekiq,我个人认为对提升效率有一定帮助,但不是主要原因,只不过把 que 换成 sidekiq 这个事情是迟早要做的,这次就顺手做了。
-
尽量降低数据库的查询次数(有点算是废话,但还是要强调的)之前我们导出单个条目,需要23次数据库查询(我直接本地试一下然后看 log/development.log 就知道了),经过优化后降低到了7次(没法再降低了,因为数据的确存在7张不同的表里)
-
不要往内存里存一整个 csv 文件(不过这个对我们的影响也没那么大,实际导出8000+行数据得到的 csv 文件也才15MB)生成多少就直接往硬盘上写
-
使用
ActiveRecord::Base.connection.uncached
这个是影响因素最大的。
ActiveRecord::Base.connection.uncached do
# 把代码这样包起来
end
# 返回一个 Tempfile
def self.csv(application_ids, user_id)
temp_file = Tempfile.new('export_application_csv')
# 最有效减低内存消耗的是 ActiveRecord::Base.connection.uncached 这一句
ActiveRecord::Base.connection.uncached do # https://github.com/rails/rails/issues/27002#issuecomment-260086170
CSV.open(temp_file.path, 'w', write_headers: true, headers: get_headers()) do |csv|
application_ids.each do |a_id|
application = Apply::Application.find_by(id: a_id)
one_row = answer_for_each_application(application)
csv << one_row
csv.flush # https://stackoverflow.com/questions/34168814/does-rubys-csv-open-buffer-to-memory-and-write-all-at-once
end
end
end
return temp_file
end
其他试过但是不管用的方法
find_each
方法试过了,好像帮助不大(从内存占用角度而言)- 哪怕从 que 换成了 sidekiq,如果不用
ActiveRecord::Base.connection.uncached
内存占用会从 180MB(平时没任务的时候就是这么大,可以在 Sidekiq Web UI 里面看到)
上升到 2047MB。
结论
用 ActiveRecord::Base.connection.uncached
让 Rails 不要缓存。
在各种方法里是最有效的提升速度的方法