壁紙アプリの画像配信を整えていた日のことです。アプリは sunset.jpg?w=640 のように、欲しい幅を付けて画像を呼びます。サーバー側には、幅ごとに分けたフォルダがあります。この二つをつなぐ振り分けを、301 で書くか、内部リライトで書くかで手が止まりました。
どちらでも「画像は出ます」。だからこそ、選んだ理由を言葉にしないまま進めてしまいやすい箇所です。結論を先にお伝えします。同じホストの中で、呼び出し側に知らせる必要がない振り分けは、URL を変えずに済ませるのがよい、と考えています。 以下は、その判断を手元の nginx で再現し、両方を実際に動かして確かめた記録です。
何を振り分けたいのか
前提を一枚にそろえます。
検証用に、640 幅は 2,000 バイト、1280 幅は 8,000 バイト、元画像は 30,000 バイトのダミーファイルを置きました。バイト数を変えておくと、どのファイルが返ったかが一目で分かります。
301 で書いた版
最初に思いつくのは、こちらです。
# A: 301 で振り分ける
map $arg_w $res { default ""; "640" "w640"; "1280" "w1280"; }
location ~ ^/a/img/(?<f>.+)$ {
if ($res = "") { return 301 /img/$f; }
return 301 /img/$res/$f;
}
curl で見ると、こうなります。
$ curl -si "http://127.0.0.1:8081/a/img/sunset.jpg?w=1280" | sed -n '1p;/^Location/p'
HTTP/1.1 301 Moved Permanently
Location: http://127.0.0.1:8081/img/w1280/sunset.jpg
リダイレクトを追わせると、num_redirects は 1、つまり画像を一枚取るのに往復が二回です。アクセスログも、同じ画像につき二行ずつ増えます。300 回呼んだログでは、301 の行がちょうど 303 行並びました(動作確認の数回を含みます)。
もう一つ、見落としやすい点があります。HTTP の仕様(RFC 9110 の 301 の項)では、301 は既定でキャッシュしてよい応答として扱われます。ブラウザは「この URL はあちらへ移った」と覚えます。振り分けの規則を後から直しても、覚えた側にはすぐ届きません。確認をシークレットウィンドウで行うのは、そのためです。ここは仕様の記述に基づく説明で、私の環境で何日覚えるかまでは測っていません。
内部リライトで書いた版
呼び出し側の URL は変えず、サーバーの内側でだけファイルを差し替えます。if を使わず、map に答えを持たせました。
# D: map だけで振り分ける(URL は変わらない)
map $arg_w $dir { default "/img"; "640" "/img/w640"; "1280" "/img/w1280"; }
location ~ ^/d/img/ {
rewrite ^/d/img/(.+)$ $dir/$1 last;
}
location /img/ {
add_header X-Served-From $uri always; # 確認用。本番では外してかまいません
}
default "/img" が、この設計の要です。w=999 のような想定外の値も、w の指定がない呼び出しも、元画像に落ちます。404 にもならず、301 にもなりません。
手元の結果です。
$ ./verify.sh http://127.0.0.1:8081/d/img/sunset.jpg
w code redirects bytes
640 200 0 2000
1280 200 0 8000
999 200 0 30000
none 200 0 30000
往復は一回、redirects は 0、返ったバイト数は期待どおりでした。X-Served-From ヘッダーには /img/w640/sunset.jpg と出ます。呼び出し側からは URL が変わらないのに、内側ではファイルが差し替わっている、という状態です。
並べて選ぶ
同じ条件の二つを、観点ごとに並べます。
私が内部リライトを選んだ理由は、表の最後の行に尽きるのだと思います。振り分けの設計を見直すたびに、ここへ戻ってきました。解像度の振り分けは「引っ越し」ではなく、置き場所の都合です。呼び出し側に教える理由がありません。教えないのであれば、往復も覚えさせる心配も、最初から作らずに済みます。
逆に、旧ドメインから新ドメインへ移す場面や、検索エンジンに「こちらが正式な URL です」と伝えたい場面では、301 の出番です。どちらが優れているかという話ではなく、振り分けが引っ越しなのか、置き場所の都合なのかで分かれます。
並び順の罠:先に当たったものが勝つ
もう一つ、手元で確かめて腑に落ちたのが、正規表現の location の順序です。nginx は、正規表現の location を書かれた順に見て、最初に合ったものを採用します。広い規則を先に書くと、後ろの細かい規則は一度も使われません。
# 広い規則が先 → /e/img/w640/sunset.jpg でも "broad" が返る
location ~ ^/e/img/.+\.jpg$ { return 200 "broad\n"; }
location ~ ^/e/img/w640/.+\.jpg$ { return 200 "narrow\n"; }
同じ二行の順序を入れ替えて、別々のポートで動かしました。
narrow-first: narrow
broad-first: broad
nginx -t は、どちらの順序でも「syntax is ok」と返します。構文が正しいことと、意図どおりに当たることは別の話だと、あらためて気づきました。規則を足すときは、細かいものを上に置く。この一点を、手順の先頭に書いています。
手元で踏んだ二つの落とし穴
どちらも、この検証の途中で実際に踏んだものです。
一つ目は、rewrite が名前付きキャプチャを上書きすることです。 最初、location ~ ^/d/img/(?<f>.+)$ で f を取り、rewrite ^ $dir/$f last; と書きました。結果は、すべての呼び出しで 404 でした。rewrite 自身の正規表現が新しく合致するので、location で取った $f が使えなくなっていたのです。直しは、rewrite の側でキャプチャを取り直すことでした。
# NG: rewrite の正規表現(^)が $f を上書きし、パスが空になる
location ~ ^/d/img/(?<f>.+)$ { rewrite ^ $dir/$f last; }
# OK: rewrite の中で取り直す
location ~ ^/d/img/ { rewrite ^/d/img/(.+)$ $dir/$1 last; }
二つ目は、前の nginx が動いたままだったことです。 設定を書き換えて nginx -s stop を打ったのに、別の設定ファイルで起動していたため止まっておらず、新しい設定は同じポートに bind できないまま、古い設定が答え続けていました。エラーログに bind() ... failed (98: Address already in use) と出ていて、ようやく気づきました。設定を変えたのに挙動が変わらないときは、規則を疑う前に、動いているプロセスと設定ファイルの組み合わせを確かめてください。
確認用スクリプト
最後に、冒頭の表を機械的に確かめるスクリプトです。リダイレクトを追わない設定にしてあるので、301 が混じっていれば一目で分かります。
#!/usr/bin/env bash
# 使い方: ./verify.sh http://127.0.0.1:8081/d/img/sunset.jpg
URL="$1"
printf '%-8s %-5s %-10s %-8s\n' w code redirects bytes
for w in 640 1280 999 ""; do
curl -s -o /dev/null -w "%{http_code} %{num_redirects} %{size_download}" "${URL}?w=${w}" \
| { read -r code redirects bytes; printf '%-8s %-5s %-10s %-8s\n' "${w:-none}" "$code" "$redirects" "$bytes"; }
done
301 版(A)に向けて動かすと、4 行すべてが 301・178 バイトで返ります。内部リライト版(D)なら、先の表のとおり 200 と各ファイルのバイト数が並びます。同じ規則を複数の置き場所(複数のサーバーや、ストレージの別系統)に持っている場合は、このスクリプトを置き場所の数だけ回してください。一か所を直したら、同じ規則のすべてに同じ直しを入れます。これを手順に入れておくと、「片方だけ古い」という静かな食い違いを避けられます。
迷ったときの判断の順番
私は、振り分けを書く前に次の三つを順に確かめています。
- その URL は、呼び出し側が覚えておくべき「住所」でしょうか。恒久的に移るなら 301、置き場所の都合なら内部リライトです。
- 振り分け先は、同じホストの中でしょうか。別ホストへ送る場合は、リダイレクトにするほかありません。
- 想定外の入力が来たとき、どこへ落とすのでしょうか。404 にするか、元画像に落とすか。壁紙のように「何か出ていればよい」配信では、元画像への落とし先を
default に書いておくことを推奨します。
往復を数えて見積もる
一覧画面で 40 枚のサムネイルを並べる場合、301 版は 80 回、内部リライト版は 40 回の要求になります。往復で 50% の削減です。端末が回線に載せる回数が減るので、スクロールの途中で画像が出そろうまでの待ちにも効きます。ここは算数の見積もりで、実機の秒数を測ったものではありません。
本番に出す前に、先に決めておくこと
本番で規則を直すとき、手元で戻せる形を先に用意しておきます。変更前の設定ファイルを日付つきで残し、戻すコマンドを手順に書いておく。内部リライトは、直したらすぐ反映される代わりに、間違いもすぐ反映されるためです。
公開の前に確かめること
- 想定外の
w の値と、w なしの呼び出しで、元画像が 200 で返ること
- 規則の並びが「細かいものが上」になっていること(
nginx -t では分かりません)
rewrite の中で、必要なキャプチャを取り直していること
- 設定を変えたあと、動いているプロセスが新しい設定で起動し直されていること
- 同じ規則を持つ置き場所すべてに、同じ直しを入れたこと
画像配信まわりの別の落とし穴は、expo-image のディスクキャッシュが膨らむ原因と、先読みとスクロール性能の設計にも書き残しています。サーバー側の振り分けとアプリ側のキャッシュは、同じ画像の両端にあたるので、あわせて見ていただけると幸いです。