時計を壊せ

駆け出してからそこそこ経ったWebプログラマーの雑記

Mustache Templateの実装を書いた

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

qiita.com

さて

ということで、ひさしぶりにCPANizeしました。なんか早速bug fixが見つかって早々に0.02です。

metacpan.org

今のPCでは初めてのCPANizeだったようで ~/.pause がなくてちょっと焦った。

なんで?

人生色々。様々があります。 たとえば、やることに追われたり、悩ましい考え事に苛まれたり、やることに追われたり、やることに追われたり、様々があります。

一方でISUCONも迫っていました。PerlでISUCONに勝ちたいので日頃の仕事でなかなか書く機会が減ってしまったPerlを素振りしたくなってきます。 気分転換がてら、Perlでちまちまなんかを作ってみるのは良いかもしれないと思いました。

そんななか、趣味でプログラミングするのにちょうど良さそうな題材として、Mustache Templateが出てきました。

Template::Mustacheが1.0系になってめっちゃ遅くなったことに困っていそうな人をみかけたのです。 確かにめちゃくちゃ遅くなったのは知っていて、たしかに調べてみても代替モジュールによさげなのがなかったのです。

まあ簡単そうだしちょうどよいだろう。ミニマルなものを作っていい感じにしよう。と実装しながらMustache Templateの仕様を追っていきます。

すると、なんとも色々な仕様がでてくるではありませんか。

lambdaとか継承テンプレートとかmustacheで1回も使ったことないぞ!などとと思いましたが、それは自分はText::XslateやText::MicroTemplateを使っていたのでまあそれはそう。

まあ必要なひともいるんだろう、そうなるとこれもたぶんほしいんだろう、これを実装するならこれもまあ実装されていて然るべきだろう、などとうっかり色々実装していくと、あれよあれよと芋づる式に全部の仕様を実装してしまいました。

なんで????

実装

Lexerでトークンに分割して、ParserでAST*1を作り、Compilerでコンパイルするという王道な実装となっております。

Lexerといってもそんなに丁寧なものではなくて、素朴なLexerとしてはたとえば "foo{{bar}}" があったときに、これを ["foo", "{{", "bar", "}}"] と分割するのが一般的かと思います。 もうちょっとParser寄りな感じで、["foo", "{{bar}}"] という感じでタグの構文そのものだけ解釈してトークン分割をする実装にしました。

これにはちょっと理由があって、Mustacheって{{}}というデリミタでタグを構成するのがデフォルトなのですが、このデリミタを変更するタグが存在するので、Lexerはそれを解釈しないと正しくトークン分割ができないのです。すると、まあそこまでしてるのに {{bar}} を分ける必要もなかろうということで自ずとそれらを一緒のトークンにまとめて扱うことにしました。

そして、Parserはその一列のトークンを解釈してMustache Templateとしての文法の構造をデータ構造上の構造に構造化します。いわゆるASTというやつですね。 たとえば、Mustacheには{{#condition}}yes{{/condition}}みたいな複数のタグを使って1つの構造を作るみたいな文法をもっているので、これはAST上もこの分岐の子要素みたいな感じで整理してあげます。

ASTを使うとめっちゃ簡単にレンダリング処理が実装できます。 素朴にレンダリングする事もできると思いますが、今回はせっかくなのでキャッシュしたテンプレートの実行効率を高めるべくCompilerとして実装しようと思います。

Compilerってなんやねんとなると思いますが、これはText::MicroTemplateよろしくテンプレートに合わせたパターンの文字列を生成するPerlのコードを文字列で作り上げてevalすることで、事前に分岐などが最適化されたコードを作る操作を指しています。

Perlコードの生成時はASTを解釈して細かい分岐をたくさん辿りますが、生成されたPerlコードはある程度の分岐が事前に解決されたものになるので直にASTを毎度解釈するより高速に実行することができるという寸法です。

Apache::LogFormat::Compilerなど様々なCPANモジュールでこのアプローチは取られています。 なんなら、こういうコードを簡単に作るためのSub::Quoteってやつまであり、これはMooの実装で使われていたりもします。

ほか、render というテンプレートをパースして即レンダリングするというインターフェースもあるんですが、このケースではコンパイルしたものが1度実行したら廃棄される前提がありかつコンテキスト変数*2がコンパイル前にすでに決定しているという特徴があるので、コンテキスト変数をヒントにASTを最適化することが可能です。 ということで、そういう処理を挟んであり、うまくいくケースではこの最適化をやらない場合と比べて3倍くらい速くなるようになることがわかっています。

ほかにも、正規表現を最適化していたりなど、地味な高速化を測っている箇所がちょいちょいあったりします。

出来栄え

github.com

このテストケースをすべてPASSしています。やったね。 ついでにテストカバレッジもステートメントレベルでは100%*3で、ほかも90%台後半まで網羅してあります。

パフォーマンスも簡単なテンプレートをレンダリングするベンチマークで比較してみました。 Template::Mustacheに比べてパースの速度が10倍、レンダリングする速度がパース済のテンプレートをキャッシュしているケースで3倍、キャッシュしていないケースで40倍となることがわかっています。 やったね。

なお、Mustache::Simpleというモジュールがあるのですが、これはテストケースをpassしなかったので比較していません。(存在しない変数を無視せずにエラーにしてくる仕様非準拠な挙動だけ確認してあとは確認してない)

ということで、まあそれなりに実用に耐える品質ではあろうと思ったのでCPANnizeしておきました。

これを作る時間でISUCONの素振りすればよかったのでは?

ISUCONの素振りってまとまった時間が必要じゃないですか……。 これ細切れの微妙な時間でちまちま進めたのです。素振りもしたかった……。

そんなわけで

まあ、完成したのでもしご入用の方はお試し下さい。

*1:Abstract Syntax Tree

*2:テンプレートに与える変数

*3:普通は絶対にこのパスは通らないところとかは除いているのでちょっとズルかも

ISUCON13にPerlで挑んだ

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

qiita.com

チーム「銀河鉄道の昼」としてISUCON13にPerlで挑みました。 最終的に2台構成で最終スコア7083、最高スコアは1台構成での9381でした。

上位30チームが50000点弱以上なので、残念ながら惨敗と言って差し支えない結果です。 くやしい……。上位チームの皆さんおめでとうございます。

何をやったのか

なんやかんや、足回りを整えたりトラブルシューティングしたりで2時間くらい食ってたきがします。

StatsHandlerのN+1解消

順位を計算する処理があり、その順位とはtipの総額とリアクションの総数によって順位付けられていました。 この際のもとの実装は以下のような感じです。

    # ランク算出
    my $ranking = [];
    for my $livestream ($livestreams->@*) {
        my $reactions = $app->dbh->select_one(
            q[
                SELECT COUNT(*) FROM livestreams l
                INNER JOIN reactions r ON r.livestream_id = l.id
                WHERE l.id = ?
            ],
            $livestream->id
        );

        my $total_tips = $app->dbh->select_one(
            q[
                SELECT IFNULL(SUM(l2.tip), 0) FROM livestreams l
                INNER JOIN livecomments l2 ON l2.livestream_id = l.id
                WHERE l.id = ?
            ],
            $livestream->id
        );

        my $score = $reactions + $total_tips;
        push $ranking->@* => Isupipe::Entity::LivestreamRankingEntry->new(
            livestream_id => $livestream->id,
            score => $score,
        );
    }

    my @sorted_ranking = sort {
        if ($a->score == $b->score) {
            $a->livestream_id <=> $b->livestream_id;
        }
        else {
            $a->score <=> $b->score;
        }
    } $ranking->@*;

    $ranking = \@sorted_ranking;

    my $rank = 1;
    for (my $i = scalar $ranking->@* - 1; $i >= 0; $i--) {
        my $entry = $ranking->[$i];
        if ($entry->livestream_id == $livestream_id) {
            last;
        }
        $rank++;
    }

うーん、では中間テーブルを作りましょう。

RedisのSortedSetを使うことも考えましたが、scoreが同順であった場合はlivestream_idの昇順となるように実装されているためそのへんをうまくやれるだけのRedis力が僕にはなかったです。 (出来ないと思っていた。実はできるという話を聞いたけどどうやってやるのか知らないので知っているひとはコメントで教えてください。)

中間テーブルをこんな感じで作って

-- スコア
CREATE TABLE `user_scores` (
  `user_id` BIGINT NOT NULL PRIMARY KEY,
  `score` BIGINT NOT NULL,
  `user_name` VARCHAR(255) NOT NULL,
  INDEX ranking_idx (`score` DESC, `user_name` DESC)
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

CREATE TABLE `livestream_scores` (
  `livestream_id` BIGINT NOT NULL PRIMARY KEY,
  `score` BIGINT NOT NULL,
  INDEX ranking_idx (`score` DESC, `livestream_id` DESC)
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

更新時によしなに更新していくと、以下のような感じで順位がSQLだけで取得できるようになりました。

    $app->dbh->query('SET @r = 0');
    my $rank = $app->dbh->select_one(q!SELECT `rank` FROM (SELECT livestream_id, score, @r := @r+1 AS `rank` FROM livestream_scores ORDER BY score DESC, livestream_id DESC) a WHERE a.livestream_id = ?!, $livestream_id);

Window関数を使って DENSE_RANK() でなんとかする手もあるかもでしたが、うまくインデックスが効かないのでこういう感じにしました。

たしかまあまあスコアは上がった気がします。 しかし、なんやかんやハマって2~3時間くらいこれで溶かしてしまった気がします。

reservation slotsのチューニング

元々の実装はこんな感じでした。

    # 予約枠をみて、予約が可能か調べる
    # NOTE: 並列な予約のoverbooking防止にFOR UPDATEが必要
    my $slots = $app->dbh->select_all_as(
        'Isupipe::Entity::ReservationSlot',
        'SELECT * FROM reservation_slots WHERE start_at >= ? AND end_at <= ? FOR UPDATE',
        $params->{start_at},
        $params->{end_at},
    );

    for my $slot ($slots->@*) {
        my $count = $app->dbh->select_one(
            'SELECT slot FROM reservation_slots WHERE start_at = ? AND end_at = ?',
            $slot->start_at,
            $slot->end_at,
        );
        infof('%d ~ %d予約枠の残数 = %d', $slot->start_at, $slot->end_at, $slot->slot);
        if ($count < 1) {
            $c->halt(HTTP_BAD_REQUEST, sprintf("予約期間 %d ~ %dに対して、予約区間 %d ~ %dが予約できません", TERM_START_AT, TERM_END_AT, $params->{start_at}, $params->{end_at}));
        }
    }

この手のはロックする範囲をいかに絞れるかがキモっぽいという直感と、幸いにもslotは減ることはあっても増えることがない仕様であることがわかったため、 すでに空になっている予約枠を予め除外してロックを取ることでロックの競合をなるべく抑えるようにしてみました。

    # 予約枠をみて、予約が可能か調べる
    my %slots = @{
        $app->dbh->selectcol_arrayref(
            'SELECT id, slot FROM reservation_slots WHERE start_at >= ? AND end_at <= ?',
            { Columns => [ 1, 2 ] }, $params->{start_at}, $params->{end_at},
        )
    };
    if (any { $_ == 0 } values %slots) {
        $c->halt(HTTP_BAD_REQUEST, sprintf("予約期間 %d ~ %dに対して、予約区間 %d ~ %dが予約できません", TERM_START_AT, TERM_END_AT, $params->{start_at}, $params->{end_at}));
    }

    my $txn = $app->dbh->txn_scope;

    # NOTE: 並列な予約のoverbooking防止にFOR UPDATEが必要
    my $overed_slots = $app->dbh->select_one('SELECT COUNT(*) FROM reservation_slots WHERE id IN (?) AND slot = 0 FOR UPDATE', [keys %slots]);
    if ($overed_slots) {
        $c->halt(HTTP_BAD_REQUEST, sprintf("予約期間 %d ~ %dに対して、予約区間 %d ~ %dが予約できません", TERM_START_AT, TERM_END_AT, $params->{start_at}, $params->{end_at}));
    }

これも、たしかまあまあスコアは上がった気がします。

どうして複数台構成にした途端にスコアが落ちたのか

MySQLを2台目に追い出したのですが、どうやらその途端にDNS Serverのスループットが改善してDNS水責め攻撃の餌食となったようです。 2台構成が動いたのが終了10分前とかだったのでそこから元に戻すのは諦めました……。

次回に向けて

今回は素振りがほとんど出来なかったので素振りする時間をなんとか確保して、トラブルシューティングに費やす時間を減らし、チューニングの手札を増やしておきたいです。 今回はnginxでキャッシュさせるといったこともほとんど出来ず、またnginx luaを使えば楽にいきそうな部分もあったのでそういったこともできるようにしていきたい。

次回もPerlで挑む覚悟で、実装移植がなかったら自前でいちからコードを書いてでもPerlで戦おうと思います。

感想

移植はモダンなPerlの機能がふんだんに使われており、面白い実装でした。(Entityクラスは正直いらなかった気がしますが……。) 問題もボリューム満点で面白く、素朴なチューニングでスコアがちゃんと上がるような問題に仕上がっていてスコアが上がる楽しさも感じました。 運営の皆さんには感謝の気持ちでいっぱいです。ありがとうございました。

YAPC::Kyoto 2020~2023の思い出

3年前、2020年の2月にこんな記事を書きました。

blog.perlassociation.org

まだその脅威の全貌が明らかにならないままに刻一刻とタイムリミットは迫り、 YAPC::Kyoto 2020のために積み上げてきたものとそれをその状況下で実施することのリスクを直視して、頭を抱えました。

自分たちの本業は当然ながらイベントの運営ではありません。 その一方で、イベントの開催には様々な準備が必要になってくるため、それなりの労力と時間がかかります。 それを誰もが持て余しているわけがないように、スタッフも暇だからそれをやっているわけではないのです。

では、それをどこから捻出するかというと、趣味や大切な家族との時間を削ったり、睡眠時間と体力を削ったり、業務調整をして同僚に助けてもらうなど、人によってその程度の差はあれど全く何も犠牲にしていないという人は少ないでしょう。 そういった、掛け替えのない時間をもらうことによってYAPC::Kyoto 2020の準備は進められてきたことを、自分はよく理解していました。

イベントは一点物です。 イベントの準備のほとんどは「その日」のためにしか使えないものであり、つまりイベントを中止するという判断はそのために準備したもののほとんどを無意味なものにしてしまうことを意味します。 例を挙げると、イベントの内容は会場や日程に大きく左右されます。必ずしも同じスペースが同じ条件で借りられるとは限りませんし、どこでどのようなことをやろうとしていたかという構想も簡単に崩れてしまいます。ゲストとして呼ぶ人々の都合がつくかもわかりません。

和気あいあいと、こんなことが出来たら面白そうだ、こんなことができるとみんな嬉しいのではないか、という具合で盛り上がっていた運営メンバーの様子を思い浮かべながら、 中止を考えざるを得ないこの変化にどのように対処するか、中止以外になにか打つ手はないか、現状でベストな打ち手はなにか、といったことをひたすら考えていました。

そして、2時間近い話し合いをオンラインで行って結論を出し、以下のようなブログエントリを書きました。

blog.perlassociation.org

あえて「開催見送り」「延期」という言葉を使い、「中止」とは書きませんでした。

イベントの延期は、それを言うこと自体は簡単ですが、それを実際に行うことは簡単ではありません。 その準備のほとんどが無駄になったことにもう一度前向きに取り組むためには意思が必要です。 そして、延期を提案するということは「いままでと同じくらいの労力と時間をもう一度ください」と言うことに等しいので、延期したいということへの共感が必要です。

しかしながら、イベントのスタッフをやった報酬は、イベントを開催してそこに参加したり関わった方々からの感謝、それだけです。 そして、これは開催することで初めてそれを得ることができます。まだそれがない状況で、延期としましょうと言ったところでそれは本当にできるのでしょうか? 最初はよくても、燃え尽きる、折れる、といった形容をされるような状態になってしまっても不思議ではないと思います。

とりあえず言っておけば良いという意見もあるかもしれませんが、もし期待値を高めてそれを裏切るという行為になってしまうと、信頼を毀損してしまいます。 自分自身のことであればそれでもよいかもしれません(よくはない)が、YAPCという世界中で何代にも渡って主宰が交代してきたカンファレンスは自分だけが築き上げたものではない、これまでYAPCをつくりあげてきた人々から受け継いだ大切な財産です。

「延期」と言うからにはたとえ誰も手伝ってくれなくてももう一度やる覚悟を持つ、そういう気持ちが自分にとっては必要でした。 YAPC::Kyoto 2020のメンバーは延期に対して前向きに向き合ってくれて、延期と決めた話し合いでも「絶対やるぞ!」となって終わるくらいに前向きでいてくれたので、それに背中を押してもらったことであまり迷わずに延期と言うことができました。

そうなれば、コロナ禍でイベントがやりづらくなっても、なんとかYAPCを開催するための土台を維持したいとなります。 コミュニティは生き物であり、集まるきっかけが失われると死んでしまいます。

オンラインでそれをどうするか、悩んだ末にJapan.pm 2021、YAPC::Japan::Online 2022というイベントをやりました。 このあたり葛藤とそこからどうしたかはイベント振り返り Meetup #2というイベントでも話したとおりです。

speakerdeck.com

そして、結果的にはこのようにYAPC::Kyoto 2023として復活を果たしました。 いろいろと不安を書いていましたが、実際はどうだったか。

なんと、YAPC::Kyoto 2020を手伝ってくれていたほとんどのスタッフが引き続き手伝ってくれました。そうでない方も3年のあいだに状況が変わって手伝えなくなってしまった、当日の予定が合わなくなってしまった。といった方ばかりで、自分の知る限りでは100%のメンバーが延期開催の実現に引き続き前向きでいてくれました。 先に書いたような不安は幸いにも杞憂に終わり、id:azumakuniyuki さん id:papix さんを中心に多くのメンバーが高いモチベーションでまた新しいカンファレンスを作ることに尽力してくれたのです。

結果的に、(ブランクが空いたことなどを加味しても)残念ながらトラブルは少なくはありませんでしたが、 それを踏まえても「とても良かった」と参加した人々に言ってもらえるような、多くの人の心に残るカンファレンスとなったのではないかと思います。 ぼくも、とても良いカンファレンスになったのではないかと思います。

この彼らの前向きさとこの結果に力をもらって、自分たちにとって掛け替えのない場をこれからも続けるために、自分にできることを引き続きやっていきたいと僕も心を新たにすることができました。 そんなわけで、YAPC::Kyoto 2023を作り上げてくれたメンバーに僕は感謝をするばかりです。本当にありがとうございました。


いろいろと思い出して感慨深いものがありました。という話でした。

YAPC::Kyoto 2023ではぼくはスタッフ業として溢れたタスクを拾ったり、あるいは #ぶつかり稽古 のリバイバルイベントの司会進行兼実況解説役やLTもやりましたが、その話はまた別で書こうかと思います。

WEB+DB Press Vol.119のPerl Hackers Hubに寄稿しました

WEB+DB PRESS vol.119の表紙
WEB+DB PRESS vol.119

Perl Hackers Hubも第64回ということでキリが良いですね。 個人的にはありがたいことに3度目のPerl Hackers Hub掲載です。

今回は「少しマニアックなPerlのテクニック」ということでPerlにまつわる少しニッチなTips集のようなものを書かせていただきました。

特殊変数を使って短くシンプルにコードを書き上げるテクニックであったり、Perl組み込み関数のsyscallを使って任意のシステムコールを呼び出す方法などを紹介します。 もっとPerlを使いこなしたい!と思っている方へのヒントとなるような内容を届けられたらと思っております。 もちろん、CPANモジュールを使わないことを推奨するわけではなく、あくまでもPerl本体の機能だけでもここまでのことができるぞという紹介になっております。

Dockerコンテナに潜ってDockerfileのデバッグをしたり、FaaSでprintデバッグをしたりすることも多い昨今ですが、まだまだオンプレミスの環境やその流れで構築されたIaaSの環境も多いことでしょう。 そのような環境でいざというときにログやデータを調査するための道具が少し足りない!というときに、CPANモジュールなどの外部パッケージのインストールが気軽にできる環境というのはそう多くはありません。

そのような環境で、外部パッケージに頼らず、多くの環境に最初からインストールされているPerlそのものの機能を使いこなすことができると、その際の解決策の選択肢の幅が広がります。 Perlそのものの機能も使いこなせれば、CPANモジュールなどの外部パッケージのインストールをする必要もなく、より素早く問題を解決できる場面もあることでしょう。 その際に役に立ったり、あるいはそのヒントになるようなものを提供出来たらと思っています。

そういうわけで、今回のテーマを書かせていただきました。 明日10/24に発売となりますので、もし良ければ書店等でお買い上げいただき、読んで頂けますと幸いです。

編集を担当して頂いた技術評論社の稲尾さんをはじめ、直接監修頂いた牧さん、ほか勤務先など各方面の関係者の方々にも様々な面で協力してもらい助けて頂きました。 この場を借りて改めて御礼申し上げます。ありがとうございました。

Cartonで不要になったモジュールをcpanfile.snapshotから削除したい

こうすることで消せた:

rm -rf local
carton install --deployment
carton install

なんか良い方法ないかなー。

追記: 2015-06-11 17:03

CIでcpanfile.snapshotを生成すれば解決しそう

参考: www.songmu.jp

YAPC::Asia Tokyo 2014で個人スポンサーしつつボランティアスタッフしつつperl5 meta programmingについて喋りつつreject conでWorkmanについてLTしました

タイトルが長い。

YAPC::Asiaとは

ここを読むと良い。

概要 / ABOUT - YAPC::Asia Tokyo 2014

Perlのトーク殆ど無いじゃんという意見についての個人的な見解

いまどき色々な言語が開発に利用されるのは当たり前だし、特定の言語だけに絞るメリットはあまり無いのではって思う。
Perlを使ってい(る|た)人が興味が深い他言語/他分野の話をしてもPerlの人には面白いし役立つしべつに良いじゃないか。楽しみ方はいくらでもある。

YAPC::Asiaと私、あるいは何故ぼくは個人スポンサーになったか

YAPC::Asiaは2011年から毎年参加しているが、質が高く素晴らしいイベントであり、良いコミュニティになっていると思う。
僕は毎年ここで開発への情熱を貰っている。色々な技術に触れてわくわくする。エンジニアとしての将来のキャリアに希望が持てる。色々な人に来てもらいたい。
そういう素晴らしいイベントも、継続するためにはやはりお金が必要だと思う。個人で出来る事は少ないけれど、少しでも運営の助けになればと思い、小額ではあるが個人スポンサーをさせて頂いた。

ボランティアスタッフをやった

2012年から個人スポンサーはやらせて頂いていたが、今年からJPAYAPC運営委員会がYAPCの主催を行うということになり、 運営リーダーも牧さんから和田さんに変わるなど、新体制での運営となった。
いろいろ大変なこともあるかもしれないので少しでも力になれればと思い、コアスタッフとしてお手伝いすることも考えたが、 ボランティアスタッフすら経験していないのに勝手が分かるはずが無いと思い、まずはボランティアスタッフとしてお手伝いしていこうと思ったので参加を決めた。

作業としてはコアスタッフの人々が殆どの事を決めてくれていたので、殆ど手を動かすだけで済んだ。
具体的には、特典のTシャツを畳んだり、マイクを質問者のもとに届けたり、懇親会の参加者への案内をしたり、イベントホール外で飲食が無いよう*1grepしてblockしにかかる仕事とかをしていた。

今年はひたすらスタッフをやってみようと思っていたので、トークは殆ど聞かなかった。*2
ぼくは短大の頃に文化祭の主催をするなどしていたので、懐かしい気分になれた。一部でもYAPC::Asiaをつくることに貢献できることに喜びがあった。
終われば懇親会やHUBでいろんなひとと飲んで騒いでできたし、keynote sessionのときは仕事は全部終わっていたので生で見た。楽しかった。

トークについて

Perl5 meta programming - YAPC::Asia Tokyo 2014ということで、Perl5でメタプログラミングをするための基礎的な要素について初学者向けに実際のコードを交えつつ解説した。(スライドはリンク先に掲載している) UNIVERSALとAUTOLOADは隣り合わせにするつもりだったがスライドを整理しているうちにズレてしまっていたなど、いろいろ不手際はあったが、少しでもpure perlメタプログラミングをするための方法について伝われば幸いである。 本当はもう少し実践的な話もしたかったが、これは別の機会でも良いだろう。 なお、このトークのスケジュールはPerl入学式Githubの裏側の話Perlあるある、そして あやかNowという人気トークの裏にあったので、当日はそんなに人は来ないだろうと目論んでおり、非常に気を抜いてリラックスして発表していたが、最終的には立ち見で聴いて頂く方も出てくるなど思いのほか多くの人に来て頂き本当に有難い限りだった。ありがとうございました。 毎年YAPCに来るたびに英語力の無さを実感していたので、英語力を鍛える意味を込めつつ英語でスライドを書いたが、もう一つ海外ゲストの人との話のネタにしたいという思いもあった。しかし、案の定というかGithubの裏側の話に吸い込まれたようでそれは叶わなかった。来年までにはもうちょっと喋れるくらいの英語力とコミュ力を身に付けたい。

LTについて

reject conでLTをした。

karupanerura/Workman · GitHub

これのデモをひたすらやった感じ。酔っ払ってたのでだいぶテキトーだった。 デモをやる裏で動画をひたすら流していて最高にカオスだった。 自分でやっておいて自分で吹き出してしまってしょうもない感じだった。

WORKMAN_CM - YouTube

YAPC Ramen Challenge

きました。

やりました。

f:id:karupanerura:20140904202906j:plain

dump sql type name. · b57ce92 · karupanerura/p5-Teng · GitHub

総括

めっちゃ楽しかったし継続していきたいので来年も(ボランティア)?スタッフやりつつ参加したいです! 関係者の皆様おつかれさまでした!ありがとうございました!来年もやりましょう!(手伝います!

*1:諸事情でイベントホール以外では飲食の許可が得られていなかった

*2:良い同僚が代わりに聞いて、社内でフィードバック会を開いてくれる事になっていたので、それを聞きつつスライドやYoutubeで補完すればいいやとなった。

Job-Queue Workerの実装を管理するdaemon

Message Queueとか

だいたいみんな、Message QueueとしてGearmanとかQ4MとかResqueとかRabbitMQとかZeroMQとかまあたくさんあるけど、なにかを使っていると思う。 Perlの人だとQudoとかTheSchwartzとかをつかっている人も多いと思う。
でも、preforkなworkerを実装するとなるとSignal処理とかをちゃんとやるのが意外と難しい。 下の2つのスライドを読むと難しいんだなぁという事がなんとなくわかるとおもう。

graceful shutdownとかgraceful restartとかは欲しいし、max_reqs_per_child的なこともしたいし、時間が掛かり過ぎているjobはリトライして新しいworkerで処理させたいとかそういう要求がある。
そこで、そのへんが共通化されたプロダクトがあると良いのではないかと考えた。

Dainamoについて

なお、perlにおいては似ている既成のプロジェクトとしてDainamoというものがある。*1

これは以下のような事ができて、以下のようなメリットがある。

  • Message Queueの種類をあまり考慮せずにコードが書ける
  • サーバー毎にworker数を設定し、Profileという単位でweightをかけてworker数をバランスする
    • サーバー毎のプロセス数をスペックに合わせて管理しやすい
    • Profileを1つのプロジェクトで複数つくって即処理したいものはweightを多めにするといったこともできる
  • 設定ファイルからProfile::Group(Profileをまとめたもの)を複数読み込み、全てのworkerを起動できる
    • 複数プロジェクトが同居するケースとかのセットアップが楽

これは業務で運用しているが、とても便利な反面、困ったこともいくつかでてきた。

  • dainamo毎にworker数を設定し、Profileという単位でweightをかけてworker数をバランスする
    • 実際にProfileに割り当てられるプロセス数が分かりにくい。
    • 他のProfileの影響を受けてProfileのworker数が変化するので複数Projectの同居などを慎重に検討しなければならない。
  • graceful_shutdown_timeout(default:10秒)を超えるとSIGKILLをworkerプロセスに送る
    • 容赦が無さすぎて失敗時に走らせたい処理とかが出来ない
  • Proc::Daemonでdaemonizeする機能がある
  • 3段階のforkとなっていてデバッグし辛い
    • master process -> manager process (profile毎に1つ) -> worker process (実際のjobを捌く)
    • (依存モジュールが古いサーバーとかだと)manager processだけゾンビってるとかが稀にある

Workmanについて

というわけで、もっとシンプルなものが欲しいと思っていた。そこで最近、Workman というプロジェクトを開始した。 Job-Queue Worker frameworkと銘打っている。だいたい以下のような事が出来る事を目標にしている。

あらゆるMessage Queueを同一のインターフェースで扱う事が出来る

  • 時代の変化で古いMessage Queueから新しいMessage Queueに移行するときとかにあまりめんどくさい事考えたくない。
  • 代わりに、一部のMessage Queueに特有の機能は使いにくくなるがそれは許容する。
  • Queueバインディングの仕様を満たすためのテストを提供してQueueバインディングを開発しやすくする。

単一のProfileに対して単一のServerを立てる

  • (dainamoと比較して)manager process単位でサーバーを立てる。
  • 複数建てたければ建てればいい

上に挙げた2つのスライドのノウハウを取り入れる

  • シグナルハンドリングの不具合とかを心配したくない。

graceful shutdown / graceful shutdown をサポートする

  • 1つだけ時間がかかっているjobがあったとしても他のworkerはすぐrestartして欲しい
  • かといっていつまでも死なないのも困るのでgraceful_shutdown_timeoutを超えたら強制終了させる

graceful_shutdown_timeoutを過ぎて強制終了させるときにSIGABRTを送る

  • worker内ではWorkman::Server::Exception::ForceKilledがthrowされるのでそれを好きにゴニョる
  • on_abortというhookがあるので再enqueueなどの処理を書いてあげる

max_reqs_per_childしてくれる

  • あたりまえのようにサポートしてほしい

daemonizeをサポートしない

テストを充実させる

  • 特にシグナル処理の安全性やWorkerのライフサイクルとかは手動で動作確認とか結構厳しいし自動化するべきである

CodeRefを簡単にworkerにしたい

  • ちょっとしたプロジェクトとかだとあると便利なはず。Amon2::Lite的な発想。syntax sugar用意してもいいかも。
  • 当然classとしてもタスクを定義出来るようにする。

実装状況

実は、だいたいコア部分はできている。 たまにテストがコケる事はあるが、sleepとか絡むので仕方ないのかなともおもう。 以下のような感じで書くともうだいたい動く。

サーバー側

use strict;
use warnings;
use utf8;

use Data::Dumper;
use Workman::Server;
use Workman::Server::Profile;
use Workman::Queue::Gearman;
use Workman::Task;

my $queue   = Workman::Queue::Gearman->new(job_servers => ['127.0.0.1:7003']);
my $profile = Workman::Server::Profile->new(max_workers => 10, queue => $queue);
$profile->set_task_loader(sub {
    my $set = shift;

    warn "[$$] register tasks...";
    $set->add(
        Workman::Task->new(Echo => sub {
            my $args = shift;
            warn Dumper $args;
            return $args;
        })
    );
    $set->add(
        Workman::Task->new(Abort => sub {
            my $args = shift;
            die Dumper $args;
            return;
        })
    );
    $set->add(
        Workman::Task->new(Busy => sub {
            my $args = shift;
            sleep 1 for 1..1000;
            return;
        })
    );
});

Workman::Server->new(profile => $profile)->run();

set_task_loaderはWorkerプロセスで実行されるのがミソ。
この中でモジュールのロードを行えばgraceful restartしたときでもTaskが再読み込みできるし、 メモリを節約したいなら先に読んでおいてCoWを効かせてメモリを節約することもできる。 重いモジュールは先に読ませておくとかそういう小手先の諸々ができる。

クライアント側

use strict;
use warnings;
use utf8;

use Workman::Client;
use Workman::Queue::Gearman;
use Try::Tiny;
use Data::Dumper;

my $queue   = Workman::Queue::Gearman->new(job_servers => ['127.0.0.1:7003']);
my $client = Workman::Client->new(queue => $queue);

$client->enqueue_background(Echo => { msg => 'hello' }) for 1..10000;

こんなかんじで、共通化のインターフェースでenqueueできる。 ドキュメンテーションとか、configファイルから読む機能とか、簡単にworker serverを立ち上げるためのコマンドとかはこれからという段階だが、なんとなく雰囲気は伝わるかなと思う。 どうだろうか?

*1:gearman限定であればMasayuki Matsuki / Gearman-Starter - search.cpan.orgがある