ISUCON11予選のNode.js実装を書いた

ISUCON11 予選おつかれさまでした。

ここ数年は参加者として予選敗退を繰り返してきたのだけど、今年はちょっと違う関わり方をしてみるか、と思い 「参考実装の移植」に立候補してみました。

isucon.net

Node.js担当として採用していただき、ちょっと不安もあったので id:hokaccha 氏にレビュアーとしてついてもらって、言語移植チームとして加わりました。

Node.js 実装

github.com

中身としては素朴な express のアプリケーションで、TypeScriptで実装しました。 mysql clientには mysql2/promise を使うことで async/await で簡潔に書けそうだったのでそれでやってみました。

www.npmjs.com

行数としては元実装の Go 1262行に対し 1243行とほぼ同等、それなりに丁寧に型定義などを書きつつ 元実装を忠実に再現したものになったかと思います。

昨年のisucon10のリポジトリなどをとても参考にしつつ、今回の自分の実装が未来の実装者の参考にもなるように、と気をつけて書いたつもりです。

1組だけですがNode.js実装で本選にも残ったチームも居たようで良かったです。

isucon.net

開発環境

移植作業をする時点で既にGoの参考実装やbenchmarker、そして開発環境がある程度できあがっていて、作業を進めやすくてとても助かりました。このへんは作問チームのレベルの高さをすごく感じました。

複数のコンポーネントからなるそれなりに複雑なアプリケーションながらも、docker-compose.yml や Makefile がしっかり用意されていて、そこに自分の実装する言語用の環境を用意できればコマンド一発でWebアプリが起動できるし benchmarkerを走らせたりもできる。 benchmarkerは -no-load オプションで負荷走行前のアプリケーション互換性チェックシナリオを走らせるので、まずはそこがすべて通るようになれば大体の移植は出来ていると判断できる、という具合。

勿論CIでもテストが走るようになっていて、それらが通っていてさらに作問チームメンバーやアドバイザリーからレビューを受けた上でapproveされたらmergeできる、といったルールもしっかり整備されていて、とても体験の良い開発環境でした。

実装についての疑問や相談もSlack上でいつでも作問メンバーが素早く回答してくれて、とにかく素晴らしいチームだな、と思いました。 このチームの人たちと同じプロジェクトに取り組めたというだけで 今回応募してみて本当に良かったと思っています。

Contributions

Node.jsの実装も一通り出来たところで 他の言語実装も出揃ってきていたので試しに動かしてみて、細かいエラーが出ていたところを修正したりもしました。

「オレでなきゃ見逃しちゃうね」的な細かいものとか

あとはPerl書ける人が少なかったようなので「いちおう私も多少Perlの経験あるのでレビューくらいなら」とちょっと見てみたり

benchmarkerの細かい挙動について指摘したりライブラリのバグをついたりもしました

あと実際に本番前々日くらいにサーバ上で各言語の初期実装に対してbenchmarkしてみたところ、何故かNode.jsがGo実装よりも2倍以上高いスコアが出た(!)という謎現象が見つかり、調査の結果それはNodeがたまたまbenchmarkerの秘孔をつく動作をしていたことが分かり修正されたのですが、そういうのを見つけることが出来るという点でも複数言語での移植は意義があるのだなぁと思いました。

tagomorisバグ

余談。

Perlの実装を手伝っているときに、benchmarkerを走らせていると何故か予期せぬところで 401 を返していてチェックが失敗するという現象が起きていた。

401 ってことはcookie-sessionまわりだよな〜 でも特に変なところとか無いはずだしな〜 と id:kfly8 氏と一緒に見ていて、ところで Plack::Middleware::Session::Cookiesecret"tagomoris" とかじゃなくて 今回は "isucondition" (もしくは SESSION_KEY 環境変数) で統一するって話じゃないですかと指摘してそこだけ直してもらった

https://github.com/isucon/isucon11-qualify/pull/1099/commits/57f165c73f92bac69a449912d8cb8118b05bf38c

 builder {
     enable 'ReverseProxy';
     enable 'Session::Cookie',
-        session_key => $ENV{SESSION_KEY} // 'isucondition_perl',
+        session_key => 'isucondition_perl',
         expires     => 3600,
-        secret      => 'tagomoris';
+        secret      => $ENV{SESSION_KEY} || 'isucondition',
     enable 'Static',
         path => qr!^/assets/!,
         root => $root_dir . '/../public/';

この変更を入れたら 先述の 401 のエラーが出なくなり…。

「えっ cookie の secret key を "tagomoris" から違う文字列に変えただけでバグが直ったの!?」と混乱した、という出来事。

種明かしをするとこの変更は secret 指定の末尾が ; だったのを , に変えてしまっているというミスが含まれていて。

-MO=Deparse してみると 変更前は

&builder(sub {
    enable('ReverseProxy');
    enable('Session::Cookie', 'session_key', 'isucondition_perl', 'expires', 3600, 'secret', 'tagomoris');
    enable('Static', 'path', qr"^/assets/", 'root', $root_dir . '/../public/');
    $app;
}

というものだったのが変更後は

&builder(sub {
    enable('ReverseProxy');
    enable('Session::Cookie', 'session_key', 'isucondition_perl', 'expires', 3600, 'secret', 'isucondition', enable('Static', 'path', qr"^/assets/", 'root', $root_dir . '/../public/'));
    $app;
}

と解釈されてしまう。続く Static middleware を enable した結果が Session::Cookie の引数に並べられる形になり、内部ではその値は無視されるのだろうけど、何が起こっているかというと「enable が呼ばれる順番が変わる」。

ここでまた別の事象として、benchmarkerが「/assets/ 以下に含まれるファイルをGETした際に Set-Cookie ヘッダが含まれているとそれによってbenchmark scenarioを回しているagentが別人になってしまい その後のリクエストで 401 になってしまう」というバグがあった。

つまり このpsgiでは 先に Session::Cookieenable していると その後に呼ばれる Static のファイルたちにも Set-Cookie がつくことになり、そのbenchmarkerのバグをつくことになってしまっていた。 "tagomoris" を修正した際に間違えて ;, に変えてしまたことにより意図せず StaticSession::Cookie の順に enable する形に変わっていて そのバグをつかないように変更されたので エラーに遭遇しなくなった、というオチでした。

奇妙な挙動にとても戸惑ったけど 複数の不具合が密接に絡んで起きた奇跡のような現象でした。という話。

いやー数年ぶりに書いたけどやっぱりPerlむずかしい…。 一晩で解決できたのも奇跡だったのかもしれない

本選へ

…というわけで ともかく予選の移植は無事(?)に完遂し、本番でも特に言語実装依存の不具合なく終えられたようで何よりでした。

また本選に向けて頑張っていこうと思います。

よろしくお願いします。