Usual Software Engineer

よくあるソフトウェアエンジニアのブログ

Nginx/OpenRestyあるある言いたい

Nginx を使う時に、設定に対して動作が意図したとおりにならないことがよくあります。 おそらく初見殺しで何度もハマる人が多いのでここであるあるをまとめておこうと思います。 OpenResty の話も混ざっていますがほぼ同じと考えて良いです。 ではさっそく、 Nginx あるある言いたい〜〜〜

location の path マッチングの優先順位がわからない

^~ とか記号だけだとよく理解できません。ドキュメントはこちらです。
Module ngx_http_core_module

  1. 完全一致

     location = /exact {
         [ configuration 1 ]
     }
    
  2. 前方一致

     location ^~ /prefix {
         [ configuration 2 ]
     }
    
  3. 正規表現ケースセンシティブ

     location ~ /case-sensitive {
         [ configuration 3 ]
     }
    
  4. 正規表現ケースインセンシティブ

     location ~* /case-insensitive {
         [ configuration 4 ]
     }
    
  5. 通常

     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 が開くことができるファイルディスクリプタの数ですが、特別制限する必要がなければ大きな値を設定しておいて良いでしょう。最低でも worker_processes * worker_connections 以上の値を設定する必要があります。
    • 例えば Linux のファイルディスクリプタの場合は ulimit などで設定しますが、 worker_rlimit_nofile が設定されている場合は Nginx 側で強制的に worker の rlimit を設定してくれるので、 Linux のファイルディスクリプタの設定の影響は受けないようです。
  • worker_connections : 2048
    • 1 worker プロセスに存在できる最大のコネクション数です、デフォルトは 512 となっています。こちらはマシンのリソースを考慮して設定する形になります。

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 で定義した設定は下位のレベルの serverlocation の 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 の中で使うとヘッダが正しく除去されない

ドキュメントでは 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 のリクエスト制限が期待通りにならない

こちらについては過去にエントリを書きました。

innossh.hatenablog.com

秒間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 の埋め込みコードの実行順がわからない

こちらの図 がとっても便利です。

https://cloud.githubusercontent.com/assets/2137369/15272097/77d1c09e-1a37-11e6-97ef-d9767035fc3e.png

github.com

リクエストのパスを 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 onchunked_transfer_encoding off を設定してしまえばチャンク形式でレスポンスを返すことはないので、一応問題ないです。 ELB の方はいじれないので諦めて HTTP(S) ではなく SSL/TCP のプロキシとして使うしかなさそうです。 proxy protocol を使えば遜色なく Web サーバのプロキシとして利用できます。

チャンク形式のレスポンスを確認する際は curl のオプションで --raw を使って生のbodyを表示すると良いです。
おまけがだいぶ長くなってしまいました。

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

マスタリングNginx

マスタリングNginx