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

複数のテストサーバをリバースプロキシで集約 (1) - daily dayflower の続きです。

前回 mod_proxy と mod_rewrite を組み合わせてリバースプロキシ環境を構成しました。が,そのままだとプロキシ先のサーバが増減するたびに設定ファイルを書き換えて httpd を再起動しなくてはなりません。

再起動することなく動的にマッピング先を変えるにはどうすればよいのか。何らかの手段でマッピング情報を「外在化」させる必要があります。

動的にマッピング先を変える

前回 [http://httpd.apache.org/docs/2.2/mod/mod_rewrite.html#rewritemap:title=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 が起動しますが,リクエストが殺到するとおかしなことになる可能性が高いです。