nginx-gridfsを使ってcarrierwaveで作ったサムネイルを表示する

RailsSinatraで画像をアップロードしたり、DBやAmazonS3に保存したりするためのライブラリとして、carrierwaveがあります。
あまり一般的では無いかもしれませんが、carrierwave-mongoidという拡張ライブラリを利用することで、
MongoDBのGridFSに画像を格納することが出来ます。


その場合、画像を表示する時に一番てっとり早いのは、
Rails上でGridFSに接続してバイナリを読み出し、send_dataでそのままクライアントに返す方法です。


しかし、この方法はRailsの処理を丸々通るので、画像のように細かくアクセスが多いものには不向きです。
そこで、次の手段がRackミドルウェアを使う方法です。
以下のようなミドルウェアを作成し、Rackのスタックに積んでおきます。

# config/initializer/carrierwave.rb
CarrierWave.configure do |config|
  config.grid_fs_database = Mongoid.database.name
  config.grid_fs_host = Mongoid.config.master.connection.host
  config.storage = :grid_fs
  config.grid_fs_access_url = "/gridfs"
end

# lib/serve_gridfs_image.rb
class ServeGridfsImage
  def initialize(app)
      @app = app
  end

  def call(env)
    if env["PATH_INFO"] =~ /^\/gridfs\/(.+)$/
      process_request(env, $1)
    else
      @app.call(env)
    end
  end

  private
  def process_request(env, key)
    begin
      Mongo::GridFileSystem.new(Mongoid.database).open(key, 'r') do |file|
        [200, { 'Content-Type' => file.content_type, 'Cache-Control' => "max-age=#{3600*12}" }, [file.read]]
      end
    rescue
      [404, { 'Content-Type' => 'text/plain' }, ['File not found.']]
    end
  end
end


アクセスの環境変数からパス情報を取得し、特定のパス以下の場合はGridFSから取得したバイナリを返し、
そうでなければ、続きのスタックを呼ぶというミドルウェアです。
この方法を利用することで、レスポンスは多少良くなります。


それでも大量にアクセスが来るとか、画像を大量に使っていたりすると、
rubyのプロセスにかかるCPU負荷が上がってきます。


そこで第三の手段。
nginx-gridfsを使って、nginxの層で直接画像を返してしまうことで、
CPU負荷の軽減とレスポンスの向上を図ります。


まずnginx-gridfsを組み込んだnginxを作ります。
nginx-gridfsのREADMEにも書かれていますが、以下のような手順になります。

git clone git://github.com/mdirolf/nginx-gridfs.git
cd nginx-gridfs
git submodule update --init

cd nginx-src-dir
./configure --add-module=/path/to/nginx-gridfs
make
make install


次にnginxの設定です。

location /gridfs/ {
  gridfs dbname field=filename type=string;
} 


この設定で、dbnameデータベースのfsコレクションに対して
/gridfs/XXXXのXXXX部分を文字列としてfilenameフィールドを検索し、
GridFSからデータを読み込んで返してくれます。
DB接続のための細かい設定等は、nginx-gridfsのREADMEに書かれています。


carrierwaveでは、以下のようにuploaderクラスでどういうパスでファイルを格納するか定義しています。

class ThumbnailUploader < CarrierWave::Uploader::Base

  def store_dir
    "thumbnails/#{model.class.to_s.underscore}/#{model.id}"
  end

  def default_url
     "/images/noimg.jpg"
  end

  def filename
    "image.jpg"
  end

end


この設定でgridfsをストレージに利用する場合、
filenameフィールドに#{store_dir/filename}というデータが保存されます。
initializerでの設定も加えて、実際にデータにアクセスする時には以下のようになります。

person.thumbnail.url # -> /gridfs/thumbnails/person/039132a4329a9329/image.jpg


このURLのままクライアントからアクセスすると、
nginxがthumbnails/person/039132a4329a9329/image.jpgという文字列をそのまま、
filenameの検索キーとして利用してくれるため、めでたくGridFSから画像が取得できます。

パフォーマンス

Rackミドルウェアで画像表示した場合と、
nginx-gridfsで画像表示した場合で、簡単なベンチマークを取ってみました。

nginx-gridfs (nginx ワーカー数2)
% ab -n 10000 -c 20 http://hogehoge/gridfs/thumbnails/hogemodel/4e00c8ecd30afe2056000c15/image.jpg
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking nagato (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        nginx/1.0.10
Server Hostname:        hogehoge
Server Port:            80

Document Path:          /gridfs/thumbnails/hogemodel/4e00c8ecd30afe2056000c15/image.jpg
Document Length:        5806 bytes

Concurrency Level:      20
Time taken for tests:   2.698 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      60390000 bytes
HTML transferred:       58060000 bytes
Requests per second:    3706.93 [#/sec] (mean)
Time per request:       5.395 [ms] (mean)
Time per request:       0.270 [ms] (mean, across all concurrent requests)
Transfer rate:          21861.46 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.3      0       3
Processing:     1    5   0.8      5      10
Waiting:        1    5   0.8      5      10
Total:          2    5   0.7      5      11
Rackミドルウェア(unicorn ワーカー数4)
% ab -n 10000 -c 20 http://hogehoge/gridfs/thumbnails/hogemodel/4e00c8ecd30afe2056000c15/image.jpg
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking nagato (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        nginx/1.0.10
Server Hostname:        hogehoge
Server Port:            80

Document Path:          /images/thumbnails/hogemodel/4e00c8ecd30afe2056000c15/image.jpg
Document Length:        5806 bytes

Concurrency Level:      20
Time taken for tests:   3.854 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      61860000 bytes
HTML transferred:       58060000 bytes
Requests per second:    2594.80 [#/sec] (mean)
Time per request:       7.708 [ms] (mean)
Time per request:       0.385 [ms] (mean, across all concurrent requests)
Transfer rate:          15675.24 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0       5
Processing:     1    7   8.5      6     192
Waiting:        1    7   8.5      6     192
Total:          2    8   8.5      6     192


全体的に性能が向上しているのが分かりますね。
ちょっと使った画像が小さ過ぎて、差が微妙になってしまってますが。


ベンチマーク中の負荷ですが、
Rackミドルウェアの方は、unicornの各ワーカーがCPU利用率80%を越える勢いで負荷がかかってしました。
一方で、nginx-gridfsは、nginxの各ワーカーがCPU利用率20%前後でした。
CPUにかかる負荷については、かなり軽減できているようです。


もっと大きなファイルで試してみれば、もうちょっと差が開く可能性があります。