• トップ
  • ブログ一覧
  • Railsで数百万件のデータを高速処理!バルクインサート・並列処理・リトライの最適解
  • Railsで数百万件のデータを高速処理!バルクインサート・並列処理・リトライの最適解

    はじめに

    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

    結果

    処理方法メモリ使用量実行時間
    each500MB5分
    find_each50MB5分

    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

    結果

    処理方法メモリ使用量実行時間
    シングルスレッド50MB10分
    4プロセス並列150MB3分

    並列処理をすることで、処理時間が約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 でも大量データを安全かつ高速に処理できます。今後の開発にぜひ役立ててください。

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    かねまさ(エンジニア)
    かねまさ(エンジニア)
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background