Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

nginxのX-Accel-Redirectを使った縮小画像配信サーバ

インフラまわりを担当しております。id:rmanzokuです。

今回は、画像配信サーバをnginxを使ってプチリプレースをしたので その実装方法を紹介します。

課題と対応

Studyplusでは、ユーザーが投稿した画像や教科書の表紙画像を任意のサイズに縮小し配信する機能があります。

この機能はリリース初期から存在し、Javaで実装されていました。 退職済みメンバーの個人リポジトリのライブラリに依存していることもあり、メンテナンスコストが非常に高くなっていました。

(皆さんもそういう経験ありますよね?)

リリース初期では、複数の機能を搭載したJavaアプリケーションでしたが現在では

  • 画像管理テーブルからリクエストされたIDに対応する画像ファイルのURLを取得する
  • そのファイルをクエリ文字列で指定した任意のサイズに縮小しユーザーへ返答する

という非常にシンプルな機能しか残っていませんでした。

今回、この画像配信機能のメンテナンスコストを下げるためリプレースを実施しました。

実装方針

前述の課題に対応するため、3パートに分けて進めました。

  1. 画像IDからDBアクセスし、画像ファイルのURLを取得するアプリケーションサーバ
  2. nginxによるX-Accel-Redirectを使った画像の取得
  3. nginxによるcubicdaiya/ngx_small_lightを使った画像リサイズ

Go言語の採用

画像IDからDBアクセスし、画像ファイルのURLを取得するアプリケーションにはGo言語を採用しました。

Go言語は豊富な標準ライブラリを備えており、今回の要件ではMySQLドライバ程度の外部ライブラリで実装できます。

実際に、100行以下の1ファイルで実装できました。 コードも見通しよくメンテしやすいアプリケーションになったと感じています。

X-Accel-Redirectとは

X-Accel-Redirectとは、nginxの内部リダイレクトを実行させるためのトリガーとなるヘッダです。

アプリケーションサーバから静的ファイルを返答する場合、ロジックのないファイルの返答のためにアプリケーションの処理が専有されてしまいます。 これを避けるために、アプリケーションサーバはX-Accel-Redirectにファイル名などを返しnginxがリダイレクトすることでアプリケーションの負荷を下げることができます。

この機能は、Rails(Rack)にも利用されており、Apache HTTP ServerではX-Sendfileヘッダとして知られています。

要件次第では、静的ファイルへのアクセス方法が一意に決まらず、DBアクセスや認証を必要とする場合があります。 アプリケーションサーバで処理をした結果、X-Accel-Redirectを有効活用することでアプリケーションとnginxの役割分担をすることが可能になります。

と、説明しましたが、nginx上の別のlocationにリダイレクトするためにも利用できます。 今回の実装では、X-Accel-Redirectは画像縮小のためのlocation /image_redirect/へのリダイレクトに利用し、X-Imagefileという独自ヘッダに画像URLを入れています。

具体的なフロー図

画像IDを1111、縮小後サイズを100x100としたときのフロー図です。

f:id:rmanzoku:20180628105537p:plain

画像ファイルURL取得サーバ

Go言語で実装した画像ファイルURLを取得するアプリケーションサーバです。

  1. URL.pathから画像IDを取得する
  2. 画像IDを使って画像管理テーブルから画像ファイルURLを取得する
  3. X-Accel-Redirectに内部リダイレクト先とクエリ文字列を入れる
  4. X-Imagefileに画像ファイルURLを入れる

旧実装では、画像の取得から圧縮まで行っていましたが、 本実装ではヘッダに値を入れているだけで、画像ファイルを扱う必要はありません。

以下にエラーやHTTP部分を除いて一部抜粋したコードを示します。

var db, _ = sql.Open("mysql", dsn)

func handler(w http.ResponseWriter, r *http.Request) {
    uri := r.URL.String()
    path := r.URL.Path

    var id int
    var accelRedirect string
    var imageFile string
    var err error

        // リクエストパスからIDを取得
        id, _ = strconv.Atoi(path[1:])

        // IDから画像ファイルURLを取得
    _ = db.QueryRow("SELECT filename FROM image_entries where id=?", id).Scan(&imageFile)

        // X-Accel-Redirectに内部リダイレクト先とクエリ文字列を入れる
    accelRedirect = "/image_redirect/?" + r.URL.RawQuery
    w.Header().Set("X-Accel-Redirect", accelRedirect)

        // X-Imagefileに画像ファイルURLを入れる
    w.Header().Set("X-Imagefile", imageFile)
}

nginx設定例

nginxでは、画像ファイルURLから渡されるヘッダに基づいてリダイレクトと画像縮小ができるように設定します。

次にserverディレクティブの設定例を示します。

server {
    listen 80;

    # クエリ文字列 `?w=100&h=100` は保持されたまま内部リダイレクトされる
    location /image_redirect/ {
        internal;

        set $path_to $upstream_http_x_imagefile;
        proxy_hide_header upstream_http_x_imagefile;
        proxy_pass $path_to;

        small_light on;
        small_light_getparam_mode on;
    }

    location / {
        proxy_pass http://ImageFetchGo;
    }
}

アプリケーションサーバImageFetchGoから ヘッダX-Accel-Redirect: /image_redirect/?w=100&h=100と返すことで、クエリ文字列を保持したまま内部リダイレクトが可能です。 また、$upstream_http_x_imagefileからヘッダX-Imagefile: https://image-storage/hogehoge.jpgの画像ファイルURLが取得できるため、proxy_passすることで画像を取得できます。

取得した画像はcubicdaiya/ngx_small_lightを利用してクエリ文字列に応じたサイズへ変換されます。 縮小された画像は、無事ユーザーへ届けられます。

まとめ

nginxのX-Accel-Redirectを利用した画像配信サーバのリプレースについて紹介しました。 普段馴染みのないヘッダですが、各機能の実装を最小限にし組み合わせることで、強力な力を発揮できます。

スタディプラスでは、ミドルウェアを活用してアプリケーション全体を効率化できるインフラエンジニアを募集しております! この記事に興味を持ってもらえるならぜひお話だけでも遊びにきてもらえればとおもいます。