13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ActiveJobで実装したデータ取り込みジョブを2.2倍高速化した

Posted at

PubAnnotationという文章への注釈を管理するWebアプリケーションがあります。
このアプリケーションには注釈のついた文章データを一括アップロードする機能があります。
7万文章をアップロードすると7時間掛かります。この処理を高速化するために工夫しました。

PubAnnotationはRuby on Railsで実装されたWebアプリケーションです。
一括アップロード機能は、ActiveJobで実装され、Sidekiqで実行されているバッチ処理です。

結果

最初に結果を示します。
文章をアップロードしたときの平均アップロード数です。
文章によってデータ量がちがいます。2000~2500程度の文章をアップロードして平均を取りました。

image.png

縦軸が1秒当たりのアップロード文章数です。横軸はアップロードした文章数です。
次の三つの修正をしました

  1. キャッシュの導入 (x1.14)
  2. INSERTクエリの戻り値でIDを取得 (x1.48)
  3. activerecord-importからActiveRecord#insert_allへの置き換え (x1.16)

全部合わせて2.2倍の高速化を実現しました。

高速化のための修正

大量データのアップロード処理なので、最初、INSERTクエリに時間が掛かっていると予想しました。

キャッシュの導入

実際に時間が掛かっている処理を特定するためにStackprofを使います。
具体的には次のような計測用のスクリプトを作って計測します。

file = './PMC-7646410.json'
annotations_collection = JSON.parse File.read(file)
annotations_collection.each { _1.deep_symbolize_keys! }
project = Project.find_by(id: 1)

StackProf.run(out: 'tmp/stackprof.dump', raw: true) do
  InstantiateAndSaveAnnotationsCollection.call project, annotations_collection
end

InstantiateAndSaveAnnotationsCollection.callが計測対象の処理です。
最初から、このように計測しやすかったわけではありません。
計測したいところを、事前のリファクタリングで切り出しています。

stackprof gemを追加します。
InstantiateAndSaveAnnotationsCollection.callはActiveRecordに依存しています。
rails runner経由で実行します。

bundle add stackprof
bundle
bin/rails runner benchmark_scripts/profile_stack_instantiate_and_save_annotations_collection.rb

stackprofには取得したプロファイルをグラフ化してくれる機能があります。

stackprof --d3-flamegraph tmp/stackprof_store_annotations_collection.dump > framegraph.html

作成されたhtmlファイルをブラウザで表示すると、どの関数に時間が掛かっているか図示されます。

image.png

カラムをホーバーするとツールチップで該当の関数と定義されている場所が表示されます。
幅の広いカラムをホバーして、ボトルネックを探していきます。
上の図はアプリケーションでボトルネックになっていそうなところをホバーしたものです。
ツールチップに表示されているのはDenotation.find_by_doc_id_and_project_id_and_hidです。
INSERTクエリより検索に時間が掛かっていそうです。

注釈には次の三種類があります

  • Denotation
  • Attribute
  • Relation

AttributeとRelationはDenotaitonを参照する注釈です。つまりAttributeとRelationをDBに保存するまえに、参照先のDenotaitonのidを取得する必要があります。この検索に時間が掛かっています。

そこでAttributeとRelationを保存する度に、Denotaiton.find_byするをやめ、事前にまとめて検索しメモリ上にキャッシュします。

この修正で1.14倍の高速化ができました。

INSERTクエリの戻り値でIDを取得

一つ前の修正で検索に時間が掛かっていることが本当であるとわかりました。
そこでもうちょっと大きな変更を加えて、さらなる高速化をはかります。
PostgreSQLではINSERTクエリの戻り値から、発行したIDを取得できます。
activerecord-importもこの機能をサポートしています。

Additionally, for users of Postgres, there will be two arrays ids and results that can be accessed.

これを使うと、INSERTクエリの戻り値で発行したIDがわかります。Denotaitonの検索そのものを無くせます。

前述のStackProfで取得した図の該当カラムを丸っと削ったことになります。
これは効果が大きく、1.48倍の高速化ができました。

activerecord-importからActiveRecord#insert_allへの置き換え

activerecord-importを使っていると、疑問におもうことがあります。
activerecord-importとActiveRecord#insert_allのどちらが速いかです。

ActiveRecord#insert_allはRails 6から導入された、Railsの標準機能です。
一方、activerecord-importは実績のあるgemです。
どちらも複数のインスタンスやハッシュから一文のINSERTクエリを作成します。SQLの実行を一回にすることで、複数レコードのINSERTを高速化します。

ほぼ同じ機能ですし、実装も大きくは違わないので、そんなに差はでない気もしますが、気にはなります。計測してみます。
今回はbenchmark gemを使って実際の処理時間を計測します。
次のようなスクリプトを作成し、InstantiateAndSaveAnnotationsCollection.call内部の実装を変えて計測します。

require 'benchmark'

count = 3
annotations_collection = JSON.parse File.read(ARGV[0])
annotations_collection.each { _1.deep_symbolize_keys! }
project = Project.find_by(id: 1)

Benchmark.bm do
  _1.report("#{count}") do
    count.times do
      InstantiateAndSaveAnnotationsCollection.call project, annotations_collection
    end
  end
end

activerecord-import ハッシュ

►bin/rails runner ./bench_instantiate_and_save_annotations_collection.rb ./PMC-7646410.json
       user     system      total        real
3  0.816089   0.023025   0.839114 (  1.223663)

activerecord-import columns and array

►bin/rails runner ./bench_instantiate_and_save_annotations_collection.rb ./PMC-7646410.json
       user     system      total        real
3  0.791784   0.031821   0.823605 (  1.109341)

ActiveRecord#insert_all

►bin/rails runner ./bench_instantiate_and_save_annotations_collection.rb ./PMC-7646410.json
       user     system      total        real
3  0.719464   0.023214   0.742678 (  1.030884)

activerecord-importにはレコードを配列で指定する方法もあります。そちらも測定しています。

意外なことにActiveRecord#insert_allが明らかに速かったです。
これを採用して1.14倍高速化出来ました。

ただ、常にActiveRecord#insert_allが速いわけではないようです。
activerecord-importを速くつかう - @ledsun blogではactiverecord-importの方が速かったです。
生成するSQLクエリに依存するのでしょうか?アプリケーション毎にどちらが速いか計測する必要がありそうです。

13
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?