首页 > 解决方案 > rails - 导出一个巨大的 CSV 文件会消耗生产中的所有 RAM

问题描述

所以我的应用程序导出了一个 11.5 MB 的 CSV 文件,并且基本上使用了所有永远不会被释放的 RAM。

CSV 的数据取自数据库,在上述情况下,整个数据都被导出。

我以下列方式使用 Ruby 2.4.1 标准 CSV 库:

export_helper.rb

CSV.open('full_report.csv', 'wb', encoding: UTF-8) do |file|
  data = Model.scope1(param).scope2(param).includes(:model1, :model2)
  data.each do |item|
    file << [
      item.method1,
      item.method2,
      item.methid3
    ]
  end
  # repeat for other models - approx. 5 other similar loops
end

然后在控制器中:

generator = ExportHelper::ReportGenerator.new
generator.full_report
respond_to do |format|
  format.csv do
    send_file(
      "#{Rails.root}/full_report.csv",
      filename: 'full_report.csv',
      type: :csv,
      disposition: :attachment
    )
  end
end

在单个请求之后,puma 进程加载了整个服务器 RAM 的 55% 并保持这种状态,直到最终完全耗尽内存。

例如,在本文中生成一个 75 MB 的 CSV 文件百万行只需要 1 MB 的 RAM。但是不涉及数据库查询。

服务器有 1015 MB RAM + 400 MB 交换内存。

所以我的问题是:

提前致谢!

标签: ruby-on-railscsvruby-on-rails-5

解决方案


而不是each您应该使用find_each专门针对此类情况的 ,因为它将批量实例化模型并随后释放它们,而each将立即实例化所有模型。

CSV.open('full_report.csv', 'wb', encoding: UTF-8) do |file|
  Model.scope1(param).find_each do |item|
    file << [
      item.method1
    ]
  end
end

此外,在将 CSV 发送到浏览器之前,您应该流式传输 CSV 而不是将其写入内存或磁盘:

format.csv do
  headers["Content-Type"] = "text/csv"
  headers["Content-disposition"] = "attachment; filename=\"full_report.csv\""

  # streaming_headers
  # nginx doc: Setting this to "no" will allow unbuffered responses suitable for Comet and HTTP streaming applications
  headers['X-Accel-Buffering'] = 'no'
  headers["Cache-Control"] ||= "no-cache"

  # Rack::ETag 2.2.x no longer respects 'Cache-Control'
  # https://github.com/rack/rack/commit/0371c69a0850e1b21448df96698e2926359f17fe#diff-1bc61e69628f29acd74010b83f44d041
  headers["Last-Modified"] = Time.current.httpdate

  headers.delete("Content-Length")
  response.status = 200

  header = ['Method 1', 'Method 2']
  csv_options = { col_sep: ";" }

  csv_enumerator = Enumerator.new do |y|
    y << CSV::Row.new(header, header).to_s(csv_options)
    Model.scope1(param).find_each do |item|
      y << CSV::Row.new(header, [item.method1, item.method2]).to_s(csv_options)
    end
  end

  # setting the body to an enumerator, rails will iterate this enumerator
  self.response_body = csv_enumerator
end

推荐阅读