RailsやSinatraで画像をアップロードしたり、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にかかる負荷については、かなり軽減できているようです。
もっと大きなファイルで試してみれば、もうちょっと差が開く可能性があります。