Rails + unicorn で nginx-upload-moduleを使ってみた


この記事は、nginx Advent Calendar 2015の5日目の記事です。

nginx-upload-module

nginxでは、クライアントから multipart/form-data でファイルのアップロードを始めると /tmp 配下に一時ファイルを作ります。nginxは、クライアントがファイルをアップロードし終わると、ミドルウェアに一時ファイルからreadして受け渡します。
この仕組みだと、サーバ内で同じファイルを何度もコピーし直すこととなり、数MBのファイルでは問題ありませんが、GB単位のファイルになるとクライアントがタイムアウトしたり、ディスクの容量が足りなくなったりと問題が生じてきます。

これを解決するのが nginx-upload-moduleです。nginx-upload-module はnginxでファイルを取得し、そのパスやファイル名などをミドルウェアに渡します。ファイルそのものではなくパスやファイル名のみを渡すので、サーバ内でのファイルの受け渡しがなくなり、前述のような問題が起きません。

この nginx-upload-module ですが、2008年から更新は止まっていて、現在の nginx で使えるかどうかは書いてありませんが、今回は nginx-1.9.6 で試してみました。
https://github.com/vkholodkov/nginx-upload-module

今回やっていること&目的は以下のブログに書いてあることとほぼ同じですので、基本ここを参考にしていきます。この記事では、この設定ではまったことなどを書いていきます。

Railsで大きなファイルを扱う際のポイント
http://techracho.bpsinc.jp/baba/2014_10_08/19139

環境

  • CentOS 6.7
  • nginx 1.9.6
  • nginx-upload-module 2.2.0

ダウンロードとコンパイル

必要なソースコード等をダウンロードし、--add-module オプションを付けてnginxをコンパイルします。

$ git clone -b release-1.9.6 https://github.com/nginx/nginx.git
$ git clone https://github.com/vkholodkov/nginx-upload-module.git'
$ cd nginx
$ auto/configure (略) --add-module=../nginx-upload-module"
$ make -j4
$ sudo make install

設定

今回は、以下のような設定をしました。

    location / {
      try_files $uri @application;

      if ($args ~ ngx_upload_module=on ) {
        upload_pass @application;
        upload_pass_form_field ".*";
        upload_store /tmp;
        upload_store_access user:rw group:rw all:rw;
        upload_resumable on;
        upload_set_form_field "$upload_field_name[filename]"   "$upload_file_name";
        upload_set_form_field "$upload_field_name[type]"       "$upload_content_type";
        upload_set_form_field "$upload_field_name[tempfile]"   "$upload_tmp_path";
        upload_aggregate_form_field "$upload_field_name[md5]"  "$upload_file_md5";
        upload_aggregate_form_field "$upload_field_name[size]" "$upload_file_size";
        upload_cleanup 200-599;
      }
    }

どのようなオプションが設定できるかは、参考の記事や 公式のドキュメント を見ると良いです。

ここでは、特に注意したいところについて書きます。

引数でモジュールの使用をスイッチする

参考の記事や多くの情報では、nginxの設定に nginx-upload-module を使いたいパスのlocationを書けというのが多いですが、これだと汎用性が低く、他のリソースにも適用したい時に不便です。

なので、if文を用いてURLにngx_upload_module 引数が付いている場合のみ、nginx-upload-moduleを使用します。これにより、新しく対応したいリソースが増えた場合にもnginxの設定を触る必要はありません。

この場合、Rails側のビューファイルも変更してあげる必要があります。
form_forを利用している場合は、urlオプションを使って、パラメーターを渡してあげます。

= form_for @upload, url: uploads_path(ngx_upload_module: 'on') do |f|

自動的にファイルを削除する

今回作ったアプリケーションでは、サーバが受け取ったあとはGoogle Cloud Storageにアップロードされるため、サーバ内に保存しておく必要がありません。

nginx-upload-moduleはそのまま使用するとファイルを残してしまうので、あっという間にディスクがいっぱいになってしまいました。
最初は試行錯誤していたのですが、upload_cleanupというオプションがあり、ステータスコードを指定することでそのステータスコードの場合のみ削除できるようです。
本来は、エラー時などに消す目的のようですが、今回はアップロードファイルは全て削除したいので、以下のように 200-599 と指定して常に削除するようにします。

upload_cleanup 200-599;

コントローラー

ほぼ参考記事と同じで、リファクタリングをした程度なので割愛します
ちなみに、開発環境やnginx-upload-moduleを使っていない場合でも動作するようになっています

  def upload_params
    params.require(:uploads).permit(:name).merge(file: upload_file, upload_filename: upload_filename)
  end

  def upload_file
    file = params[:uploads][:file]
    if file.is_a?(Hash) && file[:tempfile]
      file[:tempfile]
    else
      file.path
    end
  end

  def upload_filename
    file = params[:uploads][:file]
    if file.is_a?(Hash) && file[:filename]
      file[:filename]
    else
      file.original_filename
    end
  end

まとめ

nginx-upload-moduleを用いて、アップロードファイルを効率良く処理する方法でした。
正直、nginx-upload-moduleがどれだけメンテされてるかわからないですし、GB単位の大きいファイルをアップロードさせる必要がある場合のみ対処すれば良いと思います。

今回はアップロードの話でしたが、ダウンロードの場合は、X-SendfileX-Accel-Redirectヘッダーを用いるとRailsで認証を行い、nginxがファイルを送るといったことも可能です。