ここ最近は並列化による処理速度アップを色々試しています。 Railsプロジェクトのデータに少々不備があることに気づいたので、それを修正するために該当データを抽出しようと思って雑にループを回したら、データ量が多いせいか、全然終了しませんでした。業を煮やした私は、これも並列化してしまおう!と思って並列化の情報を集めることに。
Rubyでの並列処理は、parallelというgemを使うと並列処理がすごく簡単にできました。
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に大量のデータがあった場合、メモリ消費量が多くなり、重くなると思います。さすがにこれはやばいだろう…と思って調べていたら、すごくセンスのある並列処理のサンプルコードが載ってる記事を見つけました。
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との相性がいいから、他のプロジェクトなどでも高速化を狙えるところがあったら積極的に使っていこうと思います。