ISUCON4予選でリモート体制で正攻法で4万2千点くらい出してきました
タイトルのとおりです。最後は少しmemcachedを入れたりしましたが、それはそこまで効果はなかったので、PerlとNginxとMySQLだけの構成で4万点くらい出せたってことで、それなりの結果かと思っています。ただ、なんかレベルが上がり過ぎてて、予選通過が微妙ってことで社会は厳しい。
チームは元々、@sugyanと@typesterと「元fujiwara組」ってことで出ようかと思ってたんだけど、なんだかんだで実現せず、じゃあ、折角だからはてなの人と組もうと思い、id:motemenとid:wtatsuruと出場するつもりでした。しかし、wtatsuru先生がAWSのリブート祭りのために参加できなくなってしまったので、優秀な若手であるid:y_uukiをインフラ担当ということで入ってもらうことにした。
全員Mackerelに関わっているので、「マカレラーズ」というチーム名にした。結果としてすごく良いチーム構成で競技を楽しめた。
チーム構成や前日の準備
id:motemenと僕がアプリケーション担当で、y_uukiがインフラ担当というチーム構成。ちょっと特徴的だったのは、3人共バラバラの場所で戦ったということ。motemenと僕がそれぞれの自宅で、y_uukiがオフィスという具合。僕らは普段の仕事でもリモートワークに取り組んでおり、そのいい練習にもなった。
チャットはSlackでやって、Sqwiggleでお互いの顔を見ながら定期的に会話をしたりした。前日からGitHubにプライベートリポジトリを作り、方針などをissueにしておくなどしていた。
基本方針としては、普段やってないことはどうせできないから正攻法で行こうということにしました。
当日
最初の1時間は、y_uukiにサーバーの設定をしてもらったり、リポジトリにコードpushしたりdeployの仕組みを整えたりしつ、適当にベンチを回しながらtopを眺めたりコードを見るなどした。deployがそれぞれの手元でコマンド一発でできるようになって、しかもそれがSlackに通知されるようになったりする仕組みをmotemenが作ってくれたのがオシャレだった。
コードを眺めた感じだと、login周りで複雑なクエリを発行しまくってて不穏だなーという話はしていたのだけど、y_uukiがNginxのログをLTSVにしてそれを集計してアクセス傾向を取れるようにしてくれて、やはり明らかにloginの処理が重いということがわかった。
ベンチ中にtopを見ると明らかにMySQLがCPU食いまくってたので、とりあえず、login周りのサブクエリをほどいて分かりやすくしようという方針になった。そのために、以下のスキーマ変更を行った。
- ユーザーの直近のログイン失敗回数を記録するカラムをusersに追加
- 直近のログイン失敗IPを記録するテーブルを追加
SQLとしては以下。
ALTER TABLE users ADD COLUMN `recent_login_failures_cnt` INTEGER NOT NULL DEFAULT 0;
CREATE TABLE `ip_login_failure` (
`ip` varchar(255) NOT NULL PRIMARY KEY,
`cnt` INT UNSIGNED NOT NULL
) DEFAULT CHARSET=utf8;
usersテーブルのカラム追加を僕がやって、ip_login_failureテーブルの追加をmotemenが担当。僕はカラム追加だけだったので、ちょっと他の部分のコードも見ていたら、初期データから、直近のログイン失敗回数を拾わないといけないことに気付いて、めんどいな~とか思ってたら、motemenがあっという間にテーブル追加の実装を終わらせていたので、そのへんの初期データの作り直しなんかもmotemenにお願いした。
初期データの作り直しはさすがにちょっと時間がかかりそうだったので、その間に、僕はスキーマ変更以外の細かいPerl実装の調整と、サーバーがStarmanだったからStaletに入れ替えたり、supervisord.confを弄らなくてもサーバー構成を変更できるようにシェルスクリプトを作ったりとかしていた。y_uukiはNginxの設定いじって静的ファイル返す設定とかやってた。
割とこの辺それぞれ自律的に動いてる感じが良かった。githubを使っていたので、僕とmotemenがそれぞれコードレビューがやりやすく、しょうもないバグをサーバーに入れることが結果として無かったのも良かった。
その辺もろもろが終わったのが12時半くらい。そこで、workloadが1のままでベンチを走らせたところ8千程度のスコアになった。調子に乗ってworkloadをあげたらうまくいかずtoo many open filesとかでたんだけど、まあこれは、ulimitとかそのへんでy_uukiがなんとかしてくれるだろうといことで雑にy_uukiに投げた。ローカルポート枯渇問題とかもこのへんで対応してくれてた。
足りないインデックスの追加をして13時時点でworkload 4で18000くらい。この時点でTop10に入る。
ただ、たまに500がでて、ベンチに失敗することがあり、そのへんで若干ハマってたんだけど、初期データの作り直しのところに不備があることに気付いてそこを修正。そこを直して13時40分くらいにベンチをかけたら、スコアも上がって23107になった。
スコアには直接は関係ないけど/reportの集計処理を直したり、workloadを調整して6くらいにしたら、27508までスコアアップ。この時点で13時45分。この辺りはかなりmotemen無双だった。
このへんまでは順調だったけど、ここからなかなかスコアが上がらなくなる。細かいクエリを間引いたり、Starletのworker数を調整したり、workloadを調整したりして、2時40分時点でスコア32049。
若干手詰まり感があったので、motemenとy_uukiはNYTProfを使ってアプリケーションのボトルネックの調査をすることに。その間に僕は、login_log周りの処理に無駄があることがわかっていたのでそのへんの最適化を担当した。
ログイン成功時に最終ログイン時間の更新と、直近ログイン失敗回数のリセットをしていたところを以下のようにして、一回のUPDATE文発行だけで行うようにした。current_*
とlast_*
のカラムを追加しているのは、ログイン成功時にリダイレクトして、そのリダイレクト先で2個前の成功時のログインを取得して前回のログイン時間として表示するような処理になっていたのでちょっとトリッキーなスキーマとUPDATE文になってる。ちなみに複数カラム追加のALTERを手で書くのにすごく苦労してしまって情けない思いをした。
ALTER TABLE `users`
ADD `last_ip` VARCHAR(20) NOT NULL DEFAULT '',
ADD `last_logged_in_at` DATETIME NULL,
ADD `current_ip` VARCHAR(20) NOT NULL DEFAULT '',
ADD `current_logged_in_at` DATETIME NULL;
UPDATE users SET
recent_login_failures_cnt = 0,
last_logged_in_at = current_logged_in_at,
last_ip = current_ip,
current_logged_in_at = NOW(),
current_ip = ?
WHERE id = ?
motemenとy_uukiもセッションのシリアライザとファイル書き込みが遅いってことを突き止め、シリアライザをMessagePackにしてセッションストアもmemcachedにするなどの変更をしていた。
これで、16時3分時点でスコアは36617でトップ10復帰。終了時間が近づいてきたので、一回Sqwiggleで会話して認識合わせなどをした。ここで認識合わせをしたのは良かったんだけど、僕が「競技終了前に再起動のチェックをしよう」とか提案してしまったのがあとから思うと完全な間違いで、予選のレギュレーションからするとそんなの、終わった後で良かった。
その後はNginx-Starletの間や、アプリケーションとMySQLの間をUnix Domain Socketにしたり、workloadを調整したりして、38000程度までスコアを上げる。
17時にmotemenがip_bannedのリストをmemcachedにする実装を入れたけど、バンされたIPリストを保持するためにMySQLにテーブルを残さざるを得なくなってりしてたので、イマイチスコアは伸びず。Redisだったらkeysでキーの一覧が取れるのでmemcachedよりかは有効だなーとか思ったけど時間的に厳しそうだったので諦めた。
…とか思ったら、競技終了後にRedis実装をやってみたら15分程度で終わり、スコアも43500とかになったので、やればよかったと超絶に後悔した。予選のレギュレーションだったら、やれる方策はなんでもやってみて破棄すればいいだけなのに妙に慎重になりすぎていたことに反省。しかもRedisだったらデータも永続化されるのでレギューレーション的にも何の問題もなかったという…。
で、その後は、DBのコミット位置をコード上で調整したり、innodb_flush_log_at_trx_commit=0にしたり、Nginxを調整したり、SessionをmemcachedからCookie Storeに切り替えたりして、ちまちまスコアを上げ、17時42分時点で42134のスコアを出したので、そのスコアで提出することにした。
総括
トップのスコアには大分及ばなかったが、正攻法にしてはそれなりに頑張ったのではないかと思う。何よりリモート体制で参加したことや、これまでチームを組んでいなかったメンバーで戦えたことがすごく楽しかった。
motemenと大体方針が一致して実装も上手く手分けができ、インフラ周りをy_uukiがいい感じにやってくれて、リモートのハンデを全然感じず楽しく競技に打ち込めた。このチームで本戦戦いたいなーと本当に思う。
ただ、ちょっとやらかしている可能性があって、ちょっと厳しいかもしれない。その辺りは本選出場者が決まるまで明言を避けたい。
最後に
毎度のことながら素晴らしいイベントを提供してくださっているLINE株式会社の運営の皆様、そして、参加チーム数が膨れ上がった今回の大会においてプレッシャーのかかる出題をこなしたCOOKPADの出題担当の皆様には感謝、感謝です。本戦も大変ですが頑張ってください。
蛇足
今回の予選のインスタンスではmackerel-agentを動かしてメトリクスを測定していたのだがそれを眺めると結構面白かった。そして、mackerel-agent自体は全然パフォーマンスに影響を及ぼさず、実際mackerel-agentを落としてベンチを回してもほとんどスコアは変わらなかった。これはすごいことだなと思ったし、高負荷のサーバーにmackerel-agentを入れても特に問題無いということを自ら証明してしまった。