そろそろISUCONということで、先日チームであつまって練習をしたところnginxでドハマリしたので復習した内容をまとめます。
やりたかったこと
ISUCONはほとんどがフロントにWEBサーバ(nginx)があり、裏にgunicorn等のAPサーバを配置した構成が多いようです。デフォルトはすべてのリクエストがAPサーバに流れているので一部の静的なページをnginxに任せたかったわけです。
その時の条件は以下でした。
- /へのアクセスはnginxに静的ファルを戻させる
- /xxx へのアクセス等、/以外はすべてgunicornにに引き渡す
- /?error=xの場合はxの値に応じた静的ファイルをnginxに戻させる
ネックになったのはgunicornに引き渡すルールが/appみたいになっておらず同じ/でも適用するディレクィブをわけなければいけなかった点です。
nginxの内部リダイレクト
nginxではindexディレクティブ等を使った場合、内部リダイレクトが発生します。以下を例とします。
# ディレクティブ1
location = / {
add_header Content-type text/html;
default_type text/html;
index index.html;
root /tmp/test/;
}
# ディレクティブ2
location / {
proxy_pass http://app;
}
気持ち的には、ディレクティブ1が適用され、/tmp/test/index.htmlが戻されてほしいですが、実際は以下の流れになります。
- ディレクティブ1が適用される
- indexディレクティブにより/index.htmlへアクセスが書き換えられる
- 内部リダイレクトが発生し、/index.htmlとして再評価される
- ディレクティブ1にはマッチしないのでディレクティブ2にマッチし、APサーバに流れる
なお、/tmp/test/index.htmlが実在しない場合は403が戻りますのでさらに混乱します。
root@denzow-ubuntu:/etc/nginx# ls -l /tmp/test/index.html -rwxr-xr-x 1 root root 23 10月 4 22:59 /tmp/test/index.html root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/ i am bottle <br/> index.html -> APサーバが応答している
root@denzow-ubuntu:/etc/nginx# rm /tmp/test/index.html root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/ <html> <head><title>403 Forbidden</title></head> <body bgcolor="white"> <center><h1>403 Forbidden</h1></center> <hr><center>nginx/1.10.3 (Ubuntu)</center> </body> </html> -> ファイルを削除すると403になる
結局、ファイルがあるかどうかは最初のlocationディレクティブでチェックし、そこで403が戻っているようです。
nginxでのクエリパラメータによる分岐
nginxではhoge?a=1&b=2のようなクエリパラメータの場合は$arg_aや$arg_bとして取得することができます。これにifディレクティブを組み合わせると以下のようになります。
if ( $arg_error = "1" ) {
#任意のアクション
}
if ( $arg_error = "2" ) {
#任意のアクション
}
なお、if内ではalias 等使えないディレクティブもあるようです。
結局どうしたか
rewriteディレクティブを使うことにしました。このディレクティブはアクセスされたURLを書き換えるものですが、更にlocationの再評価をさせるかどうかを指定することが可能です。
rewrite ^(.*)$ /error1.html break;
このように末尾にbreakをつけ場合はlocationの再評価をしないため、ここでアクセス先が確定します。これを利用し以下の様に設定しました。
:
upstream app {
server localhost:8080;
}
server {
location = / {
add_header Content-type text/html;
default_type text/html;
root /tmp/test/;
if ( $arg_error = "1" ) {
rewrite ^(.*)$ /error1.html break;
}
if ( $arg_error = "2" ) {
rewrite ^(.*)$ /error2.html break;
}
rewrite ^(.*)$ /index1.html break;
}
location / {
proxy_pass http://app;
}
}
:
これで、以下のような動きになります。
| URL | アクセス先 |
|---|---|
| / | /tmp/test/index1.html |
| /?error=1 | /tmp/test/error1.html |
| /?error=2 | /tmp/test/error2.html |
| /hoge | apサーバ宛 |
| /foo | apサーバ宛 |
確認してみましょう。
root@denzow-ubuntu:/etc/nginx# ls -l /tmp/test/ 合計 12 -rwxrwxrwx 1 root root 24 10月 4 22:44 error1.html -rwxr-xr-x 1 root root 24 10月 4 22:44 error2.html -rwxrwxrwx 1 root root 24 10月 4 23:11 index1.html root@denzow-ubuntu:/etc/nginx# grep . /tmp/test/* /tmp/test/error1.html:static file error1.html /tmp/test/error2.html:static file error2.html /tmp/test/index1.html:static file index1.html
root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/ static file index1.html root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/?error=1 static file error1.html root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/?error=2 static file error2.html root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/hoge i am bottle <br/> hoge root@denzow-ubuntu:/etc/nginx# curl http://192.168.56.100/foo i am bottle <br/> foo
ちゃんと思った通りの動きになっていますね。
なお、nginxのifディレクティブは実運用では推奨されていないようです。あくまでISUCON等の限られた場で生きる知識として理解したいと思います。