daily dayflower

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 ドキュメントによると,

がサポートされているようです。

実際にはどうなのか,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;
}
  1. apr_dbd_open()DB をオープンして
  2. apr_dbd_select() で選択系 SQL を発行し
  3. apr_dbd_get_row() で各行を fetch*2
  4. apr_dbd_get_entry() で各行の各コラムを取得
  5. さいごに 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 NULL on 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

手順としては,

  1. apr_dbd_escape() でエスケープ処理を行って
  2. 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_recpool を利用して),リクエスト終了時にメモリプールを破棄するようにすればいいでしょう。

そうやって考えてみれば,APR-util DBD API の多くの関数で,つどつど apr_pool_t を要求している理由がわかりました。

*1漢(オトコ)のコンピュータ道: Apache mod_dbd設定編 参照

*2apr_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.cap_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
}

おおまかに

  1. request_rec の準備
  2. request_rec の整備 (via ap_process_request_internal())
  3. レスポンスハンドラの実行
  4. ファイナライズ

のような行程を経ることになります。

ちなみに 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つのステップを「基本的に」経ることになります。

  1. サブリクエストの作成(lookup)
  2. サブリクエストの実行(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;
}

つまり,

  1. make_sub_request() でサブリクエスト用 request_rec 構造体を初期化する
  2. ap_parse_uri()URI のパースをおこなう
  3. ap_is_recursion_limit_exceeded() でサブリクエスト・内部リダイレクト回数が所定よりオーバーしていないか検査する
  4. 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_recmain フィールドに生成元の 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();
    }
}

のようになっています。

細部に違いはあるものの,大まかなフローとしては,

  1. internal_internal_redirect()リダイレクトrequest_rec の用意
  2. ap_process_request_internal()request_rec の整備
  3. 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 Version 2.2 にも記述されていますが,実際にわたしも経験しました*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 を行い以下略とか。

つまり

  1. sub request を発行する
  2. その sub request がよさげだなと思ったら ap_internal_fast_redirect() でそれをメインリクエストに昇格
  3. あとは通常のリクエスト処理にゆだねる

な手順で使われています。

request_recnext, 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 ...... */
};

サブリクエストに関する部分を図示すると以下のようになります。

f:id:dayflower:20081125152939p:image

root じゃなくて main でした。暇ができたら図を直します。

内部リダイレクトに関する部分は以下になります。

f:id:dayflower:20081125153013p:image

わざわざ図示するほどのことでもなかったですが,

  • 自分がサブリクエストとして呼ばれている場合 r->mainNULL 以外になる
  • 自分が内部リダイレクト先として呼ばれている場合 r->prevNULL 以外になる
  • おのおの親の親(や兄弟の兄弟)のように順々にたどっていける
    • これを用いてリクエストのループが発生しているかどうか検出可能

ことがわかります。

*1:どこかで呼んでますかね?きちんとフローを追っていないので見逃しがあるかもしれません。

*2:原因についてはきちんと読み込んでいないのでわかりませんでした。

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->filenameNULL になってる)を元に親ディレクトリを算出しようとして不正アクセスが発生した模様。

とりあえず呼び出し前に

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 からリクエストの処理フェーズ順を書き出してみます。

  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 の書き換えを行っています(この場合,サブリクエストを発行する必要がない)。このあたりのメカニズムについては 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 でも HeaderNameReadmeName を処理するために使われています。

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

おわりに

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

*4mod_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 も書いておくと,

  • 自由自在なアルゴリズムでプロキシ先を決定できる
    • たとえばアクセスした日時によりプロキシ先を決定したり,アクセス元 IP address にしたがってプロキシ先を決定したり

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_luaLuaHandlerFixup を書くとかすればよかった気がしてきたー。外部プログラムのためにサブリクエストを発行するのと,組み込まれた 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                      */
};

いつか書く

*1:通常のリクエスト処理がとんでもなく遅くなると思います。

*2:でも PHP って worker MPM サポートしてないんですよね。

*3:mod_perl 使うならこんなの使わずそもそも自分で PerlFixupHandler 書くべきでしょう。

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_NAMEREQUEST_URI を結合していたので,split() によって分割して該当するマッピング先を決定しています。それだけのスクリプトです。

フィルタリングプログラムが落ちたらどうしよう

かくして「マッピング情報」を「外在化」させることに成功しました。しかし,このマッピング用スクリプトは Perl で書いており,(まあないとは思いますが)いつか落ちてしまうかもしれません。とても不安です。

これに対処するため,フィルタリングプログラムをできるだけ「軽く」書いて,実際のマッピング作業をさらに外在化させてみましょう。どういうことかというと,

  • prg: で指定されるフィルタリングプログラムは UNIX ドメインソケットに接続してフィルタリング情報を投げる
  • UNIX ドメインソケットで待ち受けして実フィルタリング作業を行うプログラムを立ち上げる

のような2層構造にするということです。これで後者が落ちたとしても後者のみ再度立ち上げればよいことになります。

前者のコード例は 502 Bad Gateway におきました。ソケットプログラミングするのは久しぶりなのでいろいろチョンボがあるかもしれません。

後者の待ち受けサーバ側ですが,たとえば下記のようなコードになります。

#!/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

*1:単純に外部サーバ名と内部サーバ名を一対一対応させるだけであればファイルによる値フィルタを使うことができます。

*2:いちおうロックパスを指定しなくても Apache に怒られながらも httpd が起動しますが,リクエストが殺到するとおかしなことになる可能性が高いです。

2008-11-14

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

f:id:dayflower:20081028150719p:image:w160:left

各所でテストサーバが立ち上がっていて,これらは一台のリバースプロキシの背後にいます。普通に 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 を使った設定を書いてみます。ProxyPassretry*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 というのは RewriteCondRewriteRule で用いられる値にフィルタリングするためのものです。int:tolower というのは,mod_rewrite に内蔵された tolower というマッパで,このマッパを lowercase という名前で用いる,ということになります。

したがって,

RewriteCond ${lowercase:%{SERVER_NAME}} =outer1.example.com

の部分は,SERVER_NAMEint:lower というフィルタをかけて,outer1.example.com に全部一致したとき,という意味になります。

もちろんこのようなフィルタをかけずに

RewriteCond %{SERVER_NAME} =outer1.example.com

だけでもいいことはいいのですが,URL のドメイン名部分は大文字でやってくるかもしれないので,念のために tolower しています。このへんは実は Dynamically configured mass virtual hosting - Apache HTTP Server Version 2.2 を参考にしました。


ともあれ,これでマッピングルールが増えるたびに,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

*1 ProxyPass には retry=?? を指定しておく - daily dayflower 参照

*2:一部のフレームワークでは設定ファイルでドメイン名を指定するものがあるようです。この場合,内部用に設定しているとリダイレクトの際に内部ドメイン名が露出してしまいます。

*3:SSL についてはまったく無視していますが。