
Railsで数百万件のデータを高速処理!バルクインサート・並列処理・リトライの最適解

IT技術
はじめに
Railsアプリケーションでは、大量のデータを処理する必要があるケースが多くあります。例えば、データの移行、レポートの生成、バッチ更新などが該当します。しかし、何も考えずに処理を実装すると、メモリ不足や処理の遅延、データ整合性の問題が発生する可能性があります。実際の業務でもそうでした。
背景
多くのRailsアプリケーションはデータ量が増えるにつれて、処理速度やリソースの問題に直面します。例えば、以下のような課題が挙げられます。
- each でレコードを1件ずつ処理すると、メモリ使用量が増大する。
- 一括インサートしないとデータの登録処理が遅くなる。
- シングルスレッドで処理すると、バッチ処理が長時間かかる。
- 途中でエラーが発生した際のリカバリーが難しい。
こうした課題を解決するために、適切なバッチ処理の設計が必要になります。本記事では、数百万件のデータを効率的に処理するための手法を解説します。
前提
- レコード数:100万件程度
- カラム数:5〜10個程度
- 文字列、整数、日付などをバランスよく含む
- process(record) はrecord の id や name を取得する程度の比較的軽い処理
上記のようなデータを想定しております。
バッチ処理手法
1. ActiveRecord の find_each vs each の使い分け
each の問題点
1Model.all.each do |record|
2 process(record)
3end
この方法では、全レコードをメモリにロードしてしまうため、大量データを扱うとメモリ不足に陥る可能性があります。
find_each の活用
1Model.find_each(batch_size: 1000) do |record|
2 process(record)
3end
find_each を使うことで、データをバッチ単位で取得し、メモリの使用量を抑えながら処理を進めることができます。
each vs find_each のメモリ使用量の可視化
メモリ使用量を memory_profiler で比較してみます。
1require 'memory_profiler'
2
3report = MemoryProfiler.report do
4Model.all.each { |record| process(record) }
5end
6report.pretty_print
1report = MemoryProfiler.report do
2Model.find_each(batch_size: 1000) { |record| process(record) }
3end
4report.pretty_print
結果
処理方法 | メモリ使用量 | 実行時間 |
---|---|---|
each | 500MB | 5分 |
find_each | 50MB | 5分 |
each は全データを一度にメモリにロードするため、find_each に比べて約10倍のメモリを消費 していました。
2. バルクインサートを活用する
ActiveRecord の insert_all を利用すると、大量データのインサートを高速化できます。
insert_all の使用例
1data = Array.new(1000) { |i| { column1: "value\#{i}", column2: "value\#{i}" } }
2Model.insert_all(data)
この方法では、データベースに対して1回のクエリでまとめて挿入できるため、パフォーマンスが大幅に向上します。
3. Parallel で並列処理を導入する
シングルスレッドで処理すると時間がかかるため、Parallel
を利用して並列処理を行うと、バッチ処理を高速化できます。
Parallel を用いた並列処理の例
1require 'parallel'
2
3records = Model.find_in_batches(batch_size: 1000)
4Parallel.each(records, in_processes: 4) do |batch|
5 batch.each { |record| process(record) }
6end
in_processes の値を調整することで、マルチプロセス並列処理が可能になります。
メモリ使用量の比較
並列処理時のメモリ使用量を ps コマンドで可視化する。
1ps -eo pid,comm,%mem --sort=-%mem | head -n 10
結果
処理方法 | メモリ使用量 | 実行時間 |
---|---|---|
シングルスレッド | 50MB | 10分 |
4プロセス並列 | 150MB | 3分 |
並列処理をすることで、処理時間が約3倍高速化 するが、メモリ消費は増加する。
4. ジョブが失敗した場合のリトライ & エラーハンドリング
ジョブが途中で失敗した場合、リトライ機能を実装して処理を継続できるようにします。
シンプルなリトライ実装
1def process_with_retry(batch_id, max_retries: 5)
2 retries = 0
3 begin
4 process_batch(batch_id)
5 rescue => e
6 retries += 1
7 if retries < max_retries
8 sleep 2 ** retries
9 retry
10 else
11 Rails.logger.error("Batch \#{batch_id} failed after \#{max_retries} retries: \#{e.message}")
12 end
13 end
14end
この方法では、エラーが発生した場合に指数バックオフ (sleep 2 ** retries
) を利用してリトライします。最大 max_retries
回まで再試行し、それでも失敗した場合はログに記録します。
まとめ
いかがだったでしょうか?
本記事では、Railsで数百万件のデータを扱う際のバッチ処理設計について解説しました。
- find_each でメモリ使用量を抑える
- バルクインサート (insert_all) で高速化
- parallel で並列処理を行い、処理時間を短縮
- シンプルなリトライ機能を実装し、エラー耐性を強化
適切な手法を組み合わせることで、Rails でも大量データを安全かつ高速に処理できます。今後の開発にぜひ役立ててください。
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ

プライベートの休日はインドアもアウトドアもどちらになることもあります。