ISUCON14 Perl実装の舞台裏

八雲アナグラ(@AnaTofuZ)

2025/02/15 湘.なんか #2

logo

八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
self.class_name

八雲アナグラ(@AnaTofuZ)

  • 1月からcodeTaktでソフトウェアエンジニア
    • 主戦場がPerlからRubyに変わりました
  • 今日は甲府@山梨からきました
    • 以前は沖縄や京都にいました
      right
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
PR

推しのイベントが大月であるのでよろしくお願いします

right

八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ISUCON14
  • Iikanjini Speed Up Contest

ISUCONとはLINEヤフー株式会社が運営窓口となって開催している、お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトルです

center

八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
Perl実装担当していました

center

八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ISUCONのPerl実装とは
  • ISUCONはお題となるWebサービスを高速化する競技
  • 作問チームが参考実装をまず作成する
    • 最近は参考実装がgolangであることが多い
  • おなじ振る舞いをするWebアプリケーションを他の言語で実装する
    • これをやるのが移植担当
  • 基本的に点数を計算するベンチマーカーが通ればヨシ!
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
最近のPerl実装
  • kobaken(@kfly8)さんがここ数年担当
    • ISUCON10~
  • 今年はちょっと作業できなさそうということで9月くらいに誘われる
    • レビューしてくれるならやります!! ということで移植担当チームに
  • 一時的にギョームで一緒に令和最新版Perlを書いていた経験もあるのでお互いの期待値が一致していたこともあり...
    • なおこの数カ月後にコロナに感染して一時期実装が危ぶまれる
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ISUCONのPerlのノリ
  • 基本的に競技日の最新安定版のPerlを使う
    • おれたちが令和最新版Perl
  • 新機能はベンチマーカーが元気に通る範囲でガンガン使う
  • 基本的に愚直に参考実装を移植する
    • 例えばループの中でクエリ実行している箇所などはその通り再現する
  • 移植において言語ならではのパフォーマンス悪化は極力避けるようにする
    • 遅いライブラリは使わない、本筋にかかわらない高速化テクはガンガンいれる
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ISURIDE
  • 今回のお題は配車アプリ
  • 初期実装はgolang
    • まずはgolangのコードを眺めて移植できそうなところから
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
実装の流れ
  • とりあえず標準機能でできなさそうなところを眺める
    • ランダム文字列の取得部分
    • ulidを使った採番
    • UNIXミリ秒時間に変換
    • 他のコンポーネントに対してHTTPリクエストするくん
  • そこを解決したら後は順次移植、ベンチマーカーが通るまで微調整をする
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ランダム文字列
  • secureRandomStrでランダム文字列が作られている
package main

import (
  crand "crypto/rand"
)

func secureRandomStr(b int) string {
  k := make([]byte, b)
  if _, err := crand.Read(k); err != nil {
    panic(err)
  }
  return fmt.Sprintf("%x", k)
}
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ランダム文字列
  • crypt/randを見るとLinuxではgetrandomが使われている
// Reader is a global, shared instance of a cryptographically
// secure random number generator. It is safe for concurrent use.
//
//   - On Linux, FreeBSD, Dragonfly, and Solaris, Reader uses getrandom(2).
//   - On legacy Linux (< 3.17), Reader opens /dev/urandom on first use.
//   - On macOS, iOS, and OpenBSD Reader, uses arc4random_buf(3).
//   - On NetBSD, Reader uses the kern.arandom sysctl.
//   - On Windows, Reader uses the ProcessPrng API.
//   - On js/wasm, Reader uses the Web Crypto API.
//   - On wasip1/wasm, Reader uses random_get.
//
// In FIPS 140-3 mode, the output passes through an SP 800-90A Rev. 1
// Deterministric Random Bit Generator (DRBG).
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ランダム文字列
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ulidの採番
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
UNIXミリ秒変換
  • 2024-11-17 07:18:51.001みたいな文字列を受け取ってUNIX時間をミリ秒で返す必要がある
  • 当然!! Perlに!! そんな機能はない!!!
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
UNIXミリ秒変換
  • Perlにおいて時間を扱うライブラリはいくつかある
    • Time::Moment
    • Time::Piece
    • DateTime
  • ギョームだとDateTime使いがちだが遅いという問題がある
    • Time::Momentが高速!!なのでTime::Momentでミリ秒変換をする
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
UNIXミリ秒変換
  • 2024-11-17 07:18:51.001のパースは別に関数とかで提供されてない
    • 自分で作る必要があるので正規表現でパースする
    • 何回も使う正規表現なのであらかじめqrでコンパイルして高速化する
  • use constant FORMAT_TIME => qr/\A(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.(\d{1,9})/;みたいにしてパース
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
UNIXミリ秒変換
sub unix_milli_from_str ($str) {
    if ($str =~ FORMAT_TIME) {
        my ($year, $month, $day, $hour, $minute, $second, $fraction) = ($1, $2, $3, $4, $5, $6, $7);
        my $nanosecond = $fraction * (10**(9 - length($fraction)));
        my $tm         = Time::Moment->new(
            year       => $year,
            month      => $month,
            day        => $day,
            hour       => $hour,
            minute     => $minute,
            second     => $second,
            nanosecond => $nanosecond,
        );
        my $milliepoch = $tm->epoch * 1000 + $tm->millisecond;
        return $milliepoch;
    }
    die "Invalid time format: $str";
}
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
最近のPerlの機能をふんだんに利用
  • サブルーチンシグネチャ
    • なんとPerlで関数名 (引数)は革命
  • defer
    • ほぼ書き心地がgolang
sub app_get_rides ($app, $c) {
    my $user = $c->stash->{user};

    my $txn = $app->dbh->txn_scope;
    defer { $txn->rollback; }
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
最近のPerlの機能をふんだんに利用
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
最近のPerlの機能をふんだんに利用
  • key/valueのセットでfor文を回せるヤツ
    for my ($model, $sales) ($model_sales_by_model->%*) {
        push $models->@*, {
            model => $model,
            sales => $sales,
        };
    }
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
  • Perlなのである程度の安全性は用意しておきたい
    • anatofuz & kfly8 での実装なので型に対しての思いがあるのもある
  • JSON変換はCpanel::JSON::XSを採用
    • Perlの世界では型がないので10が文字列なのか数字なのか文脈で変わる
    • Cpanel::JSON::XS::Typesを使うと型を指定できる
  • レスポンスのバリデーションもしたい
    • こういった用途はType::Tinyというライブラリを使える
      • がCpanel::JSON::XS::Typesの型とはライブラリが違うので互換性がない
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
そういえば
  • 今回のISUCONは(なぜか)Perl実装が2つ用意されてます
    • Kossy
      • よくISUCONで使われているSintraっぽいやつ
    • Mojolicious::Lite
      • よく海外で使われているSintraの全部入りっぽいやつ
  • なぜこんなことになったのかというと、試し解き時に...
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
SSE
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
SSE

ISURIDEではクライアントにライドの状態の変化を通知するための2つのエンドポイントが実装されています。

  • ユーザー向け通知: /api/app/notification
  • 椅子向け通知: /api/chair/notification

これらはリファレンス実装では通常のJSONレスポンスを返すエンドポイントですが、SSE(Server-Sent Events)を利用してリアルタイム通知を実装することも可能です。
どちらの実装においても、状態が変更されてから3秒以内に通知されていることが期待されます。

八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
SSE

SSE.....

八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
SSE

SSEのエンドポイントを生やす

八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
SSE

...Perlで!?

八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
Perl/Plackでの通常のレスポンス
  • 配列形式で値を返すとHTTPレスポンスが変える
    • 配列はプリミティブすぎるのでPlack::Responseを使うといい感じに抽象化可能
    • ISUCONで使っているKossyはPlack::Responseをラップしている
my $app = sub {
    my $env = shift;
    return [
        200,
        [ 'Content-Type' => 'text/plain' ],
        [ "Hello World" ],
    ];
};
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
Perl/Plackでの通常のレスポンス
  • 配列形式で値を返すとHTTPレスポンスが変える
    • 配列はプリミティブすぎるのでPlack::Responseを使うといい感じに抽象化可能
    • ISUCONで使っているKossyはPlack::Responseをラップしている
my $app = sub {
    my $env = shift;
    my $res = Plack::Response->new(200);
    $res->content_type('text/html');
    $res->body("Hello World");
    return $res->finalize;
};
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
PerlでSSE
  • PlackではCallbackを返すと非同期にレスポンスを返すことができる
my $app = sub {
  my $env = shift;
  return sub {
    my $respond = shift;
    # do some event stuff
    $event->callback(sub { $respod->([ $code, $headers, $body ]) });
  };
};
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
PerlでSSE

ということはPlack::ResponseではなくCallbackを返すようにすればよいのでは...!?

八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ところがどっこい
  • なんとISUCONでよく使われているWAFのKossyはPlack::Responseでやりとりするのが前提の設計になっている
  • 当日までにPRを作るとネタバレになるので、やるとしたらモンキーパッチしかない
    • めちゃくちゃでかいモンキーパッチを競技用のコードに仕組むか...!?
      • やりたくはないンゴね...
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ISUCON slackで相談
  • ぼく「ちょっとSSEしづらいっすよね...」
  • ??? 「 Mojolicious::Lite(イベント駆動対応しているWAF)にしたら早いよね〜」
  • ぼく 「じゃあそれで」
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ということで
  • ISUCONの歴史上初(?) Perlの実装が2つできた状態
  • Mojoliciousでのサンプル実装をサッと作った
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
みどころ
  • Mojolicousを採用する場合DB周りなどはMojoシリーズのものに統一したほうがパフォーマンスが良い
    • Kossyで使っていた技術スタックからMojoシリーズの技術スタックに差し替え
    • 当然インターフェイスも異なるので通常のままだとソースコードの内容がKossy/Mojoで大幅に異なる
  • 使う場合はサッと切り替えたいので極力ここのギャップを少なくしたい
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
みどころ
  • メタプログラミングを活用してDBアクセスを同じ書き心地に
  • Kossy
        my $owner = $app->dbh->select_row(
            q{SELECT * FROM owners WHERE id = ?},
            $chair->{owner_id}
        );
  • Mojo
      my $owner = $db->select_row(
            q{SELECT * FROM owners WHERE id = ?},
            $chair->{owner_id}
        );
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
みどころ
  • クラスを独自拡張したりMojoの機能でmysqlのショートカットアクセスを生やす
# similar DBIx::Sunny
{
    *Mojo::mysql::Database::select_one = sub($self, @args) {
        $self->query(@args)->array->[0];
    };

    *Mojo::mysql::Database::select_row = sub($self, @args) {
        $self->query(@args)->hash;
    };

    *Mojo::mysql::Database::select_all = sub($self, @args) {
        $self->query(@args)->hashes;
    };
}

helper mysql => sub($c) {
    state $mysql = connect_db();
};
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
みどころ
  • 最終的にJSONを返す部分も同じように
  • Kossy
    return $c->render_json({ rides => $items }, AppGetRidesResponse);
  • Mojo
    return $c->render_json(HTTP_OK, { rides => $items }, AppGetRidesResponse);
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
流石に差がでたところ
  • スタートアップスクリプトの差
    • app.psgi vs app.pl
  • トランザクションの処理
    • Kossyはスコープベースの処理
    • Mojoは宣言的な処理
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
反省
  • エラーログをdumpするのをオミットしていた
    • すんませんすんません...
  • 突然2実装生やすのびっくりしたという感想
  • 前日のリハーサルで文字コード起因のバリデーションエラーが出て泣きながら深夜に治す
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2
ということで
  • ISUCONでPerl移植しました
  • Perlもまだ現代的なWebアプリかけるぞ
  • 来年GraphQLとかgRPCがISUCONで出てきたらPerlは泣いちゃいそう
八雲アナグラ(@AnaTofuZ) ISUCON14 Perl実装の舞台裏 湘.なんか #2