ruby-on-rails - rails - 导出一个巨大的 CSV 文件会消耗生产中的所有 RAM

标签 ruby-on-rails csv ruby-on-rails-5

所以我的应用程序导出了一个 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

然后在 Controller 中:
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% 并保持这种状态,直到最终完全耗尽内存。

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

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

所以我的问题是:
  • 究竟是什么消耗了这么多内存?是 CSV 生成还是与 DB 的通信?
  • 我是不是做错了什么而错过了内存泄漏?或者这只是图书馆的工作方式?
  • 有没有办法在不重启 puma workers 的情况下释放内存?

  • 提前致谢!

    最佳答案

    您应该使用 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 而不是将其写入内存或磁盘,然后再将其发送到浏览器:
    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
    

    关于ruby-on-rails - rails - 导出一个巨大的 CSV 文件会消耗生产中的所有 RAM,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52833450/

    相关文章:

    mysql - MySQL自动导入的错误日志?

    node.js - 循环二进制 Float64Array 文件 - NodeJS

    ruby-on-rails-5 - 通过 Rails 5 API 和 Active Storage 接受图像

    ruby-on-rails - Angular 2 和 ActionCable 集成

    ruby-on-rails - Windows 7 上的 MySQL gem 安装错误

    ruby-on-rails - 设计中的范围究竟是什么?

    ruby-on-rails - 可以 haml 渲染<需要输入类型 ="text">

    java - CSV 到平面 xml java 程序

    ruby-on-rails - Rails - html block 的条件显示

    html - 如何将用户范围划分为每行 3 列,而不是 1 列?