LoginSignup
3
3

More than 5 years have passed since last update.

Twiggy で応答の遅いサーバーを模倣する

Last updated at Posted at 2013-12-04

数年前から Advent Calendar に憧れがあったんですが,今年はライバル?が少なくて初投稿できてひゃっほーい,な @dayflower です。

クライアントの性能テスト用に,単純な応答をゆっくり返すモックサーバをささっと書く必要に駆られたことはありませんか。わたしはあります。

(単純な) レスポンスを,だらだらと返したい!でも大量にリクエストをさばきたい!できれば単純に書きたい!

AnyEvent ベースの PSGI compliant なウェブサーバの Twiggy を使えば,応答を非同期に返すことができるので,低コスト低リソースでそんなことができます。

おことわり

  • おそくておそくて connection すら張れないよ,という状況は模倣できません。
    • Twiggy (というか AnyEvent::Socket) が全力で accept するので。
    • ふつうに event model でないサーバアプリたててください。
    • 503 を模倣するとかなら (もちろん) できます。
  • 1バイト送るのに1秒かかるの!,という状況は模倣していません。
    • 実のところ Twiggy (& AnyEvent) の得意分野なので,やってできないことはありません。responder にとりあえず empty body 返して,チマチマ writer で書き込めばいいのです。

まずはシンプルに

単純なレスポンスを,リクエストからちょい遅れてから返すサーバをつくります。

とりあえず, cpanfile から。

requires "Twiggy";
requires "AnyEvent";

AnyEventTwiggy が依存しているので書かなくてもいいです。なんとなく書きました。

次にアプリケーション本体です。

use strict;
use warnings;
use AnyEvent;
use Plack::Loader;

my $app = sub {
    my $env = shift;

    if ($env->{REQUEST_URI} =~ m{(?: ^ | / ) favicon.ico $}x) {
        return res_404();
    }

    warn "request: " . $env->{REQUEST_URI};

    return sub {
        my $responder = shift;

        my $w; $w = AE::timer(5, 0, sub {
            my $content = '200 OK';
            my %headers = (
                'Content-Type'   => 'text/plain',
                'Content-Length' => length $content,
            );

            $responder->([ 200, [ %headers ], [ $content ] ]);

            undef $w;
        });
    };
};

sub res_404 {
    return [ 404, [ 'Content-Type', 'text/plain' ], [ '404 Not Found' ] ];
}

Plack::Loader->load('Twiggy', host => '0.0.0.0', port => 20080)->run($app);

そのまま carton exec server.pl などとすると動きます。

とても単純だったので拍子抜けしたのではないでしょうか。

いくつかポイントがあります。

  • ふつうの PSGI App のように,リクエストに対してささっと返すこともできます。
    • favicon への応答に関するところです (サブルーチン化する必要はないのですが)
      • favicon のログがでるとうざかったりするので,除外しています。
  • 非同期に応答するには,ふつうの PSGI handler の戻り値 (配列) ではなく,サブルーチンリファレンスを返します。そのサブルーチンに第一引数として,応答用のサブルーチンリファレンス (上記サンプルでの $responder ) が渡されるので,それを使ってレスポンスを返します。
    • 上記サンプルでは,さらに AE::timer (AnyEvent->timer) を利用して,5秒間応答を遅延させています。
  • 本当はサーバが非同期に応答できるかどうか, psgi.streaming 環境変数を検査して判定する必要があります。
    • ですが今回は Twiggy 決め打ちなので,省略しています。
  • という,いままで述べたあたりのことは PSGI Spec の Delayed Response and Streaming Body 項 に全部書いてありますので,そちらを読むことをおすすめします。
  • (Twiggy 決め打ちなので) Plack::Loader 使っていますが, $app をそのまま返せば,ふつうの PSGI ファイルになります。
    • ただし psgi.streaming をサポートした非同期応答に対応した PSGI サーバでないと動きません。

応答時間をランダムに変えたり,相手によって変えたり,もっとまともなレスポンスを返したり,夢がひろがりますね。

応用 (1) カラフルに出力する

クライアントの送信する内容を覗いてみたくなったことはありませんか。

use strict;
use warnings;
use Term::ANSIColor qw( colored );
use AnyEvent;
use Plack::Request;
use Plack::Loader;

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);

    if ($req->path_info =~ m{(?: ^ | / ) favicon.ico $}x) {
        return res_404();
    }

    print "\n";

    # Request Method & URI
    my $meth_uri = sprintf '%s %s', $req->method, $req->uri->as_string;
    print colored([qw( cyan reverse )], $meth_uri), "\n";

    # Headers
    foreach my $field ($req->headers->header_field_names) {
        my $vals = $req->header($field);
        print colored([qw( yellow )], "$field: $vals"), "\n";
    }

    # Query Parameters
    my $query_params = $req->query_parameters;
    foreach my $field (sort keys %$query_params) {
        my $vals = join q{, }, $query_params->get_all($field);
        print colored([qw( green )], "$field: $vals"), "\n";
    }

    # TODO for Body Parameters!

    # Start to respond
    my $delay = 1 + int(rand(3));

    return sub {
        my $responder = shift;

        my $w; $w = AE::timer($delay, 0, sub {
            my $content = '200 OK';
            my %headers = (
                'Content-Type'   => 'text/plain',
                'Content-Length' => length $content,
            );

            $responder->([ 200, [ %headers ], [ $content ] ]);

            undef $w;
        });
    };
};

sub res_404 {
    return [ 404, [ 'Content-Type', 'text/plain' ], [ '404 Not Found' ] ];
}

Plack::Loader->load('Twiggy', host => '0.0.0.0', port => 20080)->run($app);

これで


こんな感じで表示されるようになります。
(おまけで,遅延時間を乱数で決めるようにしています)

Request body の表示は実装していません。JSON のリクエストだったらパースして整形して表示したり,とかやってみてもいいですね。

応用 (2) イライラする Proxy を作る

せっかく非同期に応答できるので, AnyEvent::HTTP を使って,なんちゃってプロキシをつくってみようと思います。

use strict;
use warnings;
use AnyEvent;
use AnyEvent::HTTP;
use Plack::Loader;

my $app = sub {
    my $env = shift;

    warn $env->{REQUEST_URI};

    return sub {
        my $responder = shift;

        my $w; $w = AE::timer(
            3, 0,
            sub {
                http_request(
                    $env->{REQUEST_METHOD},
                    $env->{REQUEST_URI},
                    sub {
                        my ($body, $headers) = @_;

                        $responder->([ $headers->{Status}, [ %$headers ], [ $body ] ]);

                        undef $w;
                    }
                );
            }
        );
    };
};

Plack::Loader->load('Twiggy', host => '0.0.0.0', port => 8080)->run($app);

多段 callback になってしまって,見づらいですがご容赦を (どうすればいいんでしょう)。

本当はリクエストされた HTTP ヘッダの内容をプロキシ先に投げたりとかしないとちゃんと完全には動かないんですが,そのへんは宿題にしましょう。

ダイヤルアップ時代の気分をそこそこ感じられることうけあいです。

参考文献

おわりに

Twiggy, イベント駆動まわりのベースが AnyEvent なので,ほかの AnyEvent を使ったライブラリ (AnyEvent::HTTP とか AnyEvent::IRC とか) と組み合わせて使うにはもってこいだったりします。

さて,明日は lestrrat さんの『普通のデーモンを 1) Server::Starterでホットデプロイ+ 2) slow-restart対応にする』です!……おや,書く人がまだ決まってない!書きたい人はチャンスです!

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3