Nginx/OpenRestyあるある言いたい
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
:100000
worker_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件) を見る