開発の最近のブログ記事

週末にsupervisorっていうものについて話してる人がいたので、特に理由なくPerlでクローンを書いてみようと思った。というわけで本当に基本の部分まで書いた。名前はBrahman。神様の名前にしたかったのはプロセスの生死を管理するから。

繰り返すけど、特に理由はない。書きたかったから。ちなみに内部的には多分本家と全然互換性はない。設定ファイルがだいたいクローンできればいいや、というくらい。


今のところできることは、例えば今daemontoolsで管理してるplakupサーバーがあったとして、こんな感じの設定を書くと立ち上がってstdout/stderrを一緒のログに保存してくれる、ってところまで。

[program:yourapp]
environment=PORT=50000
directory=/service/yourapp
command=/service/yourapp/run
stdout_logfile=/var/log/yourapp.log
redirect_stderr=true

以上を書いて、brahmand -c config.ini とか書くと勝手にこのプロセスがずっと生きてるように管理してくれるでござる。brachmanctl list とかすると、現在動いているプロセスとかでてくるので、supervisor.processes.pid あたりをkillしてみると生き返るのが見えるかと思います。

一応頑張った点としては、万が一 ログのプロセスが死んじゃったとしても、daemontoolsのsuperviseに相当するプロセスが生き残ってればまたログを取るプロセスが生き返って、もう一度アタッチしてログをとり続けられる事と、ログを取る以外のプロセスもアタッチすることによってイベントを受け付ける事ができる、など。

あと外部からのコマンドうけつけはJSONRPCで行う。今はごめん、list(動いているプロセスとかをリスト)とstop(brahmandと管理してるプロセスをストップ)しかできないけど、しこしこ書いていけばいくらでもAPIは追加できる。

ここまで正味二日間でできた。JSONRPCは以前書いた物の流用、あとはTwiggyとかを以前からバリバリ書いてたおかげで楽だった。Twiggyがそのままエンベッドできるのにも助かった。

とりあえず自分の使ってる開発サーバーはこいつに移行できるようにしてみようかな、と思ってるけど、人柱とか一緒にやってくれる人がいると嬉しいなー、と。

なんかふと気づいたら最近以前書いたPerlでシグナル処理の記事にブクマがついていたので続き的な感じで書いてみた。

例えば 以下のように、ワーカーとかでずーーーーっとDBにクエリを投げてその結果を使って処理をする、というような処理を書くとする

     while ( $loop ) {
         my $sth = $dbh->prepare( .... );
         $sth->execute();
         while ( $sth->fetchrow_arrayref ) {
              ....
         }
     }

以前書いた%SIGを用いたPerlの普通のシグナル処理では、もしexecute()でブロックしていた場合など(例:Q4Mでqueue_waitしてる)ではいくらSIGINTとかを送ってもブロックしたまんまになる。理由はPerlの素のシグナル処理は*Perlの*1オペと1オペの間に実行されるから。データベースに対する処理はCレベルでブロックしていて、実はこれはほとんどの場合Perlの1オペがブロックしてる間に終わらない

そういう場合は%SIGでシグナル処理をするのではなくて、POSIX::SigActionとかを使う。POSIXレイヤーでシグナルハンドラを指定できるので、Cレイヤでのブロック状態中でもシグナルハンドラを呼び出せる。Perlの世界から見ると、Perlのオペ完了を待たずにすぐに実行されるのでこれらをリアルタイムシグナルと呼ぶ。基本の使い方はperldoc POSIXした時にでてくる。

で、シグナルを受け取った時に現在実行中のステートメントハンドラをキャンセルするには$sth->>cancelを呼べばいいし、ついでにDBも接続を落としちゃうなら$dbh->disconnectとかしちゃえばいい

    use POSIX qw(:signal_h); # import SIGINT, SIGTERM, and other signal related stuff

    my $sth; # declare now to make it accessible
    my $sigset = POSIX::SigSet->new( SIGINT ); # specify which sig we're handling
    my $cancel = POSIX::SigAction->new(sub { # the handler
        if ( $loop ) {
            eval { $sth->cancel };
            eval { $dbh->disconnect };
            $loop = 0;
        }
    }, $sigset, &POSIX::SA_NOCLDSTOP);
    POSIX::sigaction( SIGINT, $cancel ); # register them handler

    while( $loop ) {
        $sth = $dbh->prepare( ... );
        $sth->execute();
        while ( $sth->fetchrow_arrayref ) {
             ....
        }
     }

これでSIGINTでちゃんとキャンセルされる。ただ、当然ながらキャンセルされた結果undefになる変数とかがあるなら、それは while の中で参照しないように自分で気をつけて処理をするように。

あと、sigaction() でシグナル設定するの面倒くさいよ!って人は%SIGRTっていう、リアルタイムシグナル用の%SIGハッシュがあるらしい。自分は間違えそうなので使ってないけど。use POSIX; すると自動的に現スコープから参照できるようになる。

そうそう、激しくオフトピだけど、use POSIX; は、実に数百個(うろ覚え)もの関数や変数を現パッケージにインポートするので、効率を重視したいなら注意。必要なものだけ明示的にインポートするか、常にPOSIX::hogeのようにして参照するほうがよいだしょう。
「オブジェクト指向なパラダイムでプログラムを書くとき」にClass::Data::Inheritableは排除すべきモジュールである。今回激しくそれを痛感しているので、だらだら書いてみたい。

まず、Perlはマルチパラダイムが可能な言語なので、Class::Data::Inheritable自体は否定されるべきものでもないし、あと必ず例外ケースはでてくるのでその際には躊躇なく使えばいいと思う。以下は最初の一文の通り、Perlでオブジェクト指向を使う場合はClass::Data::Inheritableは基本的に使わず、あくまで例外ケースに留めるべきだ、という事を伝えたい。

まずその1: クラスアトリビュートはグローバル変数

クラスアトリビュートはグローバル変数です。異論は認めません。

Singletonもそうだけど・・・使う場所はあるんだけど、グローバル変数は限りなく限定的に使われるべき。基本的に使わない物なので、確実にインスタンス変数では無理、もしくはその方がパフォーマンスが○○%向上する!みたいな箇所以外では原則使うべきものではない。

特にオブジェクト指向とは混ぜるな危険!ざっとググった中で一番簡潔に書いてあったのはこのブログかな

2: オブジェクアトリビュートなのかクラスアトリビュートなのかが激しくわかりにくい。

アトリビュートはどれもアクセッサを通して使うけど、そいつらのうちどれがクラスアトリビュートなのか、どれがインスタンスアトリビュートなのか見ても全然わからない。

結果、グローバルな副作用を常に意識しながらソースコードを改修しなくてはならない。

3:ベタ書きを推奨する→アトリビュート初期化のタイミングが制御しにくい

クラスアトリビュートを使ってるとその指定を.pmファイル内にベタ書きする事が多くなる。Catalystとかでもそうだよね。ある一定の効果はあるので、完全否定じゃないんだけど、アトリビュートとかを全部これで初期化しはじめると・・・微妙。

普通オブジェクトはインスタンス化するタイミングでアトリビュートが初期化されるべきなのに、クラスが*読み込まれた*時点で初期化されちゃう。結果、設定等をBEGIN { } で書く必要があったりする。どうなんだ、これは。

4: アトリビュートのライフサイクルが指定できない→同クラスのオブジェクトインスタンス変数とライフサイクルが違うので困る

クラスアトリビュートはPerlの場合基本的には永続。明示的に ->attr( undef ); や ->attr( $new_value ); とかしないと値がリセットされることはない。どのタイミングでそれをしたらいいのか(していいのか)もわからない。

そのクラスが基本的にデータを貯蔵してるだけのクラスならいいんだけれども、そこでさらに同じクラスでオブジェクトインスタンスを作ろうものなら今度はどの変数が永続して生きてて、どの変数がインスタンスが解放されると解放されて・・・とか考え始めないといけない。

オブジェクト指向で書くなら、オブジェクト本体のライフサイクルに合わせるべきで、これを考え出すと途端に辻褄を合わせるためだけのコードが発生すると思う。

否定ばっかりしていてもしょうがないので、代替策も。

  1. まず基本方針としてクラスデータを持ちたい場合でも極力インスタンス変数で我慢する。
  2. 初期化はそのオブジェクトを初期化するクラスから値を渡す。魔法で勝手に初期化しようとするとすぐグローバル変数に頼りたくなる。
  3. アプリケーション全体を一つのオブジェクトでラップする
  4. クラスデータは例えば*デフォルト値*を指定するのに使う

具体策も書こうと思ったけど、時間が切れたのでとりあえず以下次号!



あくまで自分はどうしてるか、って話ですが、最近はCatalystでなんか書くときはこんな感じで使ってます。

my_catalyst_model_setup.png
色んな事がこの図に詰まっているので、箇条書きしてみる:

  • Model::APIがAPIオブジェクトを作成して、使用時にはModel::APIに対して`find()`というメソッドを使って実際のAPIオブジェクトを持ってくる
  • Schema等はMyApp::Schemaに定義し、Model::APIのアトリビュートとして持っている。cacheも同等。これらの初期化引数は設定ファイルのModel::APIから取れるようにしておく
  • Catalyst::Model::DBIC::Schemaは*使ってない*
  • Model::APIではACCEPT_CONTEXTが呼ばれた時点で、もしまだ初期化が行われていなければ、SchemaやAPIの初期化を行っている。Catalyst::Model::Adaptorは*使ってない*
蛇足だけど、Catalyst::Plugin::AuthenticationでStore::DBIx::Classを使うときはどうしてもDBIC::Schema的な形で認証データが入っているモデルを要求されるので、DBIC::Schemaを使いたくなってしまう・・・が、実は単純にその認証データが入っているresultsetが欲しいだけなので、こんな感じの小さいモデルを一個作ってやりすごしている:
    package MyApp::Web::Model::DBIC::Member;
    use Moose;
    use namespace::autoclean;
    BEGIN { extends "Catalyst::Model" }

    has schema => (is => 'rw');

    sub ACCEPT_CONTEXT {
          my ($self, $c) = @_;
          if (! $self->schema) {
              $self->schema( $c->model('API')->schema ); # Model::API~A~K~B~Ischema~B~R~[~W~B~S~A~A~O~B~K
          }
          return $self->schema->resultset('Member');
    }
    __PACKAGE__->meta->make_immutable();
    1;

ちなみに MyApp::CLI::Hogeとかを書くときには、以下のような感じのでやっている:

  • 全部のAPIが必要じゃないことのほうが多いので適時スクリプトの中身によって必要なSchemaやらAPIやらを作成している。(Model::APIに相当するものはない)
  • WithDBICっていうRoleを作って、DBスキーマが必要な場合のSchema生成等のコードを一元化している
  • 引数等はCatalystの設定ファイルを流用するようなことは*してない*。MooseX::Getopt(もしくはMooseX::SimpleConfig)を使ってコマンドラインで --connect_info=dbi:mysql:dbname=hoge と指定できるようにしている
Orochiとか使えたほうがもっと楽な気はするんだけど、とりあえず依存関係を増やすのもあれなのでこんな感じでやっております。
Spatial インデックス貼れるKVMってことで、おー!と思ったんだが、ぶっちゃけどうしたらいいものか迷う結果に。

自分の作った非同期アプリの中で、AnyEvent::DBI風味(ブロッキングする可能性のあるDBアクセスを別プロセスにわけて順番にクエリを処理しつつ、プロセス間でデータのやりとりを行う)なアクセス方法で試したみたところ、主キーによるデータ取得はMySQLベースのものより30%ほど高速だったものの( 700+ qps -> 900+qps)、Spatialインデックスによる検索が30%ほど逆に落ちてしまった(540 qps -> 380 qps )。MongoDBのspatialインデックス貧弱だなぁと脊髄反射で思ったんだけど、よく考えるとMongoDBのPerlバインディングかもしれん。APIはオブジェクトっぽく洗練されてるんだけどムダが多すぎ。もっと速くできるだろ!この辺りはもう少し調査が必要。

あと、データが無くなる可能性が高い事も様々な文献でみてわかった。どのデータベースも結局のところハードディスクに確実に書き込まれるかなんて時と場合によってわからないんだけど、MongoDBの場合は「そこはちゃんと対応しようとするとパフォーマンスヒットがあるからとりあえずレプリケーションとスナップショットを取るようにして回避してよ」というポリシーがありきで、ディスクに書き込めたかどうかをなるたけ追求する、という事ではないらしい。まぁそれはポリシーの問題だから良しとしよう。しかしそれなら自分は「使い方としてはMemcachedと同じくらいに考えておけ」という事だと受け取った。つまり揮発性のあるデータストレージだということ。

そうなると話は全然変わってくる。MongoDBをプライマリストレージにするわけにはいかない。MySQL等でマスターデータを持っておいて、それを定期的にMongoDBにデプロイする形になるんだろうな。

レプリケーション設定は比較的簡単だと認識。

インストールも超絶簡単。起動も超絶簡単。

ふーむ。色々微妙だな。


まだこう、細かいpros/consがわからないのでなんとも言えないんだけど、とりあえずaio_open/aio_writeと普通のopen/print/closeで同じ事した場合とでベンチマークとか取ってみた。これでいいのかなー

環境はMac OS X 10.5.8, 2.4 GHz Intel Core 2 Duo, 4GM RAM.

Comparing with buffer size 10...
         Rate normal    aio
normal 80.0/s     --   -19%
aio    99.0/s    24%     --
Comparing with buffer size 100...
         Rate normal    aio
normal 80.0/s     --   -18%
aio    97.1/s    21%     --
Comparing with buffer size 1000...
         Rate normal    aio
normal 76.9/s     --   -13%
aio    88.5/s    15%     --
Comparing with buffer size 10000...
         Rate normal    aio
normal 52.4/s     --   -27%
aio    71.9/s    37%     --
Comparing with buffer size 100000...
         Rate normal    aio
normal 15.9/s     --   -63%
aio    42.7/s   169%     --

コードはこちら、githubで。
前のエントリ書いたら宮川さんにオススメ方法を教えてもらった

http://twitter.com/miyagawa/status/10271128928
plackup \
      -L Shotgun
      -MPlack::App::WrapCGI
      -e 'Plack::App::WrapCGI->new(script => "/path/to/cgiscript.cgi")'

だそうです!これなら全ての要件を満たせるもよう!そしてapp.psgiを作る必要ありません。

今これやってみて一つだけ問題点。多分POD入りのCGIは動かない。それと、__DATA__は見てくれるけど、__END__があると動かない。__END__さっき簡単なパッチのpull requestを送っておきました!

なお、-L Shotgunを使うと CGIファイルの中身は実行時までコンパイルされないのでご注意(つまり、plackup -rもいらないってことですね)
dve_deprecated.png

I'll support it for a while should there be any problems , but in general, please consider using Data::Recursive::Encode instead.

Data::Visitor::Encodeの開発を停止します。もうしばらくの間は問題等あればサポートしますが、基本的にはData::Recursive::Encodeを使って下さい。
ミニブログサービスで先行するTwitterと差別化を図った上で新たなサービスを出すのは面倒くさいので、id:miyagawa氏のコードを丸々パクった上でTwitter支援ツール、Hamakiを昨日の夜からちょこちょこと開発しました(本当はTwitter以上に色々できるんですが、そのあたりはまださわってない)

もう自分がやりたい事はだいたいできるようになったのでとりあえず公開です。

まず前提としてTwitterを使ってていくつか不満な点があったわけです:

  • Twitterで特定のアプリの発言がちょっぴり気になるので自分のTLに表示したくない
  • かといって上記アプリの発言している人たちの他の発言は読みたいので、それらの人をremoveするのもしたくない。
  • あと、全般的にtwitterをリロードするような事をしていると非常に効率が悪い
  • かといって、今のところアプリを気に入ったアプリも存在しないし、第一おれは色々カスタマイズしたいんじゃ、ゴルァ。
ってことで、Plackベースの非同期エンジンTatsumakiを使用したHamakiです。こんな感じで見れます。

hamaki.png

ほしかった機能としては、まず自動ロード。一回ページを読み込んでしまえば、その後は置いておけばJSが勝手に新規発言をロードしてページをリフレッシュしてくれます。

これはAnyEvent::Twitter::StreamでTwitter Stream APIを使うことによって実現してます。ちょっと仕組み上、普通のTwitter TLとは表示内容が違ってきますが、僕はこっちのほうが好きだったりします。

あと、フィルタリングができます。例えば、発言内容によって更新を無視したりできるわけです。

ちなみに今回使った設定はこれですね。ごめんね、radiotube。
で、最後に、元々のパクったソースコードにはなかった機能、Twitterに自分の新発言を送信する部分を追加したので、このUIだけでTwitter的な事はほとんどできてしまうことになります。まぁ、DMとか@自分とかはみれないけど、そんなんたまにしかみないし・・・

設定ファイルを書いたらあとは./script/hamaki.pl --configfile=config.yamlってするだけ!サーバーが立ち上がるのでそこにブラウザで /chat/twitterとかにアクセスすれば上記画面がでて勝手に更新してくれますお。

とまぁ色々書きましたが、実際の構造のほとんどはid:miyagawa氏のものですね。元々Tatsumakiに入っているデモ用のスクリプトdemo.plが素晴らしかったので、僕はこれをががっとファイルごとにわけて整理して、スコープが違ってもデータを共有できるようにして、Twitterにポストする機能と、それを実現化するためのTatsumakiフレームワークの拡張を子クラスで行った程度です。なので、miyagawa++, Tatsumaki++, Plack++ なのでした。
神南でお茶中。「daisuke」アカウントでlocal::libを設定してあったのをすっかり忘れて"sudo cpan"してたら、/usr/local/lib/perl5に最新のモジュールが入っているのに、~/perl5 から重複するモジュールが先に検出されておかしな事になる、というエラーにあってしまった。

みんな、local::libしたらsudoとかしないで、そのままcpanだよ!


筆者

CPAN Activity

Profile

daisuke - a.k.a. "lestrrat", Perl hacker at Livedoor Inc, Japan Perl Association 代表理事

このアーカイブについて

このページには、過去に書かれたブログ記事のうち開発カテゴリに属しているものが含まれています。

前のカテゴリはcodeです。

次のカテゴリはFx.Stylesです。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

Powered by Movable Type 4.1