Help us understand the problem. What is going on with this article?

5日かかる計算だった100万近くある画像のアップロードを3時間弱にした話

More than 3 years have passed since last update.

この記事はなに?

ベーシック Advent Calendar 2016 20日目です。

機会があって100万枚近い画像を3時間弱でアップロードできるアップローダを作成しました。今回はアップローダを作成する際の手法をご紹介したいと思います!

そもそも何故やることになったのか

僕の携わっているサービスが全面リニューアルをおこなっているのですが、要件の中に、MongoDBからMySQLに乗り換えるというものがありました。その際に大量にある画像を下記理由から再アップロードする必要が出てきました。

  • MongoDBからMySQLへの変更に伴い、データの持ち方が大きく変わったので、画像パスを再登録する必要があった。
  • 画像を保存しているディレクトリ構造がいけてなく、S3上での検索が困難だったので整えたかった。

また、画像のアップロード作業をする際は、サービスを一時停止、また万が一の為にやり直しをするバッファが欲しかったので、あまり時間を掛ける事が出来ませんでした。なので今回は3時間以内を目標にアップローダの開発を行ないました。

前提

単位

今回出てくる単位としてユーザ、マテリアル、画像という単位があります。
ユーザに対してマテリアルが複数紐付き、マテリアルに対して画像が3枚紐付いています。
image3.png

画像のサイズ

1枚約2MB程度を想定しています。

環境

環境 バージョン スペック
EC2 c4.xlarge
S3
Ruby 2.3.1
Rails 5.0.0.1
capistrano 3.4.1
sidekiq 4.1.2
ElastiCache(Redis) 3.2.4 cache.m3.medium
RDS(MySQL) 5.6.27 db.m3.large
MongoDB 3.0.8
CarrierWave 0.10.0

今回は上記環境をベースにアップローダを作成しました。

基本の構成

まずは単純な下記構成で画像のアップロードを試みました。
image1.png

処理の流れ

バッチをスタートさせ、MongoDBに保存してあるパスを元にバケットAから画像を取得します。
※ S3に保存してる画像を取得するとファイル名がかわってしまうので、一度ローカルに保存してからリネーム、再取得をするメソッドを作成しました。(ここはもって良い方法がありそう…)

S3から取得した画像を一旦保存してリネームするメソッド
def open_image
  # バケットAのDOMAINを設定
  DOMAIN = 'https://baqet-a.s3.amazonaws.com/'
  DOMAIN.freeze

  # S3に上がっている画像名を取得
  image_name = File.basename("#{DOMAIN}hogehoge")
  # S3から画像を取得(ファイル名が変わってしまっている)
  open_image = open(image_path)
  new_image_path = "/tmp/#{image_name}"
  # 保存した画像をリネーム
  File.rename(open_image, new_image_path)
  # リネームした画像を再取得
  open(new_image_path)
end

あとは必要なモデルに上記で作成したファイルを保存すれば自動的にCarrierWaveがS3に画像をアップロードしてくれます!(簡単・便利!)
CarrierWaveの詳しい説明に関しては下記が非常に参考になりました!有難うございます!
Carrierwave + Rails 4.2.5 画像アップローダー

結果

ユーザ100人
マテリアル数 455
処理時間: 10分

う、めっちゃ遅い…計算してみると
(1000000枚 / (455 * 3枚) * 10分) / 60分 = 122時間
このままだと5日以上かかってしまいます…

とてもじゃないですが、5日も待てません。そこで、アップロード処理を非同期にすることで、高速化を図りました。

sidekiqを使った非同期処理

上記のままだと余りにも時間がかかってしまうので、次にsidekiqを使った非同期処理を試しました。
sidekiqに関しては下記URLに詳しく書かれています。
sidekiqの使い方

今回はスレッド数を下記のように20にし、ユーザ単位でジョブを生成するようにしました。

config/sidekiq.yml
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
:concurrency: 20 #スレッド数
:queues:
  - [default, 5]

問題① MySQL接続時にエラー発生

sidekiqを使って、非同期処理にしたところ下記のようなエラーがでました。

error_log
ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it

どうやら、コネクション出来る最大数が決まっていたようです。
詳しくは下記サイトに書いてありました!有難うございます!
ActiveRecordのconnection_poolで怒られる

今回はsidekiqのスレッド数を20にしたのでpool数は少し多めの30に設定しました。

database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: 30
  socket: /var/lib/mysql/mysql.sock

これで上記のエラーは出なくなりました。

問題② MongoDB接続時にエラー発生

こちらも同様に接続エラーが発生しました。
どうやらスロークエリがあったのが原因だったので、indexを張り高速化することでエラーを解消しました。
これでsidekiqを使った非同期処理の準備が整いました!

結果

ユーザ100人
マテリアル数 455
処理時間: 4分

sidekiqを入れたことで10分から4分に短縮することが出来ました。
(1000000枚 / (455 * 3枚) *4分) / 60分 = 49時間

約2日まで縮める事が出来ました。
ですが、まだまだ遅い!!
そこでsidekiq専用サーバを別に作成して、より非同期処理できる環境を用意することにしました。

sidekiq専用サーバを建てる

次にsidekiq専用サーバを複数建てて、より多くのタスクを非同期で捌けるようにしました。
複数のsidekiqを一つのRedisを監視する構成にすれば、よしなに各sidekiqサーバがタスクを分担してくれたので、ここは特に難しいところもなく、環境を作ることが出来ました。
下記サイトが非常に参考になりました!(有難うございます!)
Resque、SidekiqからSucker PunchまでActiveJobのバックエンドについてひと通り調べてみた

構成

image2.png

sidekiqサーバを効率良く増やすために

sidekiqサーバを1台づつ作ると非常に時間がかかってしまうので、まずは1台Capstranoでデプロイ出来るまでの環境を作成しました。
あとは、AWSの管理画面からそのサーバのAMIを作成して、ポチポチサーバを複製します。
サーバを複製したら、capistarnoの設定ファイルに先ほど複製したサーバを追加してデプロイすれば準備完了です。

結果

ユーザ1000人
マテリアル数 15800
処理時間: 30分
うーんかなり高速化しましたが
(1000000枚 / (15800 * 3枚) * 26分) / 60分 = 9.1時間
まだ半日近くかかってしまっています…

原因・対策

ユーザ単位でジョブを投げていたので大量のマテリアルが紐付いているユーザを担当することになったsidekiqサーバに処理が集中してしまい、うまく処理を分散することが出来ていませんでした。なので、ユーザ単位ではなくマテリアル単位でジョブを作成する仕様に変更しました。

ジョブの粒度をユーザからマテリアル単位に変更

ジョブの粒度をユーザからマテリアル単位に変更しました。その際に先ほど設定したMySQLのpool数が足りなくなるので、pool数を250に増やしました。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: 250
  socket: /var/lib/mysql/mysql.sock

結果

ユーザ1000人
マテリアル数 15800
処理時間: 8分
粒度をユーザからマテリアル単位に変えたことで3倍ほど高速化しました!
これを本番を想定した画像枚数にしてみると
((1000000枚 / 15800 * 3枚) * 8分) / 60分 = 2.7時間!!
これで100万枚近い画像を3時間弱でアップロードすることが出来るようになりました!!

まとめ

  • sidekiqを使って非同期処理にする。その際はDBのコネクションエラーに注意する。
  • それでも間に合わない場合はsidekiqサーバを複数台用意して更に処理を分散してあげる。
  • 効率的に非同期処理をさせるために、なるべく粒度は細かい単位でジョブを作る。

終わりに

無事に1週間かかる処理を3時間に短縮することが出来ました!更に処理を短縮したい場合は、sidekiqのスレッド数・サーバ台数増やせばまだまだ高速化出来そうです!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away