質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Ruby on Rails 5

Ruby on Rails 5は、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

Ruby

Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

Q&A

解決済

2回答

2583閲覧

【Rails5】10万件超のCSVエクスポート方法について

ssk

総合スコア332

Ruby on Rails 5

Ruby on Rails 5は、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

Ruby

Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

0グッド

1クリップ

投稿2018/10/11 01:32

編集2018/10/15 07:34

前提・実現したいこと

Rails5でCSVダウンロードを作っています。
10万件超のデータをメモリ負荷を考慮してCSVエクスポートしたいです。

発生している問題・エラーメッセージ

puma-worker-killerにメモリ不足でプロセスを切られてしまいます。

該当のソースコード

ruby

1class ExportsController < ApplicationController 2 def index 3 items = Item.all 4 render_csv(items) 5 end 6 7 def render_csv(records) 8 filename = "Item_#{Time.current.to_i}.csv" 9 self.response.headers['Content-Type'] ||= 'text/csv; charset=Shift_JIS' 10 self.response.headers['Content-Disposition'] = "attachment;filename=#{filename}" 11 self.response.headers['Content-Transfer-Encoding'] = 'binary' 12 self.response.headers['Last-Modified'] = Time.current.ctime.to_s 13 14 self.response_body = Enumerator.new do |y| 15 names = Item.csv_column_names 16 y << encode_sjis(names.values.to_csv) 17 18 records.find_in_batches do |group| 19 group.each do |record| 20 values = record.csv_column_values 21 y << encode_sjis(values.map{|k, v| v.call(record)}.to_csv) 22 end 23 GC.start 24     Rails.logger.info "memory using #{ObjectSpace.memsize_of_all * 0.001 * 0.001} MB" #追記 25 end 26 end 27 end 28end

試したこと

render_csvの中でfind_eachfind_in_batchesを500件や100件ずつで試してみましたが、状況は変わらず。

追記

■コードに追加した内容
Rails.logger.info "memory using #{ObjectSpace.memsize_of_all * 0.001 * 0.001} MB"

■以下、tail -f development.log | grep --line-buffered "memory using"の結果です。
memory using 368.840598 MBでpuma-worker-killerが発動しています。

memory using 185.512727 MB memory using 236.11603 MB memory using 218.543507 MB memory using 249.18884 MB memory using 258.381909 MB memory using 238.562832 MB memory using 325.655192 MB memory using 320.25811200000004 MB memory using 286.946893 MB memory using 313.74037 MB memory using 276.651044 MB memory using 296.949558 MB memory using 311.553982 MB memory using 302.603379 MB memory using 333.15229800000003 MB memory using 352.51917 MB memory using 368.840598 MB

追記2

■Rails.logger.infoの前にGC.startを追加した後のメモリ使用量
先程に比べるの一定してメモリが増えるようになりました。

memory using 173.27937400000002 MB memory using 211.016275 MB memory using 218.16826200000003 MB memory using 224.72966 MB memory using 234.490212 MB memory using 241.19169300000001 MB memory using 248.46728700000003 MB memory using 256.185135 MB memory using 265.241077 MB memory using 272.950707 MB memory using 279.282393 MB memory using 287.88837800000005 MB memory using 295.887373 MB memory using 305.225944 MB memory using 308.89255099999997 MB memory using 317.53945500000003 MB →puma-worker-killer発動

追記3

find_in_batches(:batch_size => 100)の時のメモリ状況
ダウンロードできました。が、メモリ増加は改善しませんでした。

・ ・ ・ memory using 618.732024 MB memory using 619.4192240000001 MB memory using 620.1064240000001 MB memory using 620.79362 MB memory using 621.48081 MB memory using 622.1680080000001 MB memory using 622.842653 MB memory using 623.016078 MB memory using 623.900232 MB memory using 621.7092120000001 MB

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

tabuu

2018/10/11 02:05

試したことについてもコードを提示していただいた方が回答を得やすいと思います。
ssk

2018/10/11 08:39

renderしている処理も提示いたしました。
guest

回答2

0

その後の話になるかもしれませんが、おそらくタイムアウト問題も出てくるでしょう
zipに圧縮してダウンロードさせる等の工夫も検討したほうが良いと思います

また、それでもどうにもならない時は

  1. ダウンロード受付データを作り、一旦「処理中です」と返す画面を見せる
  2. ActiveJobを使いバックグラウンドでCSV作成&zip圧縮
  3. 作成完了後にその画面が更新されたら zip のダウンロードリンクを表示する

等の、待ってもらう工夫が必要でしょう
1万件のタイミングで、ダウンロード受付データの進捗を更新して
作成中に更新したら「現在何%」と出すと更に親切ですね

ただ、作り込むとキリがないので妥協も必要です

投稿2018/10/11 07:29

Ighrs

総合スコア656

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ssk

2018/10/11 08:43

ありがとうございます。 render処理を追加いたしました。 takahashim様にも同様の追加質問をしていますが、ストリーミングとローカルCSV書き出しとで、メモリ使用量は異なるのでしょうか?
Ighrs

2018/10/11 08:54

ストリーミングは本題に無関係だと思っています puma-worker-killerが出るタイミングは把握していますか? 配信以前の作成中に出ているのではないでしょうか? CSV作成時のメモリ削減:都度ファイルに書き出す 通信量の削減: zip圧縮 という意図で書いています
ssk

2018/10/15 00:36

配信以前の作成中にでています。 なるほど、都度ファイルに書き出す&zip圧縮で実装してみます。
guest

0

ベストアンサー

find_eachfind_in_batches を使っても、例えば読み込んだ結果のCSVファイルをメモリに貯めていると不味そうです。
出力用の一時的なCSVファイルを用意し、CSVのデータはそこに保存しておいて、最後にそれを send_file 等を使って返してやるとよいかと思います。

また、find_eachなどの代わりに pluck を使って、ARオブジェクトを作らないようにすることも検討した方がよいかと思います。


(追記)

あああー、これはぜんぜん違う話でしたね。

ruby

1 items = Item.all

がダメです。allはNGです。
RailsガイドのActive Record クエリインターフェイスの1.2 複数のオブジェクトをバッチで取り出すに書かれている、「# このコードはテーブルが大きい場合、メモリを大量に消費する可能性あり」の例そのものになってます。

recordsを渡すのではなく、

def index render_csv end

みたいにしておいて、

ruby

1 def render_csv 2## (略) 3 self.response_body = Enumerator.new do |y| 4 names = Item.csv_column_names 5 y << encode_sjis(names.values.to_csv) 6 7 Item.find_in_batches do |group| 8 group.each do |record| 9 values = record.csv_column_values 10 y << encode_sjis(values.map{|k, v| v.call(record)}.to_csv) 11 end 12 end 13 end 14 end

というように、find_in_batchesで初めてオブジェクトを生成するようにしないと、find_in_batchesとかを使うメリットがないです。

投稿2018/10/11 03:19

編集2018/10/11 10:16
takahashim

総合スコア1877

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ssk

2018/10/11 08:40

ありがとうございます。 render処理を追加いたしました。 ストリーミングとローカルCSV書き出しとで、メモリ使用量は異なりますか?
takahashim

2018/10/11 09:01

ストリーミングとCSV書き出しのやり方次第ではありますが、Enumeratorを使うよりは素朴にIO#writeを使ってローカルにCSVを作る方が確実かとは思います。
takahashim

2018/10/11 10:17

すみません、コードを良く見たらぜんぜん違う話だったので追記しました。
ssk

2018/10/15 00:57

追記、ありがとうございます。上記の方法で試してみましたが、途中でpuma-worker-killerに怒られてしまいました。 ■怒られた内容 PumaWorkerKiller: Out of memory. 2 workers consuming total: 1005.00390625 mb out of max: 921.6 mb. Sending TERM to pid 3723 consuming 676.84375 mb.
ssk

2018/10/15 02:30

度々、申し訳ございません。 エクスポート時のメモリ状況を追記いたしました。
takahashim

2018/10/15 03:08

うーん、そうですか。 ちょっと遅くなりますが、「Rails.logger.info…」の前の行に「GC.start」を入れてみるとどうでしょうか?
ssk

2018/10/15 03:21

GC.startを追加しました。 追記2にメモリ状況を追記いたしました。
takahashim

2018/10/15 04:58

うーん、実際にメモリを使っているようですね。 大々的な修正は手間がかかりそうなので、小手先の対応としては、 Item.find_in_batches do |group| を Item.find_in_batches(:batch_size => 100) do |group| にして、1000件単位から100件単位に変えてみるとどうでしょうか。
ssk

2018/10/15 07:36

追記3を追加しました。 エクスポートはできましたが、メモリが徐々に増加する現象は改善できませんでした。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問