開発メモ

August 27, 2010

テストケースの実行にあわせて Apache を起動・終了する方法

ウェブアプリケーションやライブラリの結合テストを行う段階になると、実際に Apache を起動してテストを実行したくなります。しかし、そのためにいちいち Apache の設定ファイルを修正して httpd を再起動して、とやっていては面倒です。特に複数のプログラムを同時に開発していると、あっちをテストしたらこっちが動かなくなって… なんてなったりして嫌気がさしてきます。

そこで、テストを実行する際に、環境毎に異なる以下のような問題を吸収しつつ、テスト専用に設定された Apache を自動的に起動終了してくれる Perl モジュール:Test::Httpd::Apache2 を書きました。

  • 環境によって、インストールパスが違う (/usr/local/apache/bin だったり /usr/sbin だったり)
  • 環境によって LoadModule の要不要や、ロードするパスが違う
  • 環境によってプログラム名が違う (Debian 系では httpd ではなく apache2)
  • Apache/2.0 と 2.2 でモジュール名が変わっているケースがある (mod_access と mod_authz_host)

使い方は簡単。テストコードの先頭で、Test::Httpd::Apache2 のインスタンスを生成すると、自動的に TCP の空きポートを探して Apache が起動するので、そのアドレスに HTTP クライアントからアクセスするだけです。起動された Apache は、Test::Httpd::Apache2 のインスタンスが解放されるタイミングで終了され、一時ファイルも自動的に消去されます。

use Test::Httpd::Apache2;

my $httpd = Test::Httpd::Apache2->new(
    required_modules => [ qw(perl) ],  # mod_perl をロード
    custom_conf => << 'EOT',
<Location />
  SetHandler perl-script
  ...
</Location>
EOT
);

my $root_url = "http://" . $httpd->listen . "/"; # 起動した host:port から URL 生成

# あとはテスト
...

そして、テスト本体が Perl のコードである必要はないので、PHP や他の言語のテストを実行するためのラッパーとして使うこともできます。

use Test::Httpd::Apache2;

my $httpd = Test::Httpd::Apache2->new(
    required_modules => [ qw(php5) ],  # mod_php をロード
    custom_conf => << 'EOT',
DocumentRoot "docroot"
SetHandler php5-script
EOT
);

# 起動した host:port から URL を生成して、環境変数に登録
$ENV{ROOT_URL} = "http://" . $httpd->listen . "/";

# my_test.php を実行
system('php', 'my_test.php') == 0
    or die "my_test.php failed: $?";

Test::mysqldTest::postgresql と組み合わせると、データベースを使うウェブアプリの結合テストが気軽に書けそうで夢が広がりまくりんぐ。

まだ荒削りなモジュールなので、改善提案やパッチ等お待ちしております。インストールは cpan -i Test::Httpd::Apache2、リポジトリは github.com/kazuho/p5-test-httpd-apache2 にあります。

それでは、have fun!

June 10, 2009

Pacific という名前の分散ストレージを作り始めた件

 大規模なウェブアプリケーションのボトルネックがデータベースであるという点については、多くの同意が得られるところだと思います。解決策としては、同じ種類のデータを複数の RDBMS に保存する「sharding」 (別名:アプリケーションレベルパーティショニング/レベル2分散注1) が一般的ですが、最近では、分散キーバリューストア (分散 KVS) を使おうとする試みもみられるようになってきています。

 分散 KVS が RDBMS sharding に対して優れている要素としては、事前の分割設計が不要で、動的なノード追加(とそれにともなう負荷の再分散)が容易、といった点が挙げられると思います。一方で、KaiKumofs のような最近の実装では eventually consistent でこそ無くなってきているものの、ハッシュベースの分散 KVS は、レンジクエリができなかったり (例: 最新5件の日記を表示)、トランザクションがないためアプリケーションプログラムが複雑になったりするという問題を抱えています。

 では、どうすればいいのか? MySQL や PostgreSQL を使った RDBMS sharding でも、動的なノード追加(と無停止での負荷の再分散)を実現したい。というのが、今回コードを書き始めた動機でした。それが Pacific です。

 技術的には、大して複雑ではありません。Pacific は、パーティショニング情報とロックを管理する中央サーバ(リゾルバと呼んでいます)と、実際のデータを保存する RDBMS のノード群によって構成されます。

 Pacific では、レンジクエリを実行するために、ユニークキーを利用したレンジパーティショニングを行います。レンジパーティショニングは、ハッシュベースのパーティショニングよりもデータの局所性が向上するので、パフォーマンスや障害の局所性が高まるという効果も期待できます。

 また、トランザクションを可能にするためには、関連するデータが常に同一のノード上に配置される必要があるため注2、全てのデータがパーティショニング用のキーに関連づけられるようなテーブル設計を強制することになります。このデータモデルは、(Pacific が RDBMS 上の分散ストレージであるという点を除けば) Google App Engine の Data Store注3 と同様です。Pacific では、パーティショニング用のキーを含むテーブルをプライマリテーブル、プライマリテーブルと 1:1 または 1:n のリレーションをもつテーブルをセカンダリテーブルと呼んでいます。

 データの再配置は、単一の (あるいは数個の) ユニークキー単位で、1) そのキーに属する全データに排他的書き込みロックをかけ、2) データを別ノードにコピー、3) パーティショニング情報を更新して書き込みロック解除、 4) 旧ノードから読んでいるクライアントがいなくなった時点で旧ノード上のデータ削除、という操作を繰り返すことで行います。再配置中に読み込みがブロックされることはありませんし、書き込みがブロックされる時間も、エンドユーザーが意識しなくていい程度に抑えることができる、と考えています注4

 一番アクセスが集中するのはリゾルバということになりますが、パーティショニング情報の変更は少ないことが予測できますから、ストレージへのアクセス数が 10万 QPS 程度になるまでは問題は発生しないと思います注5。また、パーティショニング情報は RDBMS に保存されるため、リゾルバが不正終了しても、データの不整合が発生することはありません。

 Pacific については、コードは公開の svn レポジトリ注6上においてありますが、現状、テストコードとラフなサンプルが動いている程度で、ドキュメントが全く未整備です。進捗や具体的な使い方等については、今後このブログで書いて行きたいと思います。

17:12追記: 高可用性については、ウェブアプリケーションが使う分散ストレージの場合、ネットワーク分断が発生しない(冗長化によって防止できる)ので、ノードをまたがるような冗長化は必要なく、各ノード毎にクラスタを組めばいいという考えです。

注1. ミクシィのCTOが語る「mixiはいかにして増え続けるトラフィックに対処してきたか」:ITpro
注2. 多くのトランザクションは、関連する数個のテーブルに対する局所的な操作であるという仮定の下、同一のキーに属するデータ内でのみトランザクショナルな操作を可能としています
注3. The Python Datastore API - Google App Engine - Google Code
注4. 書き込みがブロックされる時間は、特定のキーに属するデータサイズ (Google App Engine で言うところのエンティティグループの大きさ) をコピーする時間に依存しますが、一番遅いのはHDDにシーケンシャル書き込みになるでしょうから、数MB/s 程度は目指したいところです
注5. より高いパフォーマンスが必要なら、リゾルバをレプリケーション対応化すればいいという話です。同時に、各ノードへの直接接続をやめて、中継サーバを用意するといった作業も必要になるでしょう
注6. http://kazuho.31tools.com/svn/pacific/