Nginx を使う時に、設定に対して動作が意図したとおりにならないことがよくあります。 おそらく初見殺しで何度もハマる人が多いのでここであるあるをまとめておこうと思います。 OpenResty の話も混ざっていますがほぼ同じと考えて良いです。 ではさっそく、 Nginx あるある言いたい〜〜〜
- location の path マッチングの優先順位がわからない
- Nginx のビルド時のパラメータを後から確認したい
- worker_processes と worker_rlimit_nofile と worker_connections
- upstream へのリクエストの HTTP バージョンが 1.0 になる
- upstream のレスポンスを見てクライアントへのレスポンスを変化させたい
- proxy_set_header でヘッダが正しく設定されない
- more_clear_headers を if の中で使うとヘッダが正しく除去されない
- ratelimit のリクエスト制限が期待通りにならない
- パーセントエンコードされたリクエストパスは upstream に流れる際にデコードされる
- Lua の埋め込みコードの実行順がわからない
- set_by_lua の例が見たい
- Lua の否定と location のマッチングの記号が狂気
- おまけ
location の path マッチングの優先順位がわからない
^~ とか記号だけだとよく理解できません。ドキュメントはこちらです。
Module ngx_http_core_module
完全一致
location = /exact { [ configuration 1 ] }前方一致
location ^~ /prefix { [ configuration 2 ] }正規表現ケースセンシティブ
location ~ /case-sensitive { [ configuration 3 ] }正規表現ケースインセンシティブ
location ~* /case-insensitive { [ configuration 4 ] }通常
location / { [ configuration 5 ] }
リバースプロキシとして設定する場合たくさんの location を定義することがあると思うので、 基本的にはできる限り 完全一致 や 前方一致 でルーティングを行うほうが読みやすい設定になると思います。
Nginx のビルド時のパラメータを後から確認したい
Nginx はビルド時に様々なモジュールを有効無効にすることができるので、ビルド後にどのモジュールを有効にしてビルドしたのかを確認したくなることがあります。
そんなときは nginx -V のオプションでビルド時の詳細なオプションを確認することができます。
$ nginx -V nginx version: nginx/1.12.1 built by gcc 6.3.0 20170516 (Debian 6.3.0-18) built with OpenSSL 1.1.0f 25 May 2017 TLS SNI support enabled configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-g -O2 -fdebug-prefix-map=/data/builder/debuild/nginx-1.12.1/debian/debuild-base/nginx-1.12.1=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-specs=/usr/share/dpkg/no-pie-link.specs -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie'
worker_processes と worker_rlimit_nofile と worker_connections
Nginx のパフォーマンス、気になりますよね。久しく Apache HTTP Server を触っていないですが、たくさんの設定を頑張って調整していたような記憶があります。 Nginx ではひとまず3つの値を正しく設定しておきましょう。
worker_processes:auto- デフォルトは
1なので、autoをセットして自動でCPUコア数分の worker が設定されるようにしましょう
- デフォルトは
worker_rlimit_nofile:100000worker_connections:2048- 1 worker プロセスに存在できる最大のコネクション数です、デフォルトは
512となっています。こちらはマシンのリソースを考慮して設定する形になります。
- 1 worker プロセスに存在できる最大のコネクション数です、デフォルトは
upstream へのリクエストの HTTP バージョンが 1.0 になる
proxy_http_version という設定値のデフォルトが 1.0 になっているため、
何も知らずにリバースプロキシの設定を行うと upstream へのリクエストの HTTP バージョンが 1.0 になってしまいます。
特に理由がなければ proxy_http_version 1.1 と設定しておくと良いでしょう。
upstream のレスポンスを見てクライアントへのレスポンスを変化させたい
こんな感じでできます。
location ~ ^/status {
rewrite ^ /api break;
proxy_pass http://localhost:8080;
proxy_intercept_errors on;
error_page 401 =200 @status_ok;
}
location @status_ok {
return 200 'OK';
}
この例はロードバランサー用の health check の設定として、 upstream が 401 を返した時に 200 のレスポンスを返すようにしています。
proxy_set_header でヘッダが正しく設定されない
proxy_set_header を設定することで、 upstream へのリクエストの任意のヘッダを設定することができます。
何が罠かと言うと、基本的には上位のレベルの http の context で定義した設定は下位のレベルの server や location の context でも継承されるのですが、
それは 同レベルの context に proxy_set_header が設定されていない時 に限ります。
つまり
server {
...
proxy_set_header X-Foo foo;
location /bar {
...
proxy_set_header X-Bar bar;
}
}
と記述した場合は /bar の location には X-Foo のヘッダが設定されていないことになります。
proxy_set_header を設定する際には記述する context のレベルを意識しましょう。
more_clear_headers を if の中で使うとヘッダが正しく除去されない
Nginx の if がよくハマるので使わない方がよいというのは定番の話なのですがそれはさておき。
more_clear_headers のドキュメントでは http server location location if の context で動作すると書いてありましたが、実際には location if の中で正しく動作しませんでした。(バージョン0.26時点)
そのため次のような Lua コードで対応しました。
header_filter_by_lua '
local removed_headers = {"Server", "X-Foo", "X-Bar"}
if ngx.req.get_headers()["Minimum"] == "0" then
for i, h in ipairs(removed_headers) do
ngx.header[h] = nil
end
end
';
ratelimit のリクエスト制限が期待通りにならない
こちらについては過去にエントリを書きました。
秒間5リクエストの制限を期待して rate=5r/s nodelay とした場合、実際には 0.2秒間に1リクエストしか受け付けない という動きになってしまうという話でした。
パーセントエンコードされたリクエストパスは upstream に流れる際にデコードされる
Nginx は リクエストパスに対してパーセントエンコードをデコードしたり、無駄なスラッシュを綺麗にしたり します。 それが良いか悪いかはさておき、 upstream にもパーセントエンコードがデコードされた状態で流されてしまうと困ることがあるかと思います。これに対する解決方法はこちら。
location /api {
rewrite ^ $request_uri;
rewrite ^/api/(.*)$ /$1 break;
rewrite ^/api$ / break;
return 400;
proxy_pass http://localhost:8080$uri;
}
単純に proxy_pass を設定するだけではなく、 $uri を生の $request_uri に書き換えて更に proxy_pass に $uri を付加します。
stackoverflowを見るにこれが決定版という感じですw
Nginx pass_proxy subdirectory without url decoding - Stack Overflow
Lua の埋め込みコードの実行順がわからない
こちらの図 がとっても便利です。
リクエストのパスを Nginx にいじられる前に何かしらを判定して変数に保存しておきたい場合は set_by_lua を使う、など慣れるまで何の処理をどこに書くが迷うので、そんなときはこの図を見ましょう。
set_by_lua の例が見たい
例えば ログに自前の変数を出力したい 時に便利です。
log_format app_log 'time:$time_iso8601\tpath:$request_uri\t$status\tapp_id:$app_id';
...
set_by_lua $app_id '
app_id = string.match(ngx.var.uri, "^/api/apps/(%w+)")
if (app_id ~= nil) then
return app_id
end
if (ngx.var.http_x_app_id ~= nil) then
return ngx.var.http_x_app_id
end
return "-"
';
リクエストパスがマッチするかヘッダが設定されていれば app_id にその値が、そうでなければ - が出力されます。
Lua の否定と location のマッチングの記号が狂気
location の ~* はケースインセンシティブな正規表現のマッチングですが
Lua の ~= はノットイコールです。wow!
おまけ
OpenRestyでどうしても意図したレスポンスヘッダが返らずハマりにハマったのですが、 結局 AWS の ELB でレスポンスヘッダが書き換えられている というオチでした。
現象としては、 HTTP/1.1 のクライアントからのリクエストに対して、 ELB と OpenResty を通じて upstream のサーバが Content-Length ヘッダも Transfer-Encoding ヘッダも持たない HTTP/1.0 用のレスポンスを返した場合に、最終的なレスポンスに transfer-encoding: chunked のヘッダが付いてボディがチャンク形式になるというものでした。
細かい話なのでまとめだけ書くと、
- Nginx/OpenResty は
chunked_transfer_encoding onが設定されていて(デフォルトはon)、Content-Lengthヘッダが設定されていないチャンクでないレスポンスを、 チャンク形式にしてTransfer-encoding: chunkedヘッダを付けて 返す - ELB は
Content-Lengthヘッダが設定されていないチャンクでないレスポンスを、 チャンク形式にしてtransfer-encoding: chunkedヘッダを付けて返すことがある - ELB は
Content-Lengthヘッダが設定されていないチャンクでないレスポンスを、Content-Lengthヘッダを付けて返すことがある
という結論でした。なにそれつらい。
ちなみに Nginx/OpenResty では proxy_buffering on と chunked_transfer_encoding off を設定してしまえばチャンク形式でレスポンスを返すことはないので、一応問題ないです。 ELB の方はいじれないので諦めて HTTP(S) ではなく SSL/TCP のプロキシとして使うしかなさそうです。 proxy protocol を使えば遜色なく Web サーバのプロキシとして利用できます。
チャンク形式のレスポンスを確認する際は curl のオプションで --raw を使って生のbodyを表示すると良いです。
おまけがだいぶ長くなってしまいました。
Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術
- 作者: 渋川よしき
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/06/14
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
- 作者: Dimitri Aivaliotis,高橋基信
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/10/26
- メディア: 大型本
- この商品を含むブログ (7件) を見る