複数のテストサーバをリバースプロキシで集約 (4)

mod_proxy_mapper のコードを詳説します。

mod_proxy に処理を移譲する方法

どこかのフェーズで

request_rec *r;

r->filename = "proxy:http://example.com/foo/bar";

r->proxyreq = PROXYREQ_REVERSE;
r->handler  = "proxy-server";

のように設定すると,handler フック(一般的なレスポンスハンドラ)において mod_proxy が呼び出されます。

なお proxyreq は通常のフォワードプロキシの場合 PROXYREQ_PROXY に設定します。また,まだリクエストがプロキシ化されていない場合,PROXYREQ_NONE になっています。

どのフェーズにフックをしかけるべきか

mod_rewrite の場合,

  • server context の場合 Translate Name フェーズ
  • directory context の場合 Fixups フェーズ

にフックをしかけています*1

server context というのは,httpd.conf においてグローバルに設定された場合をさします。一方 directory context とは,.htaccess<Directory> / <Location> などによってディレクトリを限定して設定された場合をさします。

フックから Apache の全体像を追う - daily dayflower からリクエストの処理フェーズ順を書き出してみます。

  1. Translate Name フェーズ
  2. Map To Storage フェーズ
  3. Header Parser フェーズ
  4. Access Checker フェーズ(access check by IP address, etc)
  5. Check User Id フェーズ (authentication)
  6. Auth Checker フェーズ (authorization)
  7. Type Checker フェーズ
  8. Fixups フェーズ

なぜ directory context で mod_rewrite が設定されている場合に Fixups フェーズでフックしているのかというと,httpd サーバは

  1. Map To Storage フェーズにおいて core_map_to_storage(r) (in server/core.c) が呼ばれる
  2. core_map_to_storage(r) 内で ap_directory_walk(r) (in server/request.c) が呼ばれる
  3. ap_directory_walk(r) 内で(存在すれば).htaccess を読み込み,r->per_dir_config を再構築する

という手数によって .htaccess を処理しているからです。

ですので .htaccess に記述された設定を反映するためには Map To Storage フェーズ以降に処理をする必要があります。ですが,このフェーズ以降(具体的には Fixups フェーズ)で URI の書き換えを行うのは「時すでに遅し」なのです。書き換えたあとの URI におけるリクエストフックチェイン(Translate Name 〜 Type Checker)が呼び出されません。したがって mod_rewrite では Fixups フェーズで URI の書き換えをおこなった場合,書き換え後の URI でサブリクエストを呼び出すようにしています。

しかしながら,リクエストごとにサブリクエストを呼び出すのでは処理が重すぎます。ですので server context で設定されていた場合には,Translate Name フェーズで URI の書き換えを行っています(この場合,サブリクエストを発行する必要がない)。このあたりのメカニズムについては http://httpd.apache.org/docs/2.2/rewrite/rewrite_tech.html#InternalAPI に詳しく書いてあります。


と長々と書いてきましたが,mod_proxy_mapper ではパフォーマンスは重視しないということと,mod_proxy に移譲する場合は URI の書き換え後にリクエストフックチェインを呼び出す必要がないということで,Fixups フェーズのみにしかけてあります。まあつまりなんにせよ mod_proxy に処理を移譲する場合,mod_rewrite がおこなっているほど複雑なことをする必要はないということです。

サブリクエストについて

mod_proxy へのマッパ一般とはあまり関係のない話です。

標準モジュールでサブリクエストが使われている場面は,mod_include(と mod_cgi((mod_cgi で使われているのは mod_include の #exec cgi の部分をうけもっているからです。)))です。[http://httpd.apache.org/docs/2.2/mod/mod_include.html#element.include:title=#include virtual="..."][http://httpd.apache.org/docs/2.2/mod/mod_include.html#element.exec:title=#exec cgi="..."] を実現するために使われています。

また,mod_autoindex でも [http://httpd.apache.org/docs/2.2/mod/mod_autoindex.html#headername:title=HeaderName][http://httpd.apache.org/docs/2.2/mod/mod_autoindex.html#readmename:title=ReadmeName] を処理するために使われています。

このように,通常サブリクエストは(レスポンス)ハンドラから呼び出して実行結果をドキュメントとして取り込むために用います。しかし,今回は(レスポンス)ハンドラフックから呼び出しているわけではありません。つまり,サブリクエストがなにがしかの出力をしてしまうと,それが通常フロー後のレスポンスハンドラ(今回の場合 mod_proxy)の出力に prepend されてしまうことになります。これは困ります。

そこで,

static apr_status_t
null_output_filter(ap_filter_t *f, apr_bucket_brigade *bb)
{
    apr_brigade_destroy(bb);

    return APR_SUCCESS;
}

のように「出力をすべて捨ててしまう」フィルタを登録し,このフィルタをサブリクエストの出力先として設定しています。*2本来出力フィルタは

    return ap_pass_brigade(f->next, bb);

のように次のフィルタチェインに処理を渡すべきなのですが,このようにあえて呼び出さないことで null 化しています。あんまりほめられたマナーではないですね。

ちなみに,サブリクエストの処理内では request_rec->protocol"INCLUDED" になります。すなわち CGI 環境変数として SERVER_PROTOCOLINCLUDED になります。

おわりに

proxy サーバのバランサモジュールとして mod_proxy_balancer というのがあります。バックエンドサーバの選定アルゴリズムを取り替えることのできる proxylbmethod provider というメカニズム*3もあるのですが,自分ごのみの柔軟なマッピングをおこないたい場合,mod_rewrite と mod_proxy_balancer に頼らず,自力で proxy server のマッピングモジュールを書いてみるのもいいのではないでしょうか。

*1:それ以外のフックもしかけていますが,リクエスURI の書き換えにかかわるフックとして両者のフェーズを利用しています。

*2:mod_include や mod_autoindex を見ると特に特別な入力フィルタを設定していないのですが,念のため入力フィルタも「入力を無視する」フィルタを登録しています。レスポンスハンドラの場合,最初に自分が入力を読み取ってしまうからそのようになっているのかもしれません。未調査。

*3:mod_proxy_balancerに独自振り分けロジックを追加できる気がする | 眠る開発屋blogApache の provider 機構 - 他モジュールに移譲するしくみ - daily dayflower 参照