(2021年2月)提升 Ruby on Rails 导出 CSV 效率(耗时1小时变成耗时4分钟)

这篇文章讲什么?

讲我最近在优化一个 Ruby on Rails 写的网站里的 csv 导出。
优化前:导出1000+行的 csv 需要1个小时。
优化后:导出8000+行的 csv 需要4分钟。

这篇文章对谁有用?有什么用?

对谁有用?正在折腾 csv 导出的 Rails 程序员
有什么用?看我是如何提升效率的

说明

由于是公司内部的系统,
因此没法贴截图。也没法贴很具体的代码。只能写文字。只能贴一两行业务无关的其他代码。

功能描述(从用户使用的视角)

  1. 用户通过各种筛选条件,筛选出他所需要的条目(可能是300条,可能是1000条,可能更多)
  2. 用户点击右上角下拉菜单的"导出为 csv",
  3. 此时页面上方横幅文字提示 "已开始导出,完成后会通过短信通知"
  4. 用户在10分钟/30分钟/1小时左右(时间不定,取决于具体导出多少条)收到短信
  5. 用户进入同一个页面里 "导出为 csv" 下方的那一条 "导出历史"
  6. 用户在 "导出历史" 页面可以看到刚刚那条导出任务,点击右侧的 "下载"
  7. 下载得到一个 csv 文件

整个流程完成。

实现方法(从程序的角度)

  1. 用户点击"导出为 csv"会添加一个后台任务(background job)
  2. 后台任务里生成 csv 文件,然后把文件上传到 AWS S3 存着
  3. 把文件地址保存到数据库里,发短信通知用户,程序就此结束。

2月也放假了懒得写那么多细节,直接来重点

  1. 我们之前用的后台队列是 que,为了解决这个问题换成了 sidekiq,我个人认为对提升效率有一定帮助,但不是主要原因,只不过把 que 换成 sidekiq 这个事情是迟早要做的,这次就顺手做了。

  2. 尽量降低数据库的查询次数(有点算是废话,但还是要强调的)之前我们导出单个条目,需要23次数据库查询(我直接本地试一下然后看 log/development.log 就知道了),经过优化后降低到了7次(没法再降低了,因为数据的确存在7张不同的表里)

  3. 不要往内存里存一整个 csv 文件(不过这个对我们的影响也没那么大,实际导出8000+行数据得到的 csv 文件也才15MB)生成多少就直接往硬盘上写

  4. 使用 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 不要缓存。
在各种方法里是最有效的提升速度的方法