2009-03-17
APR-util の DBD API を使ってみる
APR(の一部の APR-util)には,各種データベースを統一インタフェースで使うことのできる DBD API があります。のでちょっと使ってみました。
ちなみに普通に C でアプリケーションを書いている人にはあまりおすすめできないと思います。統一インタフェースというほど方言を吸収しているとはいえないので。もっとも portability layer としてすでに APR を使用していて,ちょっくら DB も(軽く)使用しようかというケースなら向いているかもしれません。どちらかというと,DB を使用する Apache のモジュールを開発している場合に,個別のデータベースクライアントライブラリにバインドするよりはこちらを使ったほうが,柔軟性(データベースの選択)が増すという用途です。さらに mod_dbd なんてのもあわせて使うとおもしろいかも。
どのデータベースエンジンがサポートされているのか
apr_dbd_open_ex() の API ドキュメントによると,
- PostgreSQL
- SQLite2
- SQLite3
- Oracle
- MySQL
- FreeTDS
がサポートされているようです。
実際にはどうなのか,apr_dbd_get_driver() を利用してたしかめてみます(CentOS 5.2 の APR Util 1.2.7 で検証)。
#include <stdio.h> #include <stdlib.h> #include <apr_pools.h> #include <apu.h> #include <apr_dbd.h> static void print_apr_error(apr_status_t status, const char *message) { char buf[1024]; fprintf(stderr, "%s%s%s\n", apr_strerror(status, buf, sizeof(buf)), message ? ": " : "", message ? message : ""); } static void error_exit(apr_status_t status) { print_apr_error(status, NULL); exit(status); } int main(int argc, char *argv[]) { apr_pool_t *pool; apr_status_t r; const apr_dbd_driver_t *driver; const char **pdb; static const char *dbs[] = { "sqlite3", "mysql", "pgsql", NULL, }; apr_pool_initialize(); apr_pool_create(&pool, NULL); /* root pool */ r = apr_dbd_init(pool); if (r != APR_SUCCESS) error_exit(r); for (pdb = dbs; *pdb; pdb ++) { fprintf(stdout, "\n[%s]\n", *pdb); r = apr_dbd_get_driver(pool, *pdb, &driver); if (r != APR_SUCCESS) { print_apr_error(r, NULL); } else { fputs("OK!\n", stdout); } } return 0; }
ビルドするのがちょっと骨なので pkg-config を利用した Makefile を置いておきます。
CFLAGS := $(shell pkg-config --cflags apr-1) \ $(shell pkg-config --cflags apr-util-1) LIBS := $(shell pkg-config --libs apr-1) \ $(shell pkg-config --libs apr-util-1) TARGET := drivers all: build build: $(TARGET) clean: rm -f $(TARGET) test: $(TARGET) ./$(TARGET) $(TARGET): main.o $(CC) -o $@ $^ $(LIBS) .PHONY: all build clean test
さて,実行すると,
$ ./drivers [sqlite3] OK! [mysql] This function has not been implemented on this platform [pgsql] OK!
のような結果になりました。
実は apu.h ヘッダに
#define APU_HAVE_PGSQL 1 #define APU_HAVE_MYSQL 0 #define APU_HAVE_SQLITE3 1 #define APU_HAVE_SQLITE2 0
とあります(もちろん環境により内容が異なります)。プログラム書いてまでたしかめるもんでもなかったな。
MySQL 用ライブラリが含まれていないのは,CentOS 5.2 採録の Apache 2.2.3 のころはライセンス不整合問題のため含まれていなかったようです*1。今回は SQLite3 を使うつもりなので,まぁいいや。
データベースの準備
もちろん DBD API で query を発行してテーブルを作成してもよいんですが,面倒なので CLI から作成しておきます。
$ sqlite3 test.db SQLite version 3.3.6 Enter ".help" for instructions sqlite> CREATE TABLE t_user ( id INTEGER, name VARCHAR ); sqlite> INSERT INTO t_user (id, name) VALUES ( 1, 'dayflower' ); sqlite> SELECT * FROM t_user; 1|dayflower sqlite> .q
なにはともあれ SELECT
めんどうなので一部実装(エラー表示やエラーチェック)をはしょっていますが,SELECT を行うプログラムを書いてみました。
#include <stdio.h> #include <stdlib.h> #include <apr_pools.h> #include <apu.h> #include <apr_dbd.h> static void print_apr_error(apr_status_t status, const char *message); static void error_exit(apr_status_t status); int main(int argc, char *argv[]) { apr_status_t r; apr_pool_t *pool; const apr_dbd_driver_t *driver; apr_dbd_t *dbh; apr_dbd_results_t *dbr; apr_dbd_row_t *row; int n, nrows, ncols; apr_pool_initialize(); apr_pool_create(&pool, NULL); /* root pool */ r = apr_dbd_init(pool); if (r != APR_SUCCESS) error_exit(r); r = apr_dbd_get_driver(pool, "sqlite3", &driver); if (r != APR_SUCCESS) error_exit(r); r = apr_dbd_open(driver, pool, "test.db", &dbh); if (r != APR_SUCCESS) error_exit(r); dbr = NULL; apr_dbd_select(driver, pool, dbh, &dbr, "SELECT * FROM t_user", 0); ncols = apr_dbd_num_cols(driver, dbr); fprintf(stdout, "num_cols = %d\n", ncols); nrows = apr_dbd_num_tuples(driver, dbr); fprintf(stdout, "num_tuples = %d\n", nrows); row = NULL; for (n = 0; apr_dbd_get_row(driver, pool, dbr, &row, -1) == 0; n ++) { int i; fprintf(stdout, "record #%d\n", n); if (row != NULL) for (i = 0; i < ncols; i ++) fprintf(stdout, "[%d]: %s\n", i, apr_dbd_get_entry(driver, row, i)); } r = apr_dbd_close(driver, dbh); return 0; }
-
apr_dbd_open()で DB をオープンして -
apr_dbd_select()で選択系 SQL を発行し -
apr_dbd_get_row()で各行を fetch*2 -
apr_dbd_get_entry()で各行の各コラムを取得 - さいごに
apr_dbd_close()でクローズ
という手順です。
APR-util 1.4 だと apr_dbd_get_name() という API でコラムの名前(テーブルの列名)を取得できるのですが,APR-util 1.3 ではできませんでした。残念。
dbr = NULL;
apr_dbd_select(driver, pool, dbh, &dbr, "SELECT * FROM t_user", 0);
のような表現が頻出しますが,このような場所では NULL を入れておかないとセグフォって落ちます。API ドキュメントに
res- pointer to result set pointer. May point to
NULLon entry
と書いてあったので,取得する行によっては NULL が返ることもあるよ,とかのほほんと構えていたら落ちたのでびっくりしました。よくよく読んだら,最初に実行する時は,とかそういう意味ですね。
ほんとは変数宣言部分で NULL を代入すればいいんですが,このような経緯もあり,自戒をこめてあえて後段で NULL を代入しています。
さて,実行します。
$ ./select num_cols = 2 num_tuples = 1 record #0 [0]: 1 [1]: dayflower
無事取得できました。
せっかくだから INSERT
つぎに,DBD API から INSERT をしてみます。
#include <stdio.h> #include <stdlib.h> #include <apr_pools.h> #include <apr_strings.h> #include <apu.h> #include <apr_dbd.h> static void print_apr_error(apr_status_t status, const char *message); static void error_exit(apr_status_t status); int main(int argc, char *argv[]) { apr_status_t r; apr_pool_t *pool; const apr_dbd_driver_t *driver; apr_dbd_t *dbh; apr_dbd_transaction_t *trans; int nrows; apr_pool_initialize(); apr_pool_create(&pool, NULL); /* root pool */ r = apr_dbd_init(pool); if (r != APR_SUCCESS) error_exit(r); r = apr_dbd_get_driver(pool, "sqlite3", &driver); if (r != APR_SUCCESS) error_exit(r); r = apr_dbd_open(driver, pool, "test.db", &dbh); if (r != APR_SUCCESS) error_exit(r); trans = NULL; apr_dbd_transaction_start(driver, pool, dbh, &trans); apr_dbd_query(driver, dbh, &nrows, apr_psprintf(pool, "INSERT INTO t_user (id, name)" "VALUES (\"%s\", \"%s\")", apr_dbd_escape(driver, pool, "123", dbh), apr_dbd_escape(driver, pool, "foo", dbh)) ); fprintf(stdout, "%d rows accected.\n", nrows); apr_dbd_transaction_end(driver, pool, trans); r = apr_dbd_close(driver, dbh); return 0; }
無駄にトランザクション処理も行っています。まぁトランザクションといっても,エラーが発生したらロールバックし(というよりその後に発行される SQL を無視する)発生しなかったらコミットするというだけのプリミティブなものですが*3。
手順としては,
-
apr_dbd_escape()でエスケープ処理を行って -
apr_dbd_query()で選択系以外の SQL 文を発行
ということになります。
プリペアードステートメントは使えないの?と思いますが,一応 API として用意はされています。しかし,APR-util 1.3 と SQLite の組み合わせでは使えなかった*4ので,今回のプログラムでは上記のように自力でエスケープ処理を行っています。プリペアードステートメントを使う場合,
apr_dbd_prepared_t *ps;
const char *values[2];
ps = NULL;
apr_dbd_prepare(driver, pool, dbh, "INSERT INTO t_user (id, name)"
"VALUES (?, ?)",
NULL, &ps);
values[0] = "12345";
values[1] = "foo";
apr_dbd_pquery(driver, pool, dbh, &nrows, ps, 2, values);
のような構文になります。
さて,実行しますと,
$ ./insert 1 rows accected.
無事1行挿入できたようです。
CLI をつかってたしかめてもいいのですが,せっかくなので先ほど作成した SELECT コマンドを実行してみます。
$ ./select num_cols = 2 num_tuples = 2 record #0 [0]: 1 [1]: dayflower record #1 [0]: 123 [1]: foo
うまくいっていますね。
メモリ使用量について
「ハンドル」を取得するための API は,だいたい次のような形になります。
int apr_dbd_get_row(const apr_dbd_driver_t *driver, apr_pool_t *pool, apr_dbd_results_t *res, apr_dbd_row_t **row, int rownum)
このように,ポインタへのポインタをとります。ので,ハンドルのメモリアロケーションは API 側でやってくれるようです。そしてリリースのための API は定義されていません。
ということは,一度データベースのハンドルを取得したのちに,複数の SQL 文を発行する(=apr_dbd_results_t ハンドルが生成される)と,どんどんメモリが確保されていくことになります(ソースを追っていないので詳細は不明ですが,おそらくそうでしょう)。今回のように CLI を書くだけなら問題にはなりませんが,Apache モジュールを書いている場合,(プロセスが落ちるまで)どんどんメモリの使用量がふえていきます。
今回はそこまでのコーディングはやっていませんが,このような場合,各リクエストごとなどの単位でサブメモリプールを生成して(あるいは request_rec の pool を利用して),リクエスト終了時にメモリプールを破棄するようにすればいいでしょう。
そうやって考えてみれば,APR-util DBD API の多くの関数で,つどつど apr_pool_t を要求している理由がわかりました。
*1:漢(オトコ)のコンピュータ道: Apache mod_dbd設定編 参照
*2:apr_dbd_select() で random パラメータを 1 にすると,行指定で fetch できます。今回は random = 0 なので,next row fetch しているだけです。
*3:APR-util 1.4 だとトランザクションのモードを指定できるようになっています。といってもやはりプリミティブな機能であることにはかわりありませんが。
*4:どちらが悪いのかは不明。
2008-11-25
Apache の sub request と internal redirect
極私的メモ。
通常のリクエスト処理フロー
modules/http/http_core.c で ap_process_http_connection() 関数 を ap_HOOK_process_connection として登録しており,これが(ap_HOOK_process_connection をオーバーライドしていなければ)通常のリクエスト処理の関数となります。
ap_process_http_connection() の実装について フックから Apache の全体像を追う - daily dayflower の再掲になりますが,
ap_process_http_connection()
{
/*
* …… request_rec の準備 ……
*/
ap_RUN_create_request(); // RUN_ALL
ap_RUN_post_read_request(); // RUN_ALL
/* quick_handler が用意されてるならとっとと実行→ fast exit */
ap_RUN_quick_handler(); // RUN_FIRST
/* request_rec の整備(r->filename トカ,AAA トカ) */
ap_process_request_internal();
/* レスポンスハンドラの呼び出し */
ap_invoke_handler();
/* ログ吐き(どんなリクエストでも必ず実行) */
ap_RUN_log_transaction(); // RUN_ALL
}
おおまかに
のような行程を経ることになります。
ちなみに ap_invoke_handler() は下記のような実装です。
ap_invoke_handler()
{
ap_RUN_insert_filter(); // VOID
ap_RUN_handler(); // RUN_FIRST
}
(今後頻出する)ap_process_request_internal() は下記のような実装です。
ap_process_request_internal()
{
ap_RUN_translate_name(); // RUN_FIRST
ap_RUN_map_to_storage(); // RUN_FIRST
ap_RUN_header_parser(); // RUN_ALL
ap_RUN_access_checker(); // RUN_ALL
ap_RUN_check_user_id(); // RUN_FIRST
ap_RUN_auth_checker(); // RUN_FIRST
ap_RUN_type_checker(); // RUN_FIRST
ap_RUN_fixups(); // RUN_ALL
}
レスポンスを返すことはしませんが,それ以外の request_rec にまつわる諸行(r->filename を決定したりアクセス認可をしたり)をおこなうフェーズです。
サブリクエスト
サブリクエストの発行については下記の2つのステップを「基本的に」経ることになります。
- サブリクエストの作成(lookup)
- サブリクエストの実行(via
ap_run_sub_req())
サブリクエストの生成
サブリクエストを生成する関数には ap_sub_req_lookup_uri(), ap_sub_req_lookup_file(), ap_sub_req_lookup_dirent(), ap_sub_req_method_uri() の4種がありますが,基本的な構造はどれも一緒です。
共通部分を抜き出すと下記のようなロジックになっています。
request_rec * ap_sub_req_lookup_hogehoge(r, next_filter)
{
request_rec *rnew;
rnew = make_sub_request(r, next_filter);
ap_parse_uri(rnew, new_uri);
if (ap_is_recursion_limit_exceeded(r)) {
return HTTP_INTERNAL_SERVER_ERROR;
}
ap_process_request_internal(rnew);
return rnew;
}
つまり,
-
make_sub_request()でサブリクエスト用request_rec構造体を初期化する -
ap_parse_uri()で URI のパースをおこなう -
ap_is_recursion_limit_exceeded()でサブリクエスト・内部リダイレクト回数が所定よりオーバーしていないか検査する -
ap_process_request_internal()でrequest_recの整備をおこなう
というフローになっています。
なお ap_sub_req_method_uri() や ap_sub_req_lookup_uri() の場合,ap_process_request_internal() する前に ap_RUN_quick_handler() してますが,まぁあまり気にしなくてよいでしょう。
make_sub_request() の実装
サブリクエスト用 request_rec 構造体を生成する make_sub_request() in server/request.c の実装(静的関数ですが)は下記のようになっています。
static request_rec *make_sub_request(r, next_filter) { request_rec *rnew; /* サブリクエスト用サブプールの作成 */ apr_pool_create(&rrp, r->pool); rnew = apr_pcalloc(rrp, sizeof(request_rec)); /* * …… rnew の整備 …… */ ap_set_sub_req_protocol(rnew, r); ap_RUN_create_request(rnew); return rnew; }
サブメモリプールを作成しているところがポイントで,あとは request_rec 構造体を用意して初期化しているだけです。また ap_HOOK_create_request が呼び出されますので,メインリクエストであろうとサブリクエストであろうとリクエスト固有の情報の初期化に使うことができます。
上記で呼び出している ap_set_sub_req_protocol() に限っては server/protocol.c に実装があります。
ap_set_sub_req_protocol(request_rec *rnew, const request_rec *r) { /* * …… rnew の雑多な環境整備 …… */ rnew->main = (request_rec *) r; }
重要なところはとくにないのですが,request_rec の main フィールドに生成元の request_rec を設定しているところがちょっとポイントです。
サブリクエストの実行
サブリクエストを実行する ap_run_sub_req() の実装は下記のようになります。
int ap_run_sub_req(r) { if (!(r->filename && r->finfo.filetype)) { ap_run_quick_handler(r, 0); } if (! OK) { ap_invoke_handler(r); } ap_finalize_sub_req_protocol(r); }
こちらがわで response handler を実行していることがわかります。
以上をまとめると
- サブリクエストの生成……リクエスト構造体の整備フェーズの hook が呼び出される
- サブリクエストの実行……レスポンスハンドラが呼び出される
ということになります。
つまり,サブリクエストの出力内容(=レスポンスハンドラの役割)が必要ないのであれば,サブリクエストの生成だけおこなっても問題はありません。じっさい添付モジュールでも,サブリクエストの lookup のみおこない,その結果(r->filename 等)をあれこれするというロジックがいくつかみうけられます。これが一番最初に「2つのステップを『基本的に』経る」と書いた理由です。
さらにまとめると,
- レスポンスに他のリクエストの処理結果を挿入したい場合,サブリクエストの lookup と
ap_sub_req_run()を組み合わせて用いる - 他の URI のリソースの情報(実体ファイルやアクセス権など)を知りたい場合,サブリクエストの lookup のみおこなう
- 他のリソースの取得を試みて成功した場合にそのリソースを返す場合,サブリクエストの lookup をおこなってその結果に
ap_internal_fast_redirect()をおこなう(後述します)
のような適用場面があるということになります。
内部リダイレクトについて
サブリクエストの lookup には数多くの API が存在しましたが,内部リダイレクトに用いる API は基本的に ap_internal_redirect() と ap_internal_redirect_handler() の2つのみです。それぞれの実装をみていきます。
まず ap_internal_redirect() は
ap_internal_redirect(new_uri, r)
{
request_rec *new = internal_internal_redirect(new_uri, r);
ap_RUN_quick_handler(new, 0); /* Not a look-up request */
if (DECLINED) {
ap_process_request_internal(new);
if (OK) {
ap_invoke_handler(new);
}
}
if (OK) {
ap_finalize_request_protocol(new);
}
else {
ap_die();
}
}
のようになっています。
ap_intenal_redirect_handler() は
ap_internal_redirect_handler(new_uri, r)
{
request_rec *new = internal_internal_redirect(new_uri, r);
if (r->handler)
ap_set_content_type(new, r->content_type);
ap_process_request_internal(new);
if (OK) {
ap_invoke_handler(new);
if (! OK) {
ap_die();
return;
}
ap_finalize_request_protocol(new);
}
else {
ap_die();
}
}
のようになっています。
細部に違いはあるものの,大まかなフローとしては,
-
internal_internal_redirect()でリダイレクト用request_recの用意 -
ap_process_request_internal()でrequest_recの整備 -
ap_invoke_handler()でレスポンスハンドラの呼び出し
のようになっています。
サブリクエストと異なり,これらの関数でレスポンスハンドラの呼び出しまでおこなっています。
internal_internal_redirect() の実装
両者で使われている,内部リダイレクト用 request_rec 構造体の生成用関数 internal_internal_redirect() in modules/http/http_request.c の実装(静的関数ですが)は下記のようになります。
static request_rec *internal_internal_redirect(new_uri, r) { request_rec *new; if (ap_is_recursion_limit_exceeded(r)) { ap_die(HTTP_INTERNAL_SERVER_ERROR, r); return NULL; } new = (request_rec *) apr_pcalloc(r->pool, sizeof(request_rec)); /* * …… new の整備 (1) …… */ new->prev = r; r->next = new; ap_RUN_create_request(new); /* * …… new の整備 (2) …… */ ap_RUN_post_read_request(new); return new; }
フローは異なりますが,make_sub_request() と似たような感じです。ただ,ap_is_recursion_limit_exceeded() をこの関数サイドで呼んでいるところと,サブリクエストまわりでは呼ばれていない*1 ap_HOOK_post_read_request フックを呼び出しているところが異なります。
また,
new->prev = r;
r->next = new;
のように,内部リダイレクト用 request_rec 構造体の prev フィールドに,呼び出し元の request_rec を設定し,呼び出し元の next フィールドに内部リダイレクト用 request_rec を設定しています。
ちなみに本筋と関係ないですが,response handler 以前の hook で内部リダイレクト処理をおこなうとおかしな挙動(クライアントからみてレスポンスが終了しない)を示します。これは Apache 1.3 API notes - Apache HTTP Server にも記述されていますが,実際にわたしも経験しました*2。このように response handler 以前に内部リダイレクトをおこないたい場合,request_rec のどこか(notes とか)にリダイレクト先を保存しておいて,それ用のカスタムレスポンスハンドラを登録しておく,というのが定石になります。mod_rewrite でもそのようになっていました。
おまけ: ap_internal_fast_redirect() について
internal redirect 系で ap_internal_fast_redirect() というのもあるのですが,こいつの実装はこんな感じです。
ap_internal_fast_redirect(rr, r)
{
/* memory pool の統合 */
apr_pool_join(r->pool, rr->pool);
/* サブリクエスト構造体をメインリクエスト構造体にコピー */
copy rr => r;
}
ここからとくにコピーされたリクエストをどうこう,というのはしていません。なので response handler hook で仕掛けるにはよろしくない感じ。(ap_process_request_internal() で呼び出される)request_rec 整備系 hook から呼び出すべきでしょう。
添付モジュールでの使われどころは,たとえば mod_negotiation で,各言語用のリソースに対して sub request の lookup を行い,(ap_run_sub_req() は行わず)適切なリソースがあったらそいつに ap_internal_fast_redirect() するなど。また mod_dir で,DirectoryIndex の各ファイルに対して sub request の lookup を行い以下略とか。
つまり
- sub request を発行する
- その sub request がよさげだなと思ったら
ap_internal_fast_redirect()でそれをメインリクエストに昇格 - あとは通常のリクエスト処理にゆだねる
な手順で使われています。
request_rec の next, prev, main
いままでちらほらとでてきましたが request_rec 構造体には next, prev, main というフィールドが存在します。
struct request_rec { /* ...... snip snip snip ...... */ /** Pointer to the redirected request if this is an external redirect */ request_rec *next; /** Pointer to the previous request if this is an internal redirect */ request_rec *prev; /** Pointer to the main request if this is a sub-request * (see http_request.h) */ request_rec *main; /* ...... snip snip snip ...... */ };
サブリクエストに関する部分を図示すると以下のようになります。
root じゃなくて main でした。暇ができたら図を直します。
内部リダイレクトに関する部分は以下になります。
わざわざ図示するほどのことでもなかったですが,
- 自分がサブリクエストとして呼ばれている場合
r->mainがNULL以外になる - 自分が内部リダイレクト先として呼ばれている場合
r->prevがNULL以外になる - おのおの親の親(や兄弟の兄弟)のように順々にたどっていける
- これを用いてリクエストのループが発生しているかどうか検出可能
ことがわかります。
2008-11-21
ap_sub_req_lookup_file() の仕様を勘違いしていた
translate_name に hook をかけてそこで ap_sub_req_lookup_file() したところ,セグフォってしまいました。
んーなんでだろうと思って CoreDumpDirectory を指定して core を解析したら ap_make_dirstr_parent(rnew->pool, r->filename) を呼んでるとこで落ちてました。
translate_name hook が呼ばれた時点では r->filename は設定されていない*1のですが,その r->filename(NULL になってる)を元に親ディレクトリを算出しようとして不正アクセスが発生した模様。
とりあえず呼び出し前に
r->filename = ""; r->canonical_filename = "";
したら大丈夫になりました。
まあつまり ap_sub_req_lookup_file() の引数って現在のリクエスト対象のファイルからの「相対パス」で指定するってことだったんですね。絶対パス指定しておけばいいだろうと思ってたのが間違いでした。この関数を利用している他のモジュールの呼び出し箇所を洗い出して勉強しとかなきゃ。
なお,上記対処法でロジック的に問題ないことは一応追ってあります。
*1:それを設定するのが translate_name hook の役目ですからね。
2008-11-18
複数のテストサーバをリバースプロキシで集約 (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 からリクエストの処理フェーズ順を書き出してみます。
- Translate Name フェーズ
- Map To Storage フェーズ
- Header Parser フェーズ
- Access Checker フェーズ(access check by IP address, etc)
- Check User Id フェーズ (authentication)
- Auth Checker フェーズ (authorization)
- Type Checker フェーズ
- Fixups フェーズ
なぜ directory context で mod_rewrite が設定されている場合に Fixups フェーズでフックしているのかというと,httpd サーバは
- Map To Storage フェーズにおいて
core_map_to_storage(r)(inserver/core.c) が呼ばれる -
core_map_to_storage(r)内でap_directory_walk(r)(inserver/request.c) が呼ばれる -
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 の書き換えを行っています(この場合,サブリクエストを発行する必要がない)。このあたりのメカニズムについては 404 Not Found に詳しく書いてあります。
と長々と書いてきましたが,mod_proxy_mapper ではパフォーマンスは重視しないということと,mod_proxy に移譲する場合は URI の書き換え後にリクエストフックチェインを呼び出す必要がないということで,Fixups フェーズのみにしかけてあります。まあつまりなんにせよ mod_proxy に処理を移譲する場合,mod_rewrite がおこなっているほど複雑なことをする必要はないということです。
サブリクエストについて
mod_proxy へのマッパ一般とはあまり関係のない話です。
標準モジュールでサブリクエストが使われている場面は,mod_include(と mod_cgi*2)です。#include virtual=”...” や #exec cgi=”...” を実現するために使われています。
また,mod_autoindex でも HeaderName と 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; }
のように「出力をすべて捨ててしまう」フィルタを登録し,このフィルタをサブリクエストの出力先として設定しています。*3本来出力フィルタは
return ap_pass_brigade(f->next, bb);
のように次のフィルタチェインに処理を渡すべきなのですが,このようにあえて呼び出さないことで null 化しています。あんまりほめられたマナーではないですね。
ちなみに,サブリクエストの処理内では request_rec->protocol は "INCLUDED" になります。すなわち CGI 環境変数として SERVER_PROTOCOL が INCLUDED になります。
おわりに
proxy サーバのバランサモジュールとして mod_proxy_balancer というのがあります。バックエンドサーバの選定アルゴリズムを取り替えることのできる proxylbmethod provider というメカニズム*4もあるのですが,自分ごのみの柔軟なマッピングをおこないたい場合,mod_rewrite と mod_proxy_balancer に頼らず,自力で proxy server のマッピングモジュールを書いてみるのもいいのではないでしょうか。
*1:それ以外のフックもしかけていますが,リクエスト URI の書き換えにかかわるフックとして両者のフェーズを利用しています。
*2:mod_cgi で使われているのは mod_include の #exec cgi の部分をうけもっているからです。
*3:mod_include や mod_autoindex を見ると特に特別な入力フィルタを設定していないのですが,念のため入力フィルタも「入力を無視する」フィルタを登録しています。レスポンスハンドラの場合,最初に自分が入力を読み取ってしまうからそのようになっているのかもしれません。未調査。
*4:mod_proxy_balancerに独自振り分けロジックを追加できる気がする | 眠る開発屋blog, Apache の provider 機構 - 他モジュールに移譲するしくみ - daily dayflower 参照
2008-11-16
複数のテストサーバをリバースプロキシで集約 (3)
複数のテストサーバをリバースプロキシで集約 (1) - daily dayflower と 複数のテストサーバをリバースプロキシで集約 (2) - daily dayflower の続きです。
mod_rewrite の RewriteMap を使ってごにょごにょしましたが,なんともまどろっこしかったです。そもそも URI の書き換えに癖のある DSL を使う mod_rewrite を使わなきゃいけないということ自体がアレです。もっと手になじんだプログラミング言語で書ければロジックもすっきりするのに!
というわけでモジュールを書いてみました( mod_proxy_mapper.c - daily dayflower)。
プロキシ専用ですが,サブリクエストを使ってプロキシ先を選定するモジュールです。
- サブリクエストを使っているので,Apache でサポートしている言語ハンドラ……CGI*1 なり PHP*2 なり mod_perl*3 なりを使ってかける
- リクエストのたびにサブリクエストを発行するのでパフォーマンスゲインは相当のもの
なんて toy module ですが,まぁ後者は(プロダクション環境でなければ)そんなに気になるほどのことでもないです。
2008-11-18 追記
cons を強調して書いたので一応 pros も書いておくと,
2008-11-18 追記おわり
LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_http_module modules/mod_proxy_http.so LoadModule proxy_mapper_module modules/mod_proxy_mapper.so ProxyMapper file:///var/www/main/proxymap/mapper.cgi <Directory /var/www/main/proxymap/> Options ExecCGI AddHandler cgi-script cgi # AddHandler persitentperl-script cgi Order allow,deny Allow from all ProxyMapper none </Directory>
のように設定します。
ProxyMapper という設定子でプロキシマッピングのためのファイルパス/URI パスを指定します。URI パスも指定できますが,サブリクエストなので外部サーバに投げることはできません。ので Fully Qualified URI ではなくパスを指定してください。そもそもモジュールの性質上,ファイルパス(file:// を先頭に書くとファイルパスとみなします)を指定したほうがよいです。
またトップで ProxyMapper を指定していますが,一応 <Location> や .htaccess レベルでも設定できます。が,やはり性格上,トップで指定することになるでしょう。
mapper.cgi の例です。
#!/usr/bin/perl use strict; use warnings; our $CONFIG = <<'END_YAML'; --- outer1.example.com: /path1: http://inner-a.example.com/path1 /path2: http://inner-b.example.com/path2 /: NULL outer2.example.com: /path3: http://inner-b.example.com/path3 /: NULL END_YAML use YAML; my $server = $ENV{HTTP_HOST}; my $path = $ENV{REQUEST_URI}; #my $mapping = YAML::LoadFile('proxy.yaml'); my $mapping = YAML::Load($CONFIG); my $result; if (exists $mapping->{$server}) { my $len = 0; foreach my $key (%{$mapping->{$server}}) { next if $len > length $key; if (substr($path, 0, length $key) eq $key) { $result = $mapping->{$server}->{$key}; $result .= substr $path, length $key; $len = length $key; } } } if (defined $result) { print "Location: ${result}\n\n"; } else { print "Status: 404 Not Found\n\n"; }
Location ヘッダに Fully Qualified URI を指定すると mod_proxy に移譲します。ステータスコードを 404 にすると,元のリクエストのステータスコードも 404 になります。
おわりに
(プロキシ)マッピングをよりロジカルに書きたいというだけなら mod_lua で LuaHandlerFixup を書くとかすればよかった気がしてきたー。外部プログラムのためにサブリクエストを発行するのと,組み込まれた Lua を実行するのとどっちがパフォーマンスがいいんだろう。
mod_proxy_mapper.c
/* ** mod_proxy_mapper.c */ #include "httpd.h" #include "http_config.h" #include "http_log.h" #include "http_protocol.h" #include "http_request.h" #include "http_core.h" #include "ap_config.h" #include "apr_strings.h" #if 0 || defined(__GNUC__) /* for syntax highlight */ #define strbegin(S, T) \ ({ size_t _l = strlen(T); \ (strlen(S) >= _l && ! memcmp((S), (T), _l)); }) #else #define strbegin(S, T) \ (strlen(S) >= strlen(T) && ! memcmp((S), (T), strlen(T))) #endif static int proxy_available = 0; typedef enum mapper_type { MAPPER_UNDEFINED, MAPPER_NONE, MAPPER_FILE, MAPPER_URI, } mapper_type; typedef struct proxy_mapper_conf { mapper_type type; const char *mapper; } proxy_mapper_conf; static void * create_dir_config(apr_pool_t *p, char *dir) { proxy_mapper_conf *conf = (proxy_mapper_conf *) apr_pcalloc(p, sizeof(proxy_mapper_conf)); conf->type = MAPPER_UNDEFINED; conf->mapper = NULL; return conf; } static const char * set_proxy_mapper(cmd_parms *cmd, void *mconfig, const char *arg) { proxy_mapper_conf *conf = (proxy_mapper_conf *) mconfig; int len; len = strlen(arg); if (! strcasecmp(arg, "none")) { conf->type = MAPPER_NONE; conf->mapper = NULL; } else if (len >= 7 && ! memcmp(arg, "file://", 7)) { if (len == 7) return "file part of ProxyMapper is not specified."; /* @@TODO: check existence */ conf->type = MAPPER_FILE; conf->mapper = apr_pstrdup(cmd->pool, arg + 7); } else { conf->type = MAPPER_URI; conf->mapper = apr_pstrdup(cmd->pool, arg); } return NULL; } static const command_rec proxy_mapper_cmds[] = { AP_INIT_TAKE1("ProxyMapper", set_proxy_mapper, NULL, ACCESS_CONF | RSRC_CONF | OR_ALL, "URI or path of mapper script"), { NULL } }; static ap_filter_rec_t *null_input_filter_handle; static ap_filter_rec_t *null_output_filter_handle; static apr_status_t null_input_filter(ap_filter_t *f, apr_bucket_brigade *bb, ap_input_mode_t mode, apr_read_type_e block, apr_off_t readbytes) { apr_bucket *b; switch (mode) { case AP_MODE_INIT: return APR_SUCCESS; case AP_MODE_READBYTES: case AP_MODE_SPECULATIVE: b = apr_bucket_eos_create(f->c->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, b); return APR_SUCCESS; default: return APR_EOF; } } static apr_status_t null_output_filter(ap_filter_t *f, apr_bucket_brigade *bb) { apr_brigade_destroy(bb); /* Yes, I know what I'm doing. This filter behaves like the bottom of output filters. So I do not call ap_pass_brigade() intentionally. */ return APR_SUCCESS; } static apr_status_t set_null_input_filter(request_rec *r) { ap_filter_t *f = apr_pcalloc(r->pool, sizeof(ap_filter_t)); /* @@TODO: assert f */ f->frec = null_input_filter_handle; f->r = r; f->c = r->connection; f->next = NULL; r->input_filters = f; r->proto_input_filters = f; return APR_SUCCESS; } static apr_status_t set_null_output_filter(request_rec *r) { ap_filter_t *f = apr_pcalloc(r->pool, sizeof(ap_filter_t)); /* @@TODO: assert f */ f->frec = null_output_filter_handle; f->r = r; f->c = r->connection; f->next = NULL; r->output_filters = f; r->proto_output_filters = f; return APR_SUCCESS; } module AP_MODULE_DECLARE_DATA proxy_mapper_module; static int hook_fixups(request_rec *r) { proxy_mapper_conf *conf; request_rec *subreq = NULL; const char *target = NULL; /* * no REQUEST_URI, it seems to be subreq-file; fast exit. */ if (! r->uri || ! *(r->uri)) return DECLINED; conf = (proxy_mapper_conf *) ap_get_module_config(r->per_dir_config, &proxy_mapper_module); if (! conf) return DECLINED; if (conf->type == MAPPER_NONE || conf->type == MAPPER_UNDEFINED) return DECLINED; for (subreq = r->main; subreq; subreq = subreq->main) { if (! strcmp(subreq->uri, r->uri)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Proxy Mapper loop detected on URI %s.", r->uri); return HTTP_FORBIDDEN; } } ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Proxy Mapper start on URI %s.", r->uri); switch (conf->type) { case MAPPER_FILE: if (! strcmp(r->canonical_filename, conf->mapper)) { ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "Proxy Mapper itself detected on URI %s; DECLINED.", r->uri); return DECLINED; } subreq = ap_sub_req_lookup_file(conf->mapper, r, NULL); break; case MAPPER_URI: if (! strcmp(r->uri, conf->mapper)) { ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "Proxy Mapper itself detected on URI %s; DECLINED.", r->uri); return DECLINED; } subreq = ap_sub_req_lookup_uri(conf->mapper, r, NULL); break; default: ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Unsupported proxy mapper type (%d) for URI %s.", conf->type, r->uri); return HTTP_FORBIDDEN; } if (subreq) { apr_status_t status; apr_table_t *outhdr; const char *value; /* subreq might overwrite headers_in */ subreq->headers_in = apr_table_copy(r->pool, r->headers_in); /* forbid use of request and response for subreq */ set_null_input_filter(subreq); set_null_output_filter(subreq); /* set Content-Length to 0 */ if (apr_table_get(subreq->headers_in, "Content-Length")) apr_table_set(subreq->headers_in, "Content-Length", apr_pstrdup(r->pool, "0")); status = ap_run_sub_req(subreq); if (status == 0 && subreq->status != 0) status = subreq->status; ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "subreq status = %d on URI %s.", status, r->uri); if (ap_is_HTTP_REDIRECT(status)) { /* Redirect status is permitted. */ status = OK; } else if (ap_is_HTTP_CLIENT_ERROR(status)) { /* Client Error (eg. NOT FOUND) => as is status */ return status; } if (status != OK && status != HTTP_OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Sub-request of proxy mapper failed: status = %d" " on URI %s.", status, r->uri); return HTTP_FORBIDDEN; /* safety */ } outhdr = apr_table_overlay(r->pool, subreq->err_headers_out, subreq->headers_out); do { value = apr_table_get(outhdr, "Location"); if (value) { if (strbegin(value, "proxy:")) { target = apr_pstrdup(r->pool, value); } else if (strbegin(value, "http://") || strbegin(value, "https://")) { target = apr_pstrcat(r->pool, "proxy:", value, NULL); } else { /* UNSUPPORTED FEATURE */ ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "Unsupported redirection to '%s'" " on URI %s.", value, r->uri); } } } while (0); } else { if (conf->type == MAPPER_FILE || conf->type == MAPPER_URI) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Lookup for proxy mapper sub-request failed" " on URI %s.", r->uri); return HTTP_FORBIDDEN; } } if (! target) { /* not for proxy remap */ return DECLINED; } ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "Proxy Mapper mapped URI %s to %s.", r->uri, target); /* req->filename construction */ r->filename = apr_pstrdup(r->pool, target); r->proxyreq = PROXYREQ_REVERSE; r->handler = "proxy-server"; return OK; } static int hook_post_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) { proxy_available = (ap_find_linked_module("mod_proxy.c") != NULL); if (! proxy_available) { ap_log_error(APLOG_MARK, APLOG_CRIT, APR_EGENERAL, s, "mod_proxy_mapper: you must enable mod_proxy."); return HTTP_INTERNAL_SERVER_ERROR; } return OK; } static void register_hooks(apr_pool_t *p) { /* fixup after mod_proxy, so that the proxied url will not * escape accidentally by mod_proxy's fixup. */ static const char * const asz_pre[] = { "mod_proxy.c", NULL }; null_input_filter_handle = ap_register_input_filter("NULL_INPUT_FILTER", null_input_filter, NULL, AP_FTYPE_CONTENT_SET); null_output_filter_handle = ap_register_output_filter("NULL_OUTPUT_FILTER", null_output_filter, NULL, AP_FTYPE_CONTENT_SET); ap_hook_post_config(hook_post_config, NULL, NULL, APR_HOOK_LAST); ap_hook_fixups(hook_fixups, asz_pre, NULL, APR_HOOK_FIRST); } /* Dispatch list for API hooks */ module AP_MODULE_DECLARE_DATA proxy_mapper_module = { STANDARD20_MODULE_STUFF, create_dir_config, /* create per-dir config structures */ NULL, /* merge per-dir config structures */ NULL, /* create per-server config structures */ NULL, /* merge per-server config structures */ proxy_mapper_cmds, /* table of config file commands */ register_hooks /* register hooks */ };
いつか書く
2008-11-15
複数のテストサーバをリバースプロキシで集約 (2)
複数のテストサーバをリバースプロキシで集約 (1) - daily dayflower の続きです。
前回 mod_proxy と mod_rewrite を組み合わせてリバースプロキシ環境を構成しました。が,そのままだとプロキシ先のサーバが増減するたびに設定ファイルを書き換えて httpd を再起動しなくてはなりません。
再起動することなく動的にマッピング先を変えるにはどうすればよいのか。何らかの手段でマッピング情報を「外在化」させる必要があります。
動的にマッピング先を変える
前回 RewriteMap というのを持ち出してきました。mod_rewrite に内蔵された内部フィルタ(int:tolower)を用いましたが,実はファイル(プレーンテキストファイル,DBM ファイル)やプログラムを値フィルタとして用いることもできます。
当初このファイルによる値フィルタを使えないかなと思ったんですが,いい方法が思いつきませんでした*1。なので,プログラムによる値フィルタを使ってみます。
RewriteMap lowercase int:tolower RewriteMap proxymapper prg:/var/www/proxymapper.pl RewriteLock /tmp/mapper.lock RewriteCond ${lowercase:%{SERVER_NAME}}%{REQUEST_URI} ^(.*)$ RewriteCond ${proxymapper:%1} ^(.+)$ RewriteRule ^/ %1 [proxy]
という設定です。
まず
RewriteCond ${lowercase:%{SERVER_NAME}}%{REQUEST_URI} ^(.*)$
というところで SERVER_NAME(を小文字化したもの)と REQUEST_URI を結合しています。それを ^(.*)$ でマッチングしているので,この RewriteCond は必ず成功します。そして括弧でくくっているので,マッチング対象が %1 という変数に入ります。
次に
RewriteCond ${proxymapper:%1} ^(.+)$
というところで,proxymapper というフィルタに先ほどのマッチング結果を渡しています。このフィルタの出力を ^(.+)$ でマッチングしているので,この RewriteCond もほぼ必ず成功します。
最後に
RewriteRule ^/ %1 [proxy]
というところでルールの適用を行います。すべての URL を先ほどのマッチング結果=proxymapper によるフィルタリング結果に置換します。そして [proxy] フラグによって mod_proxy に処理を移譲します。
proxymapper というのは
RewriteMap proxymapper prg:/var/www/proxymapper.pl
で指定したマッパです。先頭に prg: とつけると,指定されたファイルをプログラムとみなしてマッピングを行います。
ちなみにこのプログラムは Apache の起動時,もっというと設定時に起動されます。つまり fork() する前なので 1 インスタンスのみ起動されます。この 1 インスタンスのプログラムを各リクエストで共有するのでフィルタリングの入出力を直列化するために
RewriteLock /tmp/mapper.lock
のようにロック用ファイルが必要となります*2。
proxymapper.pl はたとえば下記のようなスクリプトです。
#!/usr/bin/perl use strict; use warnings; $| = 1; while (<STDIN>) { chomp; my ($server, $path) = split '/', $_, 2; if (0) {} elsif ($server eq 'outer1.example.com') { if (0) {} elsif ($path =~ m{ \A \Q/path1\E }xms) { print "http://inner-a.example.com${path}", "\n"; next; } elsif ($path =~ m{ \A \Q/path2\E }xms) { print "http://inner-b.example.com${path}", "\n"; next; } } elsif ($server eq 'outer2.example.com') { if (0) {} elsif ($path =~ m{ \A \Q/path3\E }xms) { print "http://inner-b.example.com${path}", "\n"; next; } } print "NULL", "\n"; }
標準入力から一行読み込んで,マッピング結果を一行出力する,というのを延々と繰り返すプログラムです。これが prg: タイプの RewriteMap プログラムのインタフェースです。NULL を返すとマッピング失敗とみなされます。
RewriteCond ${lowercase:%{SERVER_NAME}}%{REQUEST_URI} ^(.*)$
にて SERVER_NAME と REQUEST_URI を結合していたので,split() によって分割して該当するマッピング先を決定しています。それだけのスクリプトです。
フィルタリングプログラムが落ちたらどうしよう
かくして「マッピング情報」を「外在化」させることに成功しました。しかし,このマッピング用スクリプトは Perl で書いており,(まあないとは思いますが)いつか落ちてしまうかもしれません。とても不安です。
これに対処するため,フィルタリングプログラムをできるだけ「軽く」書いて,実際のマッピング作業をさらに外在化させてみましょう。どういうことかというと,
-
prg:で指定されるフィルタリングプログラムは UNIX ドメインソケットに接続してフィルタリング情報を投げる - UNIX ドメインソケットで待ち受けして実フィルタリング作業を行うプログラムを立ち上げる
のような2層構造にするということです。これで後者が落ちたとしても後者のみ再度立ち上げればよいことになります。
前者のコード例は LIMILIC - OpenIDでテキストを共有できるWebサービス におきました。ソケットプログラミングするのは久しぶりなのでいろいろチョンボがあるかもしれません。
後者の待ち受けサーバ側ですが,たとえば下記のようなコードになります。
#!/usr/bin/perl use strict; use warnings; our $SOCKET_PATH = '/tmp/proxymapper'; our $CONFIG = <<'END_YAML'; --- outer1.example.com: /path1: http://inner-a.example.com/path1 /path2: http://inner-b.example.com/path2 /: NULL outer2.example.com: /path3: http://inner-b.example.com/path3 /: NULL END_YAML use IO::Socket::UNIX; -e $SOCKET_PATH && unlink $SOCKET_PATH; my $listener = IO::Socket::UNIX->new( Type => SOCK_STREAM, Local => $SOCKET_PATH, Listen => SOMAXCONN, ) or die $!; $SIG{PIPE} = sub { print {*STDERR} "SIGPIPE\n"; }; while (1) { my $conn = $listener->accept() or die $!; $conn->autoflush(1); print {*STDERR} "connection accepted\n"; while (1) { my $line = $conn->getline(); last if ! $line; print {*STDERR} "received: ", $line; my $result = 'NULL'; ######################################## chomp $line; my ($server, $path) = split '/', $line, 2; if (defined $server && defined $path) { $path = '/' . $path; use YAML; #my $mapping = YAML::LoadFile('proxy.yaml'); my $mapping = YAML::Load($CONFIG); if (exists $mapping->{$server}) { my $len = 0; foreach my $key (%{$mapping->{$server}}) { next if $len > length $key; if (substr($path, 0, length $key) eq $key) { $result = $mapping->{$server}->{$key}; $result .= substr $path, length $key; $len = length $key; } } } } ######################################## $conn->print($result, "\n"); print {*STDERR} "sent: ", $result, "\n"; } print {*STDERR} "connection closed\n"; $conn->close(); }
ここまで自力で書く必要もない気がしますが,IO::Socket でゴリゴリ書いています。つーても多重化してないのでここがすごく律速になってしまうでしょう。あくまで参考ということで。
ここではマッピング情報をコード内部に埋め込んでいますが,外部ファイルから読み込むようにすれば,晴れて httpd の再起動を行う必要のないリバースプロキシ環境ができあがったことになります。
とはいえ,まだまどろっこしいですよね。ということで続きます。→ 複数のテストサーバをリバースプロキシで集約 (3) - daily dayflower
2008-11-14
複数のテストサーバをリバースプロキシで集約 (1)
各所でテストサーバが立ち上がっていて,これらは一台のリバースプロキシの背後にいます。普通に mod_proxy でやっているのですが,テストサーバを増やすたびに設定ファイルを書き換えて httpd を再起動,としなくちゃいけないのが煩わしい。
で,リバースプロキシのマッピング先を動的に構成したいなと思ったわけです。
以降では下記の例を使います。
| アクセス URL | リダイレクト先 |
|---|---|
| http://outer1.example.com/path1/ | http://inner-a.example.com/path1/ |
| http://outer1.example.com/path2/ | http://inner-b.example.com/path2/ |
| http://outer2.example.com/path3/ | http://inner-b.example.com/path3/ |
おことわり
今後のお話ででてくる設定を適用すると,mod_proxy を素で使った場合よりパフォーマンスがだいぶんと落ちます。なのでプロダクション環境にはおすすめできません。あくまでテストサーバ環境ということで。
素直に mod_proxy だけで書いてみる
まずは mod_proxy を使った設定を書いてみます。ProxyPass に retry*1 を設定していないのはサンプルということでご容赦を。
LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_http_module modules/mod_proxy_http.so NameVirtualHost * ProxyRequests Off ProxyPreserveHost On <VirtualHost *:*> ServerName outer1.example.com DocumentRoot /var/www/empty # http://outer1.example.com/path1/ => http://inner-a.example.com/path1/ ProxyPass /path1 http://inner-a.example.com/path1 # http://outer1.example.com/path2/ => http://inner-b.example.com/path2/ ProxyPass /path2 http://inner-b.example.com/path2 </VirtualHost> <VirtualHost *:*> ServerName outer2.example.com DocumentRoot /var/www/empty # http://outer1.example.com/path3/ => http://inner-b.example.com/path3/ ProxyPass /path3 http://inner-b.example.com/path3 </VirtualHost>
普通に書くとだいたいこんな感じになるのではないでしょうか。んで,ドメインが増えるたびに <VirtualHost> を追加し,マッピング先が増えるたびに ProxyPass を追加していく,ということになります。見通しは悪くないのですが,記述する分量はずいぶんと多いですね。
ヘッダの調整について
ProxyPassReverse, ProxyPassReverseCookieDomain, ProxyPassReverseCookiePath あたりの話です。
なんでこんなのが用意されてるか。
さきほどの例で,inner-b.example.com にアクセスがあった状況について考えてみます。
GET /path2/hoge Host: outer1.example.com
のようなリクエストにたいして /path2/foo にリダイレクトする場合。
Host ヘッダを考慮して
HTTP/1.1 303 See Other Location: http://outer1.example.com/path2/foo
のようにレスポンスを返してくれればよいですが,
HTTP/1.1 303 See Other Location: http://inner-b.example.com/path2/foo
のように自分のホスト名をドメイン名としてしまう「頑固」なウェブアプリケーションも存在します*2。
こんなとき,mod_proxy 側の設定で
<VirtualHost *:*> ServerName outer1.example.com ProxyPassReverse /path2 http://inner-b.example.com/path2 </VirtualHost>
のようになっていると
HTTP/1.1 303 See Other Location: http://outer1.example.com/path2/foo
mod_proxy がレスポンスヘッダを適宜書き換えてくれるというわけです。
とはいうものの,レスポンス HTML(のたとえば <a> タグとか)に含まれる (Fully Qualified) URL まで書き換えてくれるわけではありません。逆にいうと,出力される HTML の内容が Host ヘッダによってきちんと変わるウェブアプリケーションであれば,ヘッダも同様にきちんと書き換えてくれるはずでしょう。なので実はいちいち設定しなくてもうまく動くことがほとんどです。
ちなみに HTML の中身まで書き換えてくれる mod_proxy_html というモジュールもあります。普通はウェブアプリ(サーバ)側を書き換えるべきだと思いますが,既存のウェブアプリ環境を書き換えることなく外部に露出したいなどどうしてもということであれば,こういうのを使ってもいいかもしれません。
mod_rewrite とからめて使ってみる
mod_rewrite を使うと,リクエスト(ヘッダ)の内容に応じて URI を書き換えたり Proxy 先を選定することが可能になります。これを使った設定は下記のようになります。
LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_http_module modules/mod_proxy_http.so LoadModule rewrite_module modules/mod_rewrite.so UseCanonicalName Off ProxyRequests Off ProxyPreserveHost On RewriteEngine On RewriteMap lowercase int:tolower # http://outer1.example.com/path1/ => http://inner-a.example.com/path1/ RewriteCond ${lowercase:%{SERVER_NAME}} =outer1.example.com RewriteCond %{REQUEST_URI} /path1 RewriteRule ^/(.*)$ http://inner-a.example.com/$1 [proxy] # http://outer1.example.com/path2/ => http://inner-b.example.com/path2/ RewriteCond ${lowercase:%{SERVER_NAME}} =outer1.example.com RewriteCond %{REQUEST_URI} /path2 RewriteRule ^/(.*)$ http://inner-b.example.com/$1 [proxy] # http://outer2.example.com/path3/ => http://inner-b.example.com/path3/ RewriteCond ${lowercase:%{SERVER_NAME}} =outer2.example.com RewriteRule ^/(.*)$ http://inner-b.example.com/$1 [proxy]
SERVER_NAME とのマッチングで「=〜」というのが見慣れないかもしれません。普通にセオリー通りに正規表現で書くなら,
RewriteCond ${lowercase:%{SERVER_NAME}} ^outer1\.example\.com$
となりますし,PCRE 表記を用いると
RewriteCond ${lowercase:%{SERVER_NAME}} ^\Qouter1.example.com\E$
のようになるでしょう。このような状況で使える全体 as-is マッチング記法「=」というのがあるということです。というかわたしも今回初めて知ったんですけどね。
また
RewriteMap lowercase int:tolower
というのも見慣れないかも。RewriteMap というのは RewriteCond や RewriteRule で用いられる値にフィルタリングするためのものです。int:tolower というのは,mod_rewrite に内蔵された tolower というマッパで,このマッパを lowercase という名前で用いる,ということになります。
したがって,
RewriteCond ${lowercase:%{SERVER_NAME}} =outer1.example.com
の部分は,SERVER_NAME に int:lower というフィルタをかけて,outer1.example.com に全部一致したとき,という意味になります。
もちろんこのようなフィルタをかけずに
RewriteCond %{SERVER_NAME} =outer1.example.com
だけでもいいことはいいのですが,URL のドメイン名部分は大文字でやってくるかもしれないので,念のために tolower しています。このへんは実は Dynamically configured mass virtual hosting - Apache HTTP Server を参考にしました。
ともあれ,これでマッピングルールが増えるたびに,RewriteCond を2〜3行,RewriteRule を1増やせばよい勘定になります。もとの mod_proxy を使う設定に比べると黒魔術的表記になっている気もしますが。
余談: ログについて
このように <VirtualHost> を使わずに複数ドメインを扱うと,異なるドメインへのリクエストログが同一のファイルに書かれてしまいます。同一のファイルにまとまってしまうことはまあいいんですが,ホスト名部分がロストするのでどのリクエストかわからなくなる弊害があります。
LogFormat "%h %l %u %t \"%m http://%{Host}i%U%q\" %s %b" vcommon CustomLog logs/virtual.log vcommon
のような設定をしてホスト名をログに記載するようにするのがいいかもしれません*3。このような形式のログを既存のログ解析ソフトが受け付けてくれるかわかりませんが。
ここまでの設定では,テストサーバが増減するたびに設定ファイルを書き換えて httpd を再起動しなくてはいけないという状況はかわっていません。続きます。→ 複数のテストサーバをリバースプロキシで集約 (2) - daily dayflower



