patorashのブログ

方向性はまだない

並列処理でActiveRecordの処理時間を短縮する

ここ最近は並列化による処理速度アップを色々試しています。 Railsプロジェクトのデータに少々不備があることに気づいたので、それを修正するために該当データを抽出しようと思って雑にループを回したら、データ量が多いせいか、全然終了しませんでした。業を煮やした私は、これも並列化してしまおう!と思って並列化の情報を集めることに。

Rubyでの並列処理は、parallelというgemを使うと並列処理がすごく簡単にできました。

github.com

parallelのいいところ

parallelのいいところは、

  • 並列処理したい対象の配列データを渡すだけでいい
  • map, each, any?, all? などに対応している
  • マルチプロセス、マルチスレッドの両方に対応している

というところでしょうか。

簡単な使い方の例

Parallelに対して、配列を渡したら、自動的にCPUの数だけプロセスをフォークして処理してくれます。並列数を指定することもできます。

require 'parallel'

# デフォルトでCPUの数だけプロセスが立ち上がる
Parallel.each(['a', 'b', 'c']) do |one_letter|
  expensive_calculation(one_letter)
end

# 3プロセスで処理
Parallel.each(['a', 'b', 'c'], in_processes: 3) do |one_letter|
  expensive_calculation(one_letter)
end

# 3スレッドで処理
Parallel.each(['a', 'b', 'c'], in_threads: 3) do |one_letter|
  expensive_calculation(one_letter)
end

ActiveRecordのデータを並列で処理する

ActiveRecordのデータを並列で処理する場合、フォークしたプロセスの数だけデータベースに接続する必要があるので、reconnectを使って再接続命令を行います。

Parallel.each(User.all, in_processes: 8) do |user|
  @reconnected ||= User.connection.reconnect! || true
  user.update_attribute(:some_attribute, some_value)
end

もっとセンスよく並列化したい

データがそんなに多くなければ、上記のようにParallelにUser.allみたいに雑にやってもいいとは思うのですが、もしUserに大量のデータがあった場合、メモリ消費量が多くなり、重くなると思います。さすがにこれはやばいだろう…と思って調べていたら、すごくセンスのある並列処理のサンプルコードが載ってる記事を見つけました。

ainame.hateblo.jp

find_in_batchesを使うところにすごく感動しました。

User.find_in_batches do |users|
  Parallel.each(users) do |user|
    @reconnected ||= User.connection.reconnect! || true
    user.update_attribute(:some_attribute, some_value)
  end
  User.connection.reconnect!
end

これならば、デフォルトで1,000件ずつ取得したデータを並列で処理して、次の1,000件へ…のようにできるので、メモリ使用量も少なくて済みます。

この形式に変更してから処理を行なったところ、20分程度で結果が返ってきたので並列化最高!!という気持ちになりました。

気づき

実はfind_eachはよく使っていたのですが、find_in_batchesは使ったことがありませんでした。多分、初めて使ったと思います。Parallelとの相性がいいから、他のプロジェクトなどでも高速化を狙えるところがあったら積極的に使っていこうと思います。