Perl

October 27, 2010

XSSに強いウェブサイトを作る – テンプレートエンジンの選定基準とスニペットの生成手法 (第1回神泉セキュリティ勉強会発表資料)

発表資料は以下のとおり。春山様はじめECナビの皆様、ありがとうございました。

October 16, 2010

My Slides at YAPC::Asia 2010 (Writing Prefork Servers / Workers, Unix Programming with Perl)

The slides I used for my presentations at YAPC::Asia 2010 are now available from the links below. Thank you to those who came, and JPA and the volunteer stuff for running the event. I hope you enjoyed (will enjoy) my slides!

October 01, 2010

Shibuya.pm でセキュアコーディングの話をしてきた件

昨日は、Shibuya Perl Mongersテクニカルトーク#14 に参加してきました。

パネラーとしてウェブサイトのセキュリティに関するディスカッションに加えていただいて、いろいろ上から目線で大局的な話をしたり。一方、ライトニングトークでは具体的な事例として、既にブログに書いた Twitter の XSS に絡んで構造化テキストの処理手法について話をさせていただきました (参照: 構造化テキストの正しいエスケープ手法について, String::Filter っていうモジュール書いた)。

とはいえ、既にブログに書いたことを繰り返すのも芸がないので、正しい設計が何か、という切り口ではなく、どういう設計をすれば「安全」か、という話になっています。スライドは以下にありますので、興味のある方はご覧ください。

主催の shibuya.pm の皆様、パネルディスカッションに参加する機会をいただいた IPA の皆様、会場をご提供いただいた IIJ の皆様、ありがとうございました。

September 23, 2010

String::Filter っていうモジュール書いた - 続: (Twitter の XSS 脆弱性に関連して) 構造化テキストの正しいエスケープ手法について

先のエントリ「(Twitter の XSS 脆弱性に関連して) 構造化テキストの正しいエスケープ手法について」の続き。

弾さんが「404 Blog Not Found:DHTML - 構造化テキストは構造化するのがやっぱ正しい」で示されているような DOM ベースの操作を行えば、原理的に XSS 脆弱性を防ぐことができます。ただ、クライアントサイド JavaScript によるレンダリングはウェブの構造を破壊するという点で筋が悪い(テーブルと FONT タグを利用したページレイアウトが批判されていた頃を覚えていらっしゃいますでしょうか。JavaScript によるレンダリングはウェブのリンク構造も破壊するので一層たちが悪いというのが自分の考え)ですし、サーバサイドでの DOM 操作は重たいので、できれば避けたいところです。

構造化テキストの HTML への変換は、よほど複雑な記法でない限りはワンパスで処理可能ですし、であれば、わざわざ DOM のような構造を作らなくても、SAX のようにイベントベース(あるいはコールバックベース)で処理すれば済むことなのですから。

このような変換の必要は、Twitter に限らず、しばしば耳にするものです。最近自分が目にしたものだと「Template中のURLを自動で賢くアンカーテキストにしたい - すぎゃーんメモ」。このケースでは URL さえリンク化できれば良かったので tokuhirom さんのコメントのとおりでいいんですが、この URI::Find を使う手法だと URL 以外のものも処理したくなった時に対応できないんだよなーと思って見ていたのでした。

そしたらちょうど Twitter の件があったので、複数の変換ルールを合成可能な文字列変換モジュール String::Filter を書いて CPAN にアップロードしました。このモジュールを使えば、ツイートの変換ルールは下の例のように書くことができます。(@user や #hashtag の前には空白が必要といったルールも入っているため)やや長く感じられるかもしれませんが、意図は明確ですし、ルールを後から追加・変更できるので使いやすいのではないでしょうか。

このような HTML スニペット生成手法と、Text::MicroTemplateText::Xslate のような型ベースの自動エスケープ機能(詳細は「Kazuho@Cybozu Labs: Text::MicroTemplate - テンプレートエンジンのセキュリティと利便性」を参照)を備えたテンプレートエンジンを組み合わせることで、自然と、より安全なウェブアプリケーションを開発できるようになっていけばいいな、と思うところです。

use HTML::Entities (encode_entities);
use String::Filter;

# デフォルトの変換ルールを指定してフィルタオブジェクトを生成
my $sf = String::Filter->new(
    default_rule => sub {
        my $text = shift;
        encode_entities($text);
    },
);

# URL の変換ルールを登録
$sf->add_rule(
    'http://[A-Za-z0-9_\\-\\~\\.\\%\\?\\#\\@/]+',
    sub {
        my $url = shift;
        sprintf(
            '<a href="%s">%s</a>',
            encode_entities($url),
            encode_entities($url),
        );
    },
);

# @user の変換ルールを登録
$sf->add_rule(
    '(?:^|\\s)\\@[A-Za-z0-9_]+',
    sub {
        $_[0] =~ /^(.*?\@)(.*)$/;
        my ($prefix, $user) = ($1, $2);
        sprintf(
            '%s<a href="http://twitter.com/%s">%s</a>',
            encode_entities($prefix),
            encode_entities($user),
            encode_entities($user),
        );
    },
);

# #hashtag の変換ルールを登録
$sf->add_rule(
    '(?:^|\\s)#[A-Za-z0-9_]+',
    sub {
        $_[0] =~ /^(.?)(#.*)$/;
        my ($prefix, $hashtag) = ($1, $2);
        sprintf(
            '%s<a href="http://twitter.com/search?q=%s">%s</a>',
            encode_entities($prefix),
            encode_entities(uri_escape($hashtag)),
            encode_entities($hashtag),
        );
    },
);

# tweet を HTML に変換する
my $html = $sf->filter($tweet);

September 22, 2010

(Twitter の XSS 脆弱性に関連して) 構造化テキストの正しいエスケープ手法について

昨日の Twitter の XSS 騒ぎは、まだ皆さんの記憶に新しいことと思います。いい機会なので、ツイートのような構造化テキストのエスケープ手法について触れておきたいと思います。

Twitter のメッセージは、単なる平文(プレインテキスト)ではなく、「@英数字」のような他のユーザーへの言及と「http://〜」のような URL を自動的にハイパーリンク化する構造化テキストです。

このような複数のルールをもつ構造化テキストを HTML 化する際には、どのようなコードを書けばいいのでしょう? まず「@〜」をリンク化してから、URL をリンク化すればいいのでしょうか? それだと、@〜 のをリンク化した A HREF タグの中の URL がさらにリンク化されていまいますね。

では、URL をリンク化してから @〜 をリンク化すればいいのでしょうか? それだと、@ を含む URL があった場合に、やはり HTML が壊れてしまいます。

正しいアプローチは、全てのルールを同時に適用することです。

Perl で書くなら、以下のように、「@〜」と URL を検出するパターンを単一の正規表現にまとめてトークナイズ (split) し、切り出されたトークンがどのタイプかを判定しながら処理していきます。

my $html = '';
for my $token (split m{(http://[0-9A-Za-z_\.\%\?\#\@/]+|\@[0-9A-Za-z]+)}, $tweet) {
    if ($token =~ m{^http://}) {
        $html .= '<a href="' . encode_entities($token) . '">'
            . encode_entities($token) . '</a>';
    } elsif ($token =~ m{^\@(.*)$}) {
        my $user = $1;
        $html .= '<a href="http://twitter.com/' . encode_entities($user) . '">'
            . encode_entities($token) . '</a>';
    } else {
        $html .= encode_entities($token);
    }
}

でもルールが複雑になってくると面倒なので... 続きはあとで書くString::Filter っていうモジュール書いた - 続: (Twitter の XSS 脆弱性に関連して) 構造化テキストの正しいエスケープ手法について」に続く

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!

August 17, 2010

Releasing Starlet 0.10, now runs faster than ever

Last Sunday my friend told me that Starman is now way faster than Starlet and I started to wonder why.  There should not be such a difference between the two servers, sicne they share a lot in common.  So I looked into the code yesterday, found the bottleneck and fixed them, as well as adding some tweaks.  Here's what I did, and the chart[1] shows how the performance changed.

Starlet

1) optimize the calculation of Content-Length

Plack::Middleware::ContentLength was the larget bottleneck.  Instead of calling the module Starlet now calculates the Content-Length header by itself in a faster way, when and only when necessary.

2) use TCP_DEFER_ACCEPT on linux

TCP_DEFER_ACCEPT is a flag that suppresses the return of accept(2) until any data arrives on a newly-established TCP socket.  By using the flag, same number of HTTP requests can be handled by a smaller set of workers, since they do not need to poll waiting for HTTP requests to arrive on non-keepalive connections.  The fact leads to less memory pressure and higher performance.  Starlet turns on the flag by default.

3) switch from alarm to select-based polling

Apps can now use alarm for their own purposes.

4) reuse the fake psgi.input

An optimization copied from Starman.

5) merge short content-length with response headers

It is sometimes faster to merge data within perl and send them as a single packet than sending the two separately.  BTW, anyone going to write a wrapper for writev(2)?

The conclusion is that Starlet is still a good choice if you are looking for an application server that runs behind a reverse proxy, maybe better than Starman until miyagawa-san copies the optimizations back :-p

NOTE 1: The benchmark was taken on linux-2.6.32 (x86_64), using a single core of Core 2 Duo running at 3GHz.  The options used were "ab -c 50", "plackup -s Starlet --max-reqs-per-child=10000", "--workers=64" for keepalive and "--workers=16" for non-keepalive.

July 03, 2010

Parallel::Scoreboard でワーカープロセスをモニタリングする方法

 cho45 さんの Plack::Middleware::ServerStatus (Starman や Starlet で Apache の mod_status 相当の情報を得られるようにする - 冬通りに消え行く制服ガールは、夢物語にリアルを求めない。 - subtech) に続き、昨日 kazeburo さんが「StarmanやStarletでmod_statusっぽい情報を得る簡易版Plack::Middleware::ServerStatus - blog.nomadscafe.jp」というエントリを書かれていらっしゃいましたが、ウェブアプリケーションサーバに限らず、複数のワーカープロセスが動作するシステムにおいて、それらの状況をモニタリングするためのスコアボードがほしい、というケースはよくあることだと思います。

 また、プロセス名を使う方法は、他の監視ツールとの相性が悪い、プロセス名の取得方法にポータビリティがない、あまり長いステータスを保存できない、バッファオーバーフローの可能性がある、といった問題があります。

 なので昨日、特定のミドルウェアや特定の規格 (Plack) に依存しない、より汎用的で確実な実装があるといいよね、という話をしていたのですが、気が向いたので書いてみました。新幹線ハック!

 使い方は簡単。なんでも保存できちゃうので JSON で構造化された情報を保存したり、あるいは HTTP の全リクエストを書いておいてモニタリングに活用する、といったことも可能です。

use Parallel::Scoreboard;

# 初期化
my $scoreboard = new Parallel::Scoreboard(
    base_dir => '/tmp/my_scoreboard',
);

# ワーカープロセス側からステータスを保存
$scoreboard->update('this is my current status');

# 全ワーカープロセスのステータスを読み込み
my $stats = $scoreboard->read_all();
for my $pid (sort { $a <=> $b } keys %$stats) {
    print "status for pid:$pid is: ", $stats->{$pid}, "\n";
}

 内部的な仕組みとしては、プロセス毎にステータスファイルを作り、flock を使って死活監視を行うようになっています。

 コードは github/kazuho/p5-Parallel-Scoreboard にあります。また CPAN にもあげておいたので、もうじき seach.cpan.org/dist/Parallel-Scoreboard からダウンロードできるようになると思います。それでは have fun!

注: 「革命の日々! setproctitle(3)のカーネルサポートが-mmに入りました」等を参照

July 02, 2010

Writing a Fast and Secure HTTP Parser (in the case of HTTP::Parser::XS)

In May, I had a chance to give a talk at Tsukuba.xs Beer Talks #1.  I forgot to upload the slide since then, but finally, here it comes :-p

The slide titled "HTTP::Parser::XS - writing a fast & secure XS module" covers the design and implementation of HTTP::Parser::XS: an XS HTTP Parser that is used by a number of Plack-compatible web servers, and picohttpparser: its underlying HTTP parser written in C.

My intention was to introduce to programmers who mostly use Perl the fun of writing tight (fast and beautiful) code using the C programming language.  It would be grateful if you could feel some of my addiction from the slides.  Enjoy!

PS. And if you find any security holes or bugs, please let me know. Thank you in advance.

February 24, 2010

既製品の管理ツールを使わないことでウェブサービスの TCO を下げる話について hbstudy#8 で話してきた件

 昨日、hbstudy#8 で話をする機会をいただくことができたので、Nagios や Amanda といった既製品の管理ツールやバックアップツールを使わずに内製したことで「パストラック」の運用コストを下げた、という話をしてきました。

 もちろん、「既製品を使わない」というのもひとつの手段にすぎませんから、それを無闇にお勧めするつもりはありません。ただ、小回りの効くツールを組み合わせる手法にも十分な競争力があるという点、あるいはその事例として参考になれば幸いです。

 スライドはこちら。hbstudy 運営の皆様、話を聞いてくださった皆様、ありがとうございました。

January 18, 2010

監視とは継続的なテストである、という話 (もしくは cronlog とテストスクリプトを組み合わせた監視手法について)

 結論から先に。cronlog を使えば、アプリケーションのテストコードと全く同じ形式で、監視用のスクリプトを書くことができます。プログラマが監視ツールの記法を覚える必要はありません。これは、プログラマが運用も行うケースでは特に有効な手法だと思います。

 先週公開した Kazuho@Cybozu Labs: crontab を使って効率的にサービス監視する方法 というエントリで、crontab と拙作の cronlog を用いてサービス監視を書く手法を紹介しました。しかし、挙げた例はいずれも ping や http のテストといった外形監視の手法です。RDBMS とウェブアプリケーションのみから構成されるサービスならそれだけで十分でしょう。

 しかし、外形監視だけでは、メッセージキューのような非同期処理の遅延を観測することはできません。また、http のログを監視して、エラーレスポンスや平均応答時間の監視も行うことが望ましいです。それらの処理をどう書くか。

 自分は、監視とは継続的なテストである、という観点から、キューの処理遅延等の監視を、アプリケーションのテストスートの一部として書いています。具体的には、以下のような感じ (一部改変)。テストはアプリケーションと設定情報を共有しているので、監視ソフトウェアに様々な設定値を入力する必要もありません。

use strict;
use warnings;

use Test::More;
use Pathtraq;

my $app = Pathtraq->new;

cmp_ok
    do {
        my $row = $app->dbh->selectrow_arrayref(
            'select XXXX', # Q4M 使ってない昔のキュー監視
        ) or die $app->dbh->errstr;
        $row->[0];
    },
    '<',
    1000,
    'delay of updater';

for my $tbl (qw(analyze_page set_page_info analyze_keyword)) {
    cmp_ok
        do {
            my $row = $app->dbh_queue->selectrow_arrayref(
                "select count(*) from $tbl",
            ) or die $app->dbh_queue->errstr;
            $row->[0];
        },
        '<',
        1000,
        "delay of $tbl",
}

done_testing;

 あとは、これらのテストコードを、crontab 上で cronlog を介してテストの実行ツール (perl なら prove) に食わせるだけ。

*/5 * * * * cd /var/webapp && exec setlock -nX /tmp/monitor.lock cronlog -- prove -v monitor.t/*.t 2>&1

 これで、監視対象項目 (=テスト項目) になんらかの問題が見つかった場合には、以下のような感じのメールが届くようになります。

Subject: Cron <user@host> cd /var/webapp && exec setlock -nX /tmp/monitor.lock cronlog -- prove -v monitor.t/*.t 2>&1

host.example.com starting: prove -v monitor.t/queue.t

#   Failed test 'delay of analyze_page'
#   at monitor.t/queue.t line 23.
#     '1396'
#         <
#     '1000'
# Looks like you failed 1 test of 4.
monitor.t/queue.t ..
ok 1 - delay of updater
not ok 2 - delay of analyze_page
ok 3 - delay of set_page_info
ok 4 - delay of analyze_keyword
1..4
Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/4 subtests

Test Summary Report
-------------------
monitor.t/queue.t (Wstat: 256 Tests: 4 Failed: 1)
Failed test:  2
Non-zero exit status: 1
Files=1, Tests=4,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.40 cusr  0.08 csys =  0.50 CPU)
Result: FAIL
command exited with code:1

 perl に限らず、テストの実行ツールは一般に、成功した場合はゼロ、失敗した場合は非ゼロを返します (そうしないと make test && make install とか自動化できない)。この特性と cronlog の、非ゼロが返った場合だけ cron を経由してログをメールで送信する、という機能を組み合わせることで、テストコードが監視系に早変わりするのです。さらに外側で setlock -nX しているのは、5分間でテストが終了しなかった場合に、やはりエラーメールを送信するため。

 以上のように、cronlog を使えば、プログラマが使い慣れたテストスートの一部として監視系を書くことができます。パストラックでは更に、リバースプロキシのログ監視を touch_if コマンドとテストコードを連動する形で実施するようにしています。どうぞご覧ください。

blockdiff を使ったお手軽ホットバックアップ環境の構築 (Linux, MySQL, etc.)

 一昨日に開催された hbstudy #7 にバックアップの話を聞きに行ってきました。Amanda を中心にした話で、とても勉強になりました。が、設定がめんどくさそうだなぁ、とも。自分の需要にはあわない感じでした。

 勉強会が終わったあとで、自作のバックアップスクリプト blockdiff に関する話を何人かの方とさせていただいたのですが、思いのほか反応が良かったので、あらためて紹介したいと思います。

 blockdiff は、一言でいうと、パーティションやデータベースのデータファイルの差分バックアップツールです。rsnapshot に似ていますが、rsnapshot ではデータベースのホットバックアップ不可能です。逆に blockdiff はディレクトリ単位でのバックアップには対応していないかわり、ファイルシステムやデータベースを、一貫性を保ちつつ実質無停止で差分バックアップすることができます

 blockdiff の具体的な特徴は以下のとおりです。

  • 設定ファイルが不要
  • 複雑な設定がありません。コマンドライン (あるいは crontab) で、適当な引数をつけてバックアップコマンドを呼び出すだけの簡単インターフェイスです。コマンドラインで動作を制御するため、(ホットバックアップを含む) 様々なバックアップロジックを組むことも可能です。

  • バックアップを取るサーバにソフトウェアのインストールが不要
  • ssh 経由でリモートサーバのバックアップを取ることができます。サーバにバックアップソフトウェアをインストールする必要はありません (バックアップを取るために必要なプログラムは自動的に送り込まれます) 注1

  • フルバックアップと増分バックアップに対応
  • フルバックアップと増分バックアップに対応しています。また、下位レイヤのスクリプト (blockdiff_dump) を直接実行すれば、差分バックアップも可能です。

  • ファイル単位、あるいは LVM スナップショットを使ったバックアップが可能
  • ファイル単位のバックアップと、LVM スナップショットを使ったホットバックアップに対応しています。

  • MySQL のホットバックアップが可能
  • 同梱の mysqllock コマンドと組み合わせることで、LVM スナップショットを使った MySQL データベースのホットバックアップが可能です。PostgreSQL については確認はしていませんが、ロックコマンドを使わなくてもバックアップが取れるんじゃないかと思います。

  • LVM ベースの VM のホットバックアップが可能
  • LVM ベースの仮想ディスクを用いた Xen の DomU のホットバックアップが可能です注2。私は使っていないので試していませんが、KVM の仮想マシンについても同等のことが可能だと思われます。LVM ベースではない VM についても、スナップショット機構によってはホットバックアップが可能だと思われます。

 blockdiff を使った LVM ボリュームのバックアップ方法については Kazuho@Cybozu Labs: リモートからXenのDomUとかLVMやファイルを差分バックアップするスクリプトを書いた で触れたので、ここでは MySQL データベースをホットバックアップする場合の設定を紹介したいと思います。前提として、MySQL のデータが LVM ボリューム上に保存されている必要があります。

 バックアップスクリプトは、以下のような感じになります。

#! /bin/bash

export BLOCKSIZE=16834
export LVCREATE_PREFIX="mysqllock --host=db-host --user=root --password=XXX"
export YEARMONTH=`date '+%Y%m'`

blockdiff_backup /var/backup/db-backup-$YEARMONTH bin/ssh_lvm_dump --gzip \
  root@db-host /dev/mapper/logical_volume_of_mysql

このバックアップスクリプトは以下のことを行っています。

  • バックアップのブロックサイズを InnoDB のブロックサイズである 16KB に設定
  • スナップショットを取る瞬間に mysqllock コマンドを使って FLUSH TABLES WITH READ LOCK を使うことで、MyISAM テーブル等の一貫性のあるバックアップを実現
  • 1ヶ月毎にフルバックアップ。それ以内は増分バックアップ

 バックアップスクリプトを実行すると、最初に db-backup-YYYYMM.1.gz, db-backup-YYYYMM.1.md5, db_backup-YYYYMM.ver という3つのファイルが作成されます。YYYYMM.1.gz がバックアップデータ、YYYYMM1.md5 がブロック単位のチェックサム情報です。次に実行すると YYYYMM.2.* が、その次は YYYYMM.3.* が、という形で増分バックアップが増えていきます。最新のバックアップの番号は .ver ファイルが記憶しているので、バックアップが途中で失敗しても問題ありません。次回バックアップ時に壊れたバックアップファイルが上書きされます。

% ls -l db-backup-201001*
-rw-r--r-- 1 backup backup 50289166539 2010-01-01 05:35 db-backup-201001.1.gz
-rw-r--r-- 1 backup backup   131072004 2010-01-01 05:35 db-backup-201001.1.md5
-rw-r--r-- 1 backup backup 10914423057 2010-01-02 04:32 db-backup-201001.2.gz
-rw-r--r-- 1 backup backup   131072004 2010-01-02 04:32 db-backup-201001.2.md5
-rw-r--r-- 1 backup backup 13648250036 2010-01-03 04:33 db-backup-201001.3.gz
-rw-r--r-- 1 backup backup   131072004 2010-01-03 04:34 db-backup-201001.3.md5
...
-rw-r--r-- 1 backup backup           3 2010-01-18 04:34 db-backup-201001.ver

 バックアップスクリプトが正しく動作することを確認したら、crontab を設定して毎日バックアップを取るようにします。

0 4 * * * setlock -nX /var/backup/backup.lock cronlog -l /var/backup/backup.log -t -- /var/backup/backup.sh 2>&1

 ここでは、daemontools の setlock コマンドを使って万が一の重複起動を抑止しています。また、kaztools の cronlog コマンドを使うことで、バックアップの進捗をログに保存しつつ、バックアップが失敗した場合は cron 経由でアラートメールが送信されるようになっています。

 ちなみに、バックアップのログ (backup.log) は、毎日次のような感じで追記されていきます。データベースの停止期間 (アプリケーションからのクエリがロックされる期間) は 03:35:56 から 03:36:00 までの4秒間であり、実質無停止でのバックアップができていることがわかります。

------------------------------------------------------------------------------
[Sat Jan  9 03:05:02 2010] backup-srv starting: /var/backup/backup.sh
[Sat Jan  9 03:05:02 2010] creating snapshot...
[Sat Jan  9 03:05:07 2010]   Logical volume "lvm_dump" created
[Sat Jan  9 03:05:07 2010] running: bin/ssh_blockdiff_dump --gzip "root@db-host" "/dev/db/lvm_dump"...
[Sat Jan  9 03:19:22 2010] removing snapshot /dev/db/lvm_dump...
[Sat Jan  9 03:19:23 2010]   Logical volume "lvm_dump" successfully removed
[Sat Jan  9 03:19:23 2010] backup completed successfully
[Sat Jan  9 03:19:23 2010] creating snapshot...
[Sat Jan  9 03:35:56 2010] issuing lock statement: FLUSH TABLES WITH READ LOCK
[Sat Jan  9 03:36:00 2010]   Logical volume "lvm_dump" created
[Sat Jan  9 03:36:00 2010] issuing unlock statement: UNLOCK TABLES
[Sat Jan  9 03:36:00 2010] running: bin/ssh_blockdiff_dump --gzip "root@db-host" "/dev/x25m.1/lvm_dump"...
[Sat Jan  9 04:18:44 2010] removing snapshot /dev/x25m.1/lvm_dump...
[Sat Jan  9 04:18:46 2010]   Logical volume "lvm_dump" successfully removed
[Sat Jan  9 04:18:46 2010] backup completed successfully
[Sat Jan  9 04:18:46 2010] command exited with code:0

 このように、blockdiff を使うことで VM 単位のバックアップだけでなく、データベースのホットバックアップも簡単に取ることができます。パストラックでは、これらバックアップスクリプトに処理を追加し、バックアップ中は統計処理を抑止するといったことも行っています。

 blockdiff や kaztools は、いずれも私の github リポジトリ (github.com/kazuho) からダウンロードして perl Makefile.PL && make all test && make install でインストール可能です。あるいは nopan がインストールされていれば、nopan http://github.com/kazuho/blockdiff http://github.com/kazuho/kaztools と1コマンドでインストールすることができます。いずれもパストラックで実際に運用しているツール群ですが、興味のある方は (at your own risk で) お試しあれ。

注1: バックアップを取るサーバに Perl 5.8 以降がインストールされている必要があります。また、公開鍵認証を用いて SSH ログインできる必要があります
注2: XenServer のスナップショット機能とは併用できません

January 14, 2010

crontab を使って効率的にサービス監視する方法

監視とは継続的なテストである、という話 (もしくは cronlog とテストスクリプトを組み合わせた監視手法について)に続きます

 今日ようやく、積ん読状態だった「Software Design 2010年1月号」を手に取ったのですが、特集が「今日から使えるスクリプト満載! [プロ直伝]お手軽サーバ監視術」。興味深く拝読したのですが、もっと楽ができるのにと思うところも。ちょうど、昨年末に運用しているサービス「パストラック」のサーバを移転し、crontab と perl で書かれたスクリプト群を使った監視環境を構築したところなので、そこで使っているスクリプト cronlog を紹介したいと思います。

 特集の前書きにも書かれていることですが、サーバやネットワーク機器が多数ある環境なら、Nagios を始めとする、専ら監視のために作られたソフトウェアを使って、監視システムを構築すべきです。逆に小規模な環境で crontab やスクリプトを監視に使うメリットがあるとしたら、それは、極力覚えることや必要な設定を減らせる、という点でしょう。

 監視項目毎にスクリプトを書くなんて面倒でやってられません。そこで cronlog! cronlog を使うことで、既存のコマンドを crontab を使った監視系に早変わりさせることができます。

 cronlog は、引数として与えられたコマンドを起動し、その終了コードが非ゼロであった場合に、コマンドの出力結果 (stdout と stderr) を標準出力にプリントするコマンドです。何を言っているか良くわからないかもしれませんが、cronlog を使うと、たとえば ping によるサーバ監視は crontab に以下のように書くことで可能となります。

MAILTO=alert@example.com
5 * * * * cronlog -- ping -n 5 my-server 2>&1

 この crontab を解説すると、次のようになります。ping コマンドの終了コードは、応答が返ってきた場合 0、返ってこない場合に 1 です。cronlog は終了コードが非ゼロの場合のみ、実行されたコマンド (ここでは ping) の出力結果を出力、つまり ping がエラーを返した場合にのみ、その内容を cron に受け渡すことになります。cron は何か出力を受け取ると MAILTO に指定されたアドレスに、その内容をメールとして送信します。以上をまとめると、ping 応答が返ってこなかった場合にエラーメッセージを送信するという処理が、上の1行で実現できていることが分かると思います。

 これなら、監視項目ごとにスクリプトを書かなくてもいいので簡単です。

 ping のかわりに HTTP の監視をしたいのなら、

5 * * * * cronlog -- wget -O - http://my-server 2>&1

と書けば OK です。あるいは、ping 監視に成功した場合のみ HTTP 監視を行う、というようなマニアックな処理も、

5 * * * * cronlog -- ping -n 5 my-server 2>&1 && cronlog -- wget -O - http://my-server 2>&1

とすることで実現できます。監視項目が大量にあったり、ワンライナーで書きたくない場合は、スクリプトを書いて、それを cronlog で呼び出せばいいのです。注意すべきは唯一、障害を検知した場合は、非ゼロの値で exit すること。このへんも小技があるのですが、それはさておき。

 cronlog には、ログをファイルに保存したり、ログの各行にタイムスタンプを挿入するオプションもあります (というか、cronで実行するタスクのログを保存しつつ、タスクがエラーを返したらメールを送信する、というのが本来の機能です。監視=副作用のないタスク、なだけ)。詳しくは man cronlog をご覧ください。

PS. cronlog はオレオレ監視スクリプト群 kaztools の一部です。インストール方法は github.com/kazuho/kaztools からダウンロードしてきて perl Makefile.PL && make && make test && make install。興味のある方はどうぞ。

December 15, 2009

[ANN] CGI::Application::Emulate::PSGI

From: Kazuho Oku
To: cgiapp-ml

Hi,

I have just uploaded CGI::Application::Emulate::PSGI onto CPAN.

It's a yet-another adapter for running CGI::Application on top of PSGI. The differences from the older one: CGI::App::PSGI is that CGI::App::Emulate::PSGI is more friendly to existing code in two aspects. From the POD,

uses CGI.pm instead of CGI::PSGI
CGI::Application::PSGI (that uses CGI::PSGI) does not support programs calling CGI.pm in func-style (like CGI::virtual_host()). CGI::Application::Emulate::PSGI sets up environment variables so that codes using CGI.pm will work.
compatible with CGI::Application::Dispatch
The interface of CGI::Application::Emulate::PSGI is different from CGI::Application::PSGI, and is compatible with CGI::Application::Dispatch.

Thanks to MARKSTOS and miyagawa for answering my question (https://rt.cpan.org/Public/Bug/Display.html?id=52756), I agree that the ideal way would be to run CGI::App::Dispatch would be to make it somehow run on CGI::Application::PSGI (since it would be cleaner in the fact that it doesn't modify %ENV for each HTTP request), CGI::App::Emulate::PSGI would be a good solution IMO for running existing CGI::App-based applications on top of PSGI.

December 09, 2009

Hot-deploying Plack applications using Server::Starter

It's time to eat my own dog food.

During YAPC::Asia 2009, I wrote a tiny module called Server::Starter, a superdaemon for hot-deploying TCP servers (see Writing Hot-deployable servers (introduction of Server::Starter) for the benefits of using the module).  On the other hand, I have been involved in developing the http servers in Plack since then.

And time has come.  Having decided to use the two in my new web application server, I wrote a module to glue them together, Kazuho Oku / Plack-Server-Standalone-Prefork-Server-Starter - search.cpan.org.

Although the name is pretty long, it is easy to use :-p

// start server
# start_server --port=80 -- plackup -s Standalone::Prefork::Server::Starter myapp.psgi

// or use setuidgid to drop privileges
# start_server --port=80 -- setuidgid www plackup -s Standalone::Prefork::Server::Starter myapp.psgi

// graceful update
# killall -HUP start_server

Or use svc -h if you are going to manage Server::Starter using daemontools.

December 01, 2009

高度に進化した分散データストアは RDBMS と見分けがつかない? (shibuya.pm #12 スライド)

 昨日開催された shibuya.pm #12 - NoSQL特集で使用したスライドを slideshare にアップロードしました。

 開発しているシャーディングミドルウェアである Incline と Pacific については YAPC::Asia 2009 を始めいろいろな所で話をする機会をいただいてきたので、今回は、なぜ RDBMS ベースのアプローチを採用したのかという背景を中心に説明させていただきました。概念的な話が多くて分かりにくかったと思います(すみません)が、細かな点についてはパフォーマンスとスケーラビリティのためのデータベースアーキテクチャ (BPStudy#25発表資料)を参照いただければと思います。

 また、中で出てきた「実体化ビュー」については、Materialized view - Wikipedia, the free encyclopediaが良くまとまっているかと思います。Incline は一言でいうと、RDBで構成されるshard群の上で read-only かつ eventually consistent な materialized view を実現するためのツールです。

November 17, 2009

リモートからXenのDomUとかLVMやファイルを差分バックアップするスクリプトを書いた

kazuho's blockdiff at master - GitHub

 月曜から XenServer の運用を始めたんですが、以下のような要件のバックアップツールがほしくなりまして。ちょっと調べた範囲で見つからなかったので、書いてみました。

  • スナップショットによる無停止でのバックアップ
  • 差分/多世代バックアップが可能
  • ネットワーク越しにプルベースでバックアップが可能

 元々は去年書いたバイナリファイルの差分バックアップスクリプト (データベースの差分バックアップとウェブサービスのお引っ越し)。これを拡張して、LVMを操作したり、バージョン番号を自動採番するようにしたり、ssh経由での転送機能をつけたりした感じです (ディスクは遅いからギガイーサなら over ssh でも問題ない)。

 詳しいことを知りたい人にはソースコードを読んでいただくとして (といっても300行ちょい)、たとえば XenServer 上の DomU のバックアップを取るなら、サーバ上で xe vm-disk-list コマンドを実行して論理ボリューム名を取得したあと、バックアップ用マシンの crontab にこんな感じで設定します。Xen以外の一般的なLVMのバックアップを取ることもできます。

0 3 * * * blockdiff_backup /var/xenbackup/vm01-`date '+%Y%m'` ssh_lvm_dump -z root@xenserver /dev/VG_XenStorage-... 2>&1

 これで、毎月1日にフルバックアップ、他の日は差分バックアップがとれる。リストアは、こんな感じ。

# for i in vm-01-200911.*.gz ; do \
>   gunzip < $i | blockdiff_merge /dev/VG_XenStorage-...
> done

 あとは、差分バックアップ時にディスクを全スキャンしなくてもよくなれば、もっとバックアップ頻度を上げられていいのにな、と思っています。

11/19追記: XenServerで使う場合の制限と詳細については、下記URLをご参照ください。

October 08, 2009

Cppref: reading cppreference.com docs offline, like man or info or perldoc

Today I created a tiny script called cppref, a wrapper for documents on cppreference.com.  Blurbs are:

  • docs are bundled with the interface, no network access required
  • works like man(1) or info(2) or perldoc(2)

It looks like follows.

$ cppref
You are here: C++ Reference

C++ Reference

                                  C++

General Topics                      * C++ Strings
                                    * C++ I/O
  * FAQ                                 + C++ String Streams
  * Pre-processor commands          * C++ Exceptions
  * Operator Precedence
  * Escape Sequences              C++ Standard Template Library (STL)
  * ASCII Chart
  * Data Types                      * Overview
  * Keywords                        * Iterators
                                    * C++ Algorithms
Standard C Library                  * C++ Vectors
                                    * C++ Double-Ended Queues
  * Overview                        * C++ Lists
  * Standard C I/O                  * C++ Priority Queues
  * Standard C String & Character   * C++ Queues
  * Standard C Math                 * C++ Stacks
  * Standard C Time & Date          * C++ Sets
  * Standard C Memory               * C++ Multisets
  * Other standard C functions      * C++ Maps
                                    * C++ Multimaps
                                    * C++ Bitsets

$ cppref vector
You are here: C++ Reference >> C++ Standard Template Library >> C++ Vectors

C++ Vectors

Vectors contain contiguous elements stored as an array.

Accessing members of a vector can be done in constant time, appending elements
to a vector can be done in amortized constant time, whereas locating a specific
value or inserting elements into the vector takes linear time.

Constructors create vectors and initialize them with some data
Operators    compare, assign, and access elements of a vector
assign       assign elements to a vector
at           returns an element at a specific location
back         returns a reference to last element of a vector
(snip)

Or if the specified term maps to multiple files,

$ cppref push_back        
multiple choices:
  stl::deque::push_back
  stl::list::push_back
  stl::vector::push_back
  string::push_back

By default, cppref uses w3m as its viewer, so you can follow the links to read the documents.

Cppref is available from search.cpan.org/dist/cppref or github.com/kazuho/cppref.  Hove fun!

October 07, 2009

Uploading an autotools-based distribution onto CPAN

Background

It is a pain to create binary packages.  But installing a program from source tarball is a tedious task.  You need to run ./configure & make && make install.  Sometimes you need to resolve the dependencies by hand as well.  That's where source-code-based package distribution systems come in, and the largest system is, IMHO, CPAN.  If you could upload a autotools-based distribution onto CPAN, then the users of the software can install them with the cpan command (or cpanp or cpanf or whatever), with the dependencies automatically resolved.

And for my case, it was considered especially benefitial, since the program I am now working on (it's called incline, a replicator for RDB shards using MySQL or PostgreSQL), uses perl scripts for running tests.  By distributing incline through CPAN, the perl modules required for running the test suite could be installed automatically.  Besides that, the expected users of the software outside my company are mostly perl users.

So I adjusted the files of incline so that it can be uploaded to CPAN (it continues to work as a normal autotools-based distribution as well).  The steps were as follows (for a working example, please see incline - search.cpan.org).

Step 1. write META.yml generator

CPAN packages should contain a file call META.yml which describes the metainformation of the package like dependencies, etc.  Since it would be a good idea to fill-in some fields of the file by reading other files of the distribution, I wrote a script file (META.yml.pl) that generates META.yml.  As you can see, it is pretty straightforward.  One point to note is that it has a --fix-makefile option that inserts dependency information into Makefile, which is mandatory for supporting old versions of the CPAN installer.

META.yml.pl
#! /usr/bin/perl

use strict;
use warnings;

use YAML;

my $meta = {
    name               => 'incline',
    abstract           => 'a replicator for RDB shards',
    version            => do {
        my $s = `echo VERSION | cpp -include src/incline_config.h`;
        $s =~ s/^.*\n\"([0-9_\.]+)\".*?$/$1/s
            or die "failed to obtain version number";
        $1;
    },
    author             => do {
        open my $fh, '<', 'AUTHORS'
            or die "failed to open AUTHORS:$!";
        my @authors = map {
            chomp $_;
            $_;
        } <$fh>;
        close $fh;
        \@authors;
    },
    license            => 'bsd',
    distribution_type  => 'script',
    dynamic_config     => 0,
    configure_requires => {
        perl          => 5.008,
        YAML          => 0,
    },
    requires           => {},
    build_requires     => {
        DBI                => 0,
        'List::MoreUtils'  => 0,
        'Scope::Guard'     => 0,
        'Test::mysqld'     => 0,
        'Test::postgresql' => 0,
    },
    resources          => {
        license => 'http://www.opensource.org/licenses/bsd-license.php',
    },
    no_index           => {
        directory => [ qw/example src t/ ],
        file      => [ qw/README.html/ ],
    },
    'meta-spec'        => {
        version => 1.4,
        url => 'http://module-build.sourceforge.net/META-spec-v1.4.html',
    },
    generated_by       => 'META.yml.pl',
};

if (@ARGV && $ARGV[0] eq '--fix-makefile') {

    my $prereq_expr = do {
        my %req = (
            %{$meta->{requires}},
            %{$meta->{build_requires}},
        );
        join ", ", map { "$_=>q[$req{$_}]" } sort keys %req;
    };
    print <<"EOT";
# MakeMaker Parameters:

#  PREREQ_PM => { $prereq_expr }

# --- MakeMaker post_initialize section:

EOT
    while (my $l = <STDIN>) {
        print $l;
    }

} else {

    print Dump($meta);

}

Step 2. call META.yml.pl from configure

As explained, older versions of the CPAN installer requires the dependencies to be written in Makefile (this is still the case for newer versions if your configuration script dynamically determines the dependencies).  In order to meet the requirement, I added a call to META.yml.pl at the end of my configure.ac.

configure.ac
AC_OUTPUT(Makefile src/Makefile)

cp Makefile Makefile.orig && perl META.yml.pl --fix-makefile < Makefile.orig > Makefile
if test "$?" -ne "0"
then
AC_MSG_ERROR([failed to add metainfo to Makefile])
fi

Step 3. adjust Makefile

There are a couple of things to adjust in the Maefile.  First, make test should be supported.  In my case, I already had one, that executes test scripts written in perl using Test::Harness.  The next thing is to include more than one POD (plain old document).  I already had one as well, since I generate man pages and readmes from POD.  The last thing is to generate and include two files in the distribution, META.yml and MANIFEST.  The Makefile uses a dependency and dist-hook to generate the files.  The files adde should be included in the EXTRA_DIST section of Makefile.am, of course.

Makefile.am
META.yml: META.yml.pl
perl $< > $@

dist-hook:
(cd $(distdir) && find * -type f) > $(distdir)/MANIFEST

test:
perl -MTest::Harness -we 'runtests(@ARGV)' t/*.t

Step 4. write Makefile.PL

Create Makefile.PL that calls ./configure.  Do not write any configuration logic in Makefile.PL, all logic should go into configure.ac so that the distribution could be built by using either Makefile.PL or configure.

Makefile.PL
exec './configure', @ARGV;
die "failed to execute configure:$!";

Conclusion

As described, it is not (so) difficult to add CPAN compatibility to existing autotools-based distributions, or to create a new distribution that works as both.  But to tell the truth it was not as easy as it seems to figure out the right way.  It would have been impossible without help from charbar, migayawa, tokuhirom, and walf443.  I would like to thank them all.

September 11, 2009

Writing Hot-deployable servers (introduction of Server::Starter)

Yesterday at YAPC::Asia 2009, I did a LT introducing verious techniques to write hot-deployable servers, and introduced a perl module called Server::Starter that encapsulates the burden of developing support for hot-deployment within each TCP server program. The presentation slides are on Slideshare.

August 18, 2009

Perl のテスト用に PostgreSQL のインスタンスを自動で構築するモジュール Test::postgresql を書いた

 以前作成した Test::mysqld (参照:Perl のテスト用に MySQL 環境を自動で構築するモジュール Test::mysqld を書いた) の姉妹版、Test::postgresql を書きました。このモジュールを使えば、テストコード内で一時的にテスト専用の PostgreSQL のインスタンスを構築し、テストを実行することができます。構築されたインスタンスは、テスト終了時に自動的に削除されます。

use DBI;
use Test::postgresql;
use Test::More;

my $pgsql = Test::postgresql->new(
    initdb_args
        => $Test::postgresql::Defaults{initdb_args} . ' --encoding=utf8',
) or plan skip_all => $Test::postgresql::errstr;

plan tests => 10;

my $dbh = DBI->connect(
    "DBI:Pg:...;port=" . $pgsql->port
);

... 以下、テストコード ...

お使いになりたい方は、CPAN か coderepos からどうぞ。

August 07, 2009

YAPC::Asia 2009 で「スケールするウェブアプリケーションを20分で作る方法」について話します

 このところ、MySQL と Perl 関連のエントリをいろいろ書いていますが、それは、スケールアウト可能で、かつ、管理が容易なウェブアプリケーションを、簡単に書けるようにしたい、という理由があるからです。

 ただ、ブログエントリだとどうしても細切れになるので、一連のモジュールやプログラムを組み合わせて、どうやってスケールするウェブアプリケーションを作るのかという話を YAPC::Asia 2009 でさせていただくことにしました。

 YAPC::Asia 2009 は9月10日(木)と11日(金)の2日間、東京工業大学大岡山キャンパスで開催されます。今日からチケット販売も始まったので、興味のある方はお越しいただければ、と思います。

August 04, 2009

Perl のテスト用に MySQL 環境を自動で構築するモジュール Test::mysqld を書いた

 ORM やウェブアプリケーション関連のライブラリなどのテストケースを書くにあたっては、 RDBMS へのアクセスが必要になります。しかし、SQLite のようなスタンドアローンのデータベースと比較すると、サーバ型データベースである MySQL に接続してテストを書くのは、既存の MySQL の権限設定やデータベース名を気にする必要があったりと、いろいろ不便です。そこで、MySQL のインスタンスをテンポラリディレクトリに自動生成し、テストが終わったら削除してくれる Perl モジュール Test::mysqld を書きました。こんな感じで使います。

use DBI;
use Test::mysqld;
use Test::More;

my $mysqld = Test::mysqld->new(
    my_cnf => { 'skip-networking' => '' }, # TCP接続を使わない
) or plan skip_all => $Test::mysqld::errstr;

plan tests => 10;

my $dbh = DBI->connect(
    "DBI:mysql:$db_name;user=root;mysql_socket=" . $mysqld->my_cnf->{socket},
);

... 以下、テストコード ...

 テスト専用の mysqld サーバが立ち上がるので、既存のデータを壊したりすることもありません。データベースを作ったり消したりも自由自在。また、既存の mysqld の設定に引きずられることもないので、mysqld の権限設定を気にする必要もありません。テストが終了すると、作成された mysqld インスタンスは自動的に消去されます。

 結論。Test::mysqld を使えば MySQL にアクセスするテストが SQLite なみに簡単に書けるよ、きっと。ということで、お使いになりたい方は、CPAN か coderepos からどうぞ。

2009/8/5追記: Test-mysqld-0.04 で mysql 関連の必要なプログラムが見つからなかった場合の処理を整理したのにあわせ、サンプルをアップデートしました。