Apacheでreverse proxyするときに、受けのパスに応じて異なるタイムアウト値を設定する方法

Apacheでreverse proxyするときに、バックエンドは同じなんだけど受けるパスに応じて別なタイムアウト値を設定しようとしてハマったのでそのメモです。

以下、Apache 2.2.22 でのお話です。2.4でどうなってるか、どなたかご存知でしたら教えてください><


まず思いつくのはこんな設定だと思います。

ProxyTimeout 7
ProxyPass    /3sec/  http://127.0.0.1:9999/  timeout=3
ProxyPass    /5sec/  http://127.0.0.1:9999/  timeout=5

バックエンド (127.0.0.1:9999) は、こんなのを動かしておけば十分です。

$ while true; do echo listen...; nc -l 9999; done

これでクライアントでアクセスしてみると…

$ time curl http://example.com/5sec/foo/bar >/dev/null
real    0m5.011s
user    0m0.000s
sys     0m0.000s

$ time curl http://example.com/3sec/foo/bar >/dev/null
real    0m5.010s
user    0m0.000s
sys     0m0.000s

/5sec/ の方は期待した通り 5 秒で(タイムアウトして)レスポンスが返ってきてますが、/3sec/ の方は 3 秒ではなく 5 秒かかっています。


これの原因は、Apache の proxy はバックエンドへの接続や設定を管理するのに「Worker」というモノを使っていて、Worker は ProxyPass で指定されたバックエンドの URL (スキーマからパスまでも含む) を一意キーとして管理、共用されているためです。

このへんはドキュメントにちゃんと書かれています。

つまり、/3sec/ で受けても /5sec/ で受けても同じ Worker に処理されることになり、その設定値は最後に現れたもので上書かれるので、/3sec/ でアクセスしてもそのタイムアウトは 5 秒になるわけです。


これを理解していれば、受けのパスに応じてタイムアウト値を変えるには、ProxyPassのURLを違えて別々の Worker が作らるようにすればいいことに気が付くでしょう。

例えば、以下のように片方だけ127.0.0.1localhostに変えると、

ProxyTimeout 7
ProxyPass    /3sec/  http://localhost:9999/  timeout=3
ProxyPass    /5sec/  http://127.0.0.1:9999/  timeout=5

期待したタイムアウトになります。ハッピー

$ time curl http://example.com/5sec/foo/bar >/dev/null
real    0m5.011s
user    0m0.000s
sys     0m0.000s

$ time curl http://example.com/3sec/foo/bar >/dev/null
real    0m3.009s
user    0m0.000s
sys     0m0.000s


もしもっとバリエーションを増やしたければ、

  • バックエンドが0.0.0.0 (INADDR_ANY) をlistenしているのなら、ホスト名を0.0.0.0やeth0のアドレスにする
  • バックエンドが複数ポートをlistenするようにして、ポートを違える
  • loに127.0.0.2をIP Aliasして、ホスト名を127.0.0.2にする
  • http://127.0.0.1/dummy/../ のようにしてパスを違える (バックエンド側で、パスを正規化してからディスパッチするようなロジックになってないとおかしなことになるので要注意)

といった方法があります。

ひとつ留意しておきたいのは、本質的には同じバックエンドなので、本来ならば Worker を共有すべきであり、ここで紹介した方法は深遠な理由がある場合の苦肉の策である、という点です。

さてここまでがおハマりその1です。


もうひとつのおハマりは、ProxyPass の代わりに ProxyPassMatch を使った場合です。

  ProxyTimeout    7
  ProxyPassMatch ^/3sec/(.*)$  http://localhost:9999/$1  timeout=3
  ProxyPass       /5sec/       http://127.0.0.1:9999/    timeout=5

reverse proxyの動作としては、先に見たものと同じ挙動になるはずの設定です。

が、

$ time curl http://example.com/5sec/foo/bar >/dev/null
real    0m5.011s
user    0m0.000s
sys     0m0.000s

$ time curl http://example.com/3sec/foo/bar >/dev/null
real    0m7.010s
user    0m0.000s
sys     0m0.000s

ProxyPassMatch を使った /3sec/ の方が、3 秒ではなく 7 秒かかってしまっています。

この設定のケースでは、

という一意キーを持った Worker が作られます。

mod_proxy は ProxyPass, ProxyPassMatch の設定に従って書き換えた URL と、この一意キーを比較して、適切な Worker を選出します。このへんの処理は、modules/proxy/proxy_util.c の ap_proxy_get_worker の下の方で行われています。

http://example.com/3sec/foo/bar というアクセスが来た場合、ProxyPassMatchの設定に従って http://loaclhost:9999/foo/bar に書き換えられた URL と、Workerのキー(worker->name)が比較されるのですが、先に見たように合致するはずの Worker のキーは「http://localhost:9999/$1」となっていて、「$1」がじゃまでマッチせずに終わってしまいます。

マッチする Worker がなかった場合は、デフォルトの Worker というのがいて、このデフォルト Worker が処理を引き受けることになります。そしてこのデフォルト Worker のタイムアウトの設定が ProxyTimeout 7 なので、/3sec/foo/bar へのアクセスは 3 秒ではなく 7 秒でタイムアウトして返ってきているわけです。

つまり、ProxyPassMatch でバックリファレンス($1とか)入りの URL を指定した場合、Worker は作られるがまず使われることはないということになります。


こちらからは以上です

関係あるかも