Home > ブログ > Nginxパズル: ネストされたlocationのマッチング動作

ブログ

Nginxパズル: ネストされたlocationのマッチング動作

Nginxでは特定URIへの設定を行うのにlocationディレクティブを使います。 locationはぱっとみ簡単そうですが、実のところどの設定が適用されるのかルールが結構分かりづらかったりします。

さらに、このlocationはネストして指定することができ、ネストしてlocationをグループ化することで、 設定の重複記述を排除したり、URLのマッチングを減らしパフォーマンス劣化の懸念を回避できます。

locationのマッチングルールは公式サイトで説明されていますが、locationがネストしている場合については説明に若干の曖昧さがあるので、まれに勘違いした設定も見られます。今回はlocationのネストについて少し詳しく説明していきます。

locationの種類

まずは基本のおさらいとしてlocationの種類には以下のものがあります。

修飾子 動作
なし リクエストURI先頭からの最長一致(longest match)を行う。
^~ リクエストURI先頭からの最長一致(longest match)を行う。
このエントリにマッチした場合は後続の正規表現のマッチングは行わない。
正規表現のマッチング停止は同階層のものに対してのみ有効(これはネスト動作の説明で解説します)。
= リクエストURIと完全一致(exact match)を行う。
これ以降のマッチングは行わずに終了(親階層の評価も終了)。
~ 正規表現によるマッチングを行う。case sensitive.
~* 正規表現によるマッチングを行う。case insensitive.

※名前付きlocationは省略

locationには正規表現を使うものとそうでないものがあります。 これらを区別するために、本記事では正規表現の含まれていないlocation(修飾子:なし, =, ^~ のもの)をプレフィクスlocation(*1)、正規表現を使うlocation(修飾子:~, ~* のもの)を正規表現locationと呼ぶことにします。

ネストを考慮しない場合のマッチング順

リクエストされたURIに対してlocationの選択を行うわけですが、 全体的な流れとしてはプレフィクスlocationのマッチングが行われた後、正規表現locationのマッチングが行われ、基本的に正規表現locationのマッチがあればそちらが優先されます。 具体的に見ていきましょう。

(1) プレフィクスlocationのマッチング

まずプレフィスクlocationのマッチング処理が動作します。 プレフィクスlocationのマッチングでは、リクエストURIに最長一致(longest match)するものが選択されます。定義順は関係ありません。

選択したlocationの修飾子が"="(完全一致型)ならここでマッチング処理は終了し、locationが確定します。

修飾子"^~"のlocationにマッチした場合も正規表現のチェックは行われないのでここでlocationが確定します。

それ以外の以下の場合は、(2)の正規表現locationの検索に進みます。

  • 修飾子なしのlocationにマッチ
  • マッチしたlocationなし

(2) 正規表現locationのマッチング

正規表現locationのマッチング処理では、設定ファイルでの定義順にURIと正規表現のマッチングを行い、マッチした時点でそのlocationが選択されてマッチング処理は終了します。

マッチしたものがなければ(1)で選択したlocationで確定します。

マッチング処理の流れを図示すると以下のようになります。

ネストがない場合のマッチング動作
図1 ネストがない場合のマッチング動作

ざっくりとしたイメージとしては以下のような優先度で選択されると考えていいでしょう。

  • location =
  • location ^~
  • location ~,~*
  • location

一応、簡単な例を示します。
以下のような5つのlocation設定があったとします。

location例

location /priv {               # (a)
}
location /private/ {           # (b)
}
location = /private/cart.php { # (c)
}
location ^~ /news {            # (d)
}
location ~ \.php$ {            # (e)
}

これに対して、以下のような動作になります。

/private/member.html

(b)が選択される。
プレフィクスlocationは(b)にマッチして正規表現locationにはマッチするものがないので最終的に(b)が選択される。

/private/cart.php

(c)が選択される。完全一致し、正規表現は評価されない。

/private/address.php

(e)が選択される。

/news/show.php

(d)が選択される。(d)は修飾子が^~のlocationなので正規表現は評価されない。

選択されるのは一つで、例えば/private/address.phpへのアクセスで(b)と(e)が同時に選択されたりはしません。設定時に間違えやすい点なので注意が必要です。

既に若干ややこしいですが、ここまでは公式ドキュメントに明確に記載されているので問題ありません。

locationがネストした場合はどうなるのでしょうか?

例えば以下のconfigを想定した場合、

    location / {                          # (a)
        location ~ \.php$ {               # (b)
        }
        location /admin/ {                # (c)
            location ^~ /admin/files/ {   # (d)
            }
            location ~ \.php$ {           # (e)
            }
        }
    }

/admin/index.phpへのアクセス時の正規表現locationのマッチング順はどうなるのでしょうか? (e)→(b)?あくまで定義順なので(b)→(e)?(*2)

/admin/files/detail.phpへのアクセスどのlocationにマッチするのでしょうか? プレフィクスlocationのマッチングで(d)がマッチし、(d)は^~なので正規表現はマッチングされず(d)になるのでしょうか?(*3)

このあたりの実際の動作を明確にしていきます。

ネストしたlocationのマッチング

先に説明したマッチング処理の流れはネストを考慮せず簡略化した手順ですが、実際にはネストされる場合があるので以下のような順番でマッチングしています。

(1) プレフィクスlocationのマッチング

プレフィクスlocationのマッチングでは、リクエストURIに最長一致(longest match)するものが選択されます。定義順は関係ありません。

選択したlocationの修飾子が"="(完全一致型)ならここでマッチング処理は終了し、この階層のlocationが確定します。

修飾子"^~"のlocationにマッチした場合も正規表現のチェックは行われないので、ここでこの階層のlocationが確定します。"="と異なり検索は続くので親階層の正規表現のマッチングは行われます

(1.1)
(1)で最長一致のlocation(修飾子なし or ^~)にマッチしていたならlocationのマッチング処理を再帰呼び出しして、ネストしたlocationに対してマッチングを開始します。 ネストしたプレフィクスlocationがない場合は、該当なしで戻ります。

マッチしたlocationがなかった場合、修飾子なしのlocationにマッチした後、再帰処理から戻ってきた場合は、(2)の正規表現locationの検索に進みます。

(2) 正規表現locationのマッチング

同階層の正規表現locationのマッチングを定義順に行います。

マッチありの場合:
終了。マッチした正規表現locationが選択されます。
ただし、配下にさらに正規表現locationがネストされていれば(*4)、それらがマッチするか再帰的にマッチング処理を行い、マッチするものがあればそれを選択します。

マッチなしの場合:
(1)でlongestマッチしたlocationがあればそれを選択して終了。

プレフィクスlocation、正規表現locationのマッチ後に再帰処理がある点がネストなしの時と説明と異なります(太字箇所)。これを図にしたものが図2になります。

ネストを考慮した実際のマッチング動作
図2 ネストを考慮した実際のマッチング動作

まぁ、、、かなりわかりにくいので、

  • マッチングは階層ごとに行われること。
  • プレフィクスlocation、正規表現locationのマッチ後に再帰的にマッチング処理が開始されること

だけ理解しておいていただければと思います。

これだと実際のマッチングがどうなるのかわかりにくいので、config例を元にマッチングの流れを図示したものが図3になります。

マッチングの流れ
図3 マッチングの流れ

※ 通常locationをネストさせる時は、トップレベルにlocation /を置いて、その中にlocationをネストすることが多いと思いますが、この例では図があまり複雑にならないようにトップ階層に直接複数のlocationを設置しています(/admin/, /private/, ...)。

要点としては以下になります。

  • プレフィクスlocationのマッチングでマッチしたlocationがあれば、再帰的にlocation配下のマッチングを開始。
  • プレフィクスlocationのマッチング完了(再帰処理からのリターン)後、同階層の正規表現マッチングを継続。
  • location =にマッチしたらその時点で終了(locationを確定)
  • 正規表現にマッチしたらその時点で終了(locationを確定)。親階層の正規表現マッチングも行わない。
  • location ^~にマッチしたなら"同階層の"正規表現マッチングは行わない。マッチング処理自体は継続されるので、再帰処理を戻りながら上位階層の正規表現のマッチングは行われる可能性がある。

マッチングの例をいくつか見てみましょう。

    location / { # (a)
        location ^~ /private/ { # (b)
        }
        location = /private/exact.php { # (c)
        }
        location /admin/ {  # (d)
            location /admin/categories/ { # (e)
            }
            location ^~ /admin/files/ {   # (f)
            }
            location ~  \.php$ { # (g)
            }
        }
        location ~ \.php$ {     # (h)
        }
    }

このconfig例の場合、以下のようになります。

/foo.html

(a)にマッチ。この際、正規表現(h)の評価はされる。

/test.php

(h)にマッチ。正規表現は(h)のみ評価される。

/private/other.html

(b)にマッチ。正規表現は(g),(h)は評価されない。

/private/exact.php

(c)にマッチ。正規表現は(g),(h)は評価されない。

/admin/members.html

(d)にマッチ。正規表現は(g),(h)とも評価はされる。

/admin/list.php

(g)にマッチ。 すでに(g)が選ばれているので(h)は選ばれない((h)の評価はされない)。

/admin/categories/animal.html

(e)にマッチ。正規表現は(g),(h)とも評価はされる。

/admin/categories/animal.php

(g)にマッチ。正規表現(h)の評価はされない。

/admin/files/detail.php

(h)にマッチ。(f)ではないのに注意。親階層の(h)は評価されるため。

ちなみにネストしていても選択されるlocationは一つだけですが、設定は上位のlocationのものも反映されます(厳密には反映されないものもありますが)。 例えば、(d)の"location /admin/"にallow, denyの設定をしておけば(e)にマッチした場合もこの設定は有効です。一方、rewrite, try_filesなどはネストしたlocationに設定が継承されないので注意が必要です。この辺りもややこしいのですが、今回はあくまでlocation選択動作の説明に注力するので触れません。

正規表現locationがネストしている例も見てみましょう。

正規表現locationがネストした例

    location / { # (a)
        location ~ ^/list-.*\.php$ {     # (b)
            location ~ ^/list-goods-book-.*\.php$ {  # (c)
            }
            location ~ ^/list-goods-.*\.php$ { # (d)
            }
        }
        location ~ \.php$ {            # (e)
        }
    }

/index.php

(e)にマッチ。

/list-member.php

(b)にマッチ。

/list-goods-book-novel.php

(c)にマッチ。(d)は評価されない。

/list-goods-book.php

(d)にマッチ。

個人的には、正規表現locationをネストしたことはありませんし必要も感じないのですが、このような動作になります。

以上でネストしたlocationのマッチング処理の説明は終わりになります。 これで、ネストした設定でも正確に動作を把握できるようになったはずです。

余計なマッチング処理の削減

最初に"ネストすることで余計なマッチング処理を省くことができる"と述べましたが、少し補足しておきます。

プレフィクスlocation設定はnginx内部ではリストではなくツリー管理されており、URLのLongest matchを行う過程で全エントリをスキャンしなくていいようになっています。 このため、location設定数が増加してもそれに比例してマッチングコストが上がっていくことはありません。 それでも、locationをネストしてURLごとに対象locationを絞り込めるなら、多少でも余計なマッチング処理を省けるのでその方がいいでしょう。

一方、正規表現locationは定義順に順番にマッチングされます。正規表現locationが最後までマッチしない場合では、全ての正規表現locationのマッチングが行わます。このため、プレフィクスlocationに比べて設定数の増加はマッチングコストへの影響が大きくなります。locationをネストしてURLごとに対象正規表現を絞り込めるならそのようにした方がいいはずです。

例えば、問い合わせフォームでしかphpを使っていないようなサイトでは、以下のようにして/contact/以外では正規表現マッチングを避けられます。

location /contact/ {
    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_index index.php;
        include /etc/nginx/fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors on;
    }
    ...
}

サイトリニューアル時に旧URLから新URLに大量のリダイレクトを設定したい場合なども、以下のように階層化して定義することで、関係ないURLでの余計なlocationマッチングを減らすことができます。

location /products/ {
    location = /products/product-a.html {
        return 301 https://$host/products/a/;
    }
    location = /products/product-b.html {
        return 301 https://$host/products/b/;
    }
    ...
}

実装の参考箇所

最後に、locationのマッチング処理はsrc/http/ngx_http_core_module.cのngx_http_core_find_location()で行われています。自分で確認してみたい人はこのあたりを追ってみてください。

(*1) 公式ドキュメントにprefix locationとあるので、プレフィクスlocationと呼ぶことにします。 ただし、ソースコード中では正規表現なしlocationはstatic locationと呼んでいるようです。参考まで。

(*2) 正解は(e)→(b)。なので(e)が選択されます。

(*3) 正解は(b)。

(*4) ちなみに正規表現locationの中には正規表現locationしかネストできません。

投稿日:2025/03/25 01:28

タグ: Server Nginx

Top

アーカイブ

タグ

Server (30) 作業実績 (22) PHP (19) ネットワーク (17) プログラミング (15) OpenSSL (10) PHP関連更新作業 (8) C (8) C++ (8) EC-CUBE (7) Webアプリ (7) laravel (6) Linux (6) Nginx (6) 与太話 (5) 書籍 (5) AWS (4) Vue.js (4) JavaScript (4) Rust (3) Symfony (2) お知らせ (2) Golang (2) OSS (1) デモ (1) MySQL (1) CreateJS (1) Apache (1)