株式会社リゾーム システム企画・開発部 第4グループの尾古(@patorash)です。
弊社で扱っているシステムで、複数のgzip圧縮したファイルを読み込んでDBにインポートする処理がありました。今回はそれのリファクタリングを行い、10%ほど速度改善した話です。
リファクタリング前の処理
リファクタリング前は、ファイルの内容を読み込んで変数に入れた後、インポート処理を行っていました。そのインポート処理においても、巨大文字列を1行ずつ読み込んで処理するようにしてありました。
file_path = Rails.root.join('files', 'import_file.jsonl.gz') contents = Zlib::GzipReader.open(file_path) do |gz| gz.read end ImportFile.import_from_string!(contents) class ImportFile class << self # 文字列からデータをインポートする # @param [String] contents 改行コード区切りのJSON文字列 # @option [Integer] batch_size 何件毎にインポートするかを指定。デフォルト1,000件。 def import_from_string!(contents, batch_size: 1_000) records = [] contents.each_line do |line| records << JSON.parse(line.chomp) if records.size == batch_size # activerecord-importを使ってバルクインサート import(records, validate: false, batch_size: batch_size) records = [] end end ImportFile.import(records, validate: false) end end end
問題点
まず、最も問題なのは、ファイルの内容を一度に全て読み込んでしまっているところです。今回の処理では、gzip圧縮から解凍した際のファイルサイズは100MBを超えるくらいのものでした。これが100MBどころではなく、もっと大きなファイルであった場合、メモリが足りなくなる可能性があります。
そして、次の問題点は、変数の内容を1行ずつ読み込んでいるところでした。1行ずつ読み込むこと自体はそんなに悪くないのですが、activerecord-importでバルクインサートをするかどうかのif文のチェックが毎回実行されていました。
リファクタリング後の処理
では、こちらをリファクタリングしていきます。
file_path = Rails.root.join('files', 'import_file.jsonl.gz') io = File.open(file_path, 'r') Zlib::GzipReader.wrap(io) do |gz| ImportFile.import_from_io!(gz) end class ImportFile class << self # IOオブジェクトからデータをインポートする # @param [IO] io IOオブジェクト # @option [Integer] batch_size 何件毎にインポートするかを指定。デフォルト1,000件。 def import_from_io!(io, batch_size: 1_000) io.each_slice(batch_size) do |lines| jsons = lines.map { |line| JSON.parse(line.chomp) } # activerecord-importを使ってバルクインサート import(jsons, validate: false) end end end end
変更点
IOオブジェクトを使うようにした
ファイルを一気に読み込むのではなく、IOオブジェクトから読み込むようにしました。
#each_slice
で1,000行ずつ読み込む
#each_line
メソッドで1行ずつ読み込むのをやめて、#each_slice
メソッドを使って、1,000行ずつ読み込むようにしました。1,000行読み込んでいるので、わざわざ1,000件のデータがあるかどうかのチェックは不要になったので、if文は削除できました。また、読み込む量も1,000行分毎で済むため、メモリ使用量も少なくて済みます。
パフォーマンス確認
「推測するな、計測せよ」というわけで、benchmark-ipsを使ってパフォーマンスの違いを検証しました。圧縮を解凍したら合計140MBくらいのデータで実験しています。
検証環境のスペック
- PC: MacBook Pro (13-inch, 2018, Four Thunderbolt 3 Ports)
- CPU: 2.3 GHz クアッドコアIntel Core i5
- メモリ: 16 GB 2133 MHz LPDDR3
- Ruby: 2.7.5
結果
11%も速くなりました!🚀
Warming up -------------------------------------- io 1.000 i/100ms text 1.000 i/100ms Calculating ------------------------------------- io 0.018 (± 0.0%) i/s - 1.000 in 56.379435s text 0.016 (± 0.0%) i/s - 1.000 in 62.673752s Comparison: io: 0.0 i/s text: 0.0 i/s - 1.11x (± 0.00) slower
かなり効果があったと言えると思います。
まとめ
今回はファイルから一気に読み込んでから処理するのではなく、IOオブジェクトを使うようにリファクタリングしたケースをご紹介しました。
ファイルを一気に読み込むのは割とやってしまいがちなのですが、ファイルサイズが大きいデータが想定される場合はあまり良い方法ではありません。そういう場合はIOオブジェクトを活用しましょう。