読者です 読者をやめる 読者になる 読者になる

時計を壊せ

駆け出しWebプログラマーの雑記

Amon2で非同期レスポンスを使う方法と、非同期WebAppのハマりどころ

この記事はPerl Advent Calendar 2013の15日目の記事です。

Amon2とは

@tokuhirom さんが開発しているPerl製のWAF*1です。
Plackを軽くwrapしたような軽量でシンプルなWAFです。
現在、Version 6.00がリリースされていますが、Version 3.50からwebsocketのサポートが入り、
その関係でPSGIの遅延レスポンス/ストリーミングレスポンスのインターフェースに対応しています。

Amon2で非同期レスポンスを使う

Amon2::Plugin::Web::Streamingを使う事により非同期でレスポンスを返す事が出来ます。
例えば、index.txを5秒後にrenderして返す場合は以下のようになります。

use strict;
use warnings;
use utf8;
use Amon2::Lite;
use AnyEvent;

get '/' => sub {
    my $c = shift;
    return $c->streaming(sub {
       my $responder = shift;

       my $w; $w = AnyEvent->timer(after => 5, cb => sub {
           my $res = $c->render('index.tt')->finalize;
           $responder->($res);
           undef $w;
       });
   });
};

# load plugins
__PACKAGE__->load_plugin('Web::CSRFDefender' => { post_only => 1 });
__PACKAGE__->load_plugin('Web::Streaming');
__PACKAGE__->enable_session();

__PACKAGE__->to_app(handle_static => 1);
__DATA__
@@ index.tt
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Streaming</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script type="text/javascript" src="[% uri_for('/static/js/main.js') %]"></script>
    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
    <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
    <link rel="stylesheet" href="[% uri_for('/static/css/main.css') %]">
</head>
<body>
    <div class="container">
        <header><h1>Streaming</h1></header>
        <section class="row">
            This is a Streaming
        </section>
        <footer>Powered by <a href="http://amon.64p.org/">Amon2::Lite</a></footer>
    </div>
</body>
</html>

@@ /static/js/main.js

@@ /static/css/main.css
footer {
    text-align: right;
}

このように$c->streamingにcallbackを渡すとそれがそのまんまPSGIの非同期レスポンスとして使えてあとは$responderをすきに料理すればいいというスンポーです。簡単ですね!

ハマりどころ

ブロックする処理を混ぜてはいけない

現実的に、現状は非同期レスポンスを返すアプリケーションを書く場合はAnyEventのイベントループの上で動くことになる為、イベントループを止めるような処理を書いてはいけません。
例えばネットワークIOがあるものなどはAnyEvent::Socketを使うライブラリを使い、sleepやwait(pid)?の代わりにAnyEvent->timer/AnyEvent->childを使うなど、イベントループを止めずに終了を通知させるAPIを使う必要があります。
イベントループを止めてしまうとその間他の処理が一切出来ないため、たとえばDBに1秒かかるクエリを投げた場合、1秒間リクエストの受付すら出来なくなってしまうという状態が起こりえます。

context毎に他のサーバーへのconnectionを保持するとリクエストの度にconnectionが増加して死に至る

HTTPの同時接続数と同じだけmysqlへの接続数が増えるという事になると、
非同期レスポンスが中心になるアプリケーションでは同時接続数が非常に膨らむ為、問題になります。

非同期レスポンス中ではAmon2->contextを使ってはいけない

Amon2->contextはAmon2における現在のcontextオブジェクトがどこからでも取れるという便利な代物ですが、
非同期レスポンスではこれはハマりどころになります。


なぜなら、Amon2の中でのリクエストの開始と終了というのはAmon2::Liteにおけるget '/'などに登録したcallbackが終わるまでの間*2だからです。
つまり、$c->streaming()の中ではcontextは基本的にundefになります。
また、同時に複数のリクエストを捌いているケースではタイミングによっては他のリクエストのcontextを参照してしまう為気付きにくいです。
シーケンスで表現すると以下のような感じです。*3



解決策

Object::Containerなどを使い、globalにリソースを持つ

globalにリソースを持つ事によって、それを複数リクエストで共有する事が出来ます。
もちろん、トランザクションなど、リソースに状態変化が起こる処理を行うと問題になりますが、
そのような処理を行わない場合は検討しても良いかもしれません。
例えば、トランザクションが必要な処理はGearmanなどのWorkerに任せてAnyEvent::Gearmanなどでタスクとして投げてその終了をcallbackで受けるなど、別プロセスで同期的に処理するのが良いかと思います。
もっとシンプルにやる方法もあるとベターかとは思いますが現状ではなかなか辛そうな印象です。
(あるはAnyEvent::DBIが同時に複数のトランザクションが走らないようなアーキテクチャになれば問題は解決するかもです)
他に良い方法があれば教えて下さい!

まとめ

非同期レスポンスむずい!!!!!!!!!!!
明日はid:mackee_wさんです!

*1:Web Application Framework

*2:もっと厳密に言えばAmon2::Web#handle_requestが終了するまでの間

*3:seqdiag.jsを使っています。walf443++