cnosuke's blog (′ʘ⌄ʘ‵)

へっぽこエンジニア

ISUCON7本戦で @k0kubun と @rkmathi とのチームで4位だった 「Railsはもはや煩悩(ryチーム」

こんにちは、cnosukeです。 最近は、主にTHEOのSRE*1をやりながら、たまにGolangで裏側のAPIを作るマンになっています。

ISUCON自体はかれこれ5回目の参加で、今のメンバーでは4回目の参加です。 前回の本戦はISUCON4で、その時は学生枠だったので、本戦自体は2回目、社会人としては初めての本戦でした。

ざっくりした感想として、予選はnginxをいい感じに設定するとか、割とインフラ面を含めた幅広い知識が要求された一方で、本戦はアプリケーションを最終的にごりごり改善していく感じはありましたね。

k0kubun.hatenablog.com

rkmathi.hatenablog.com

やったこと概要

問題は、数年前に流行ったクッキークリッカーを模したゲームで、さすがKLabさんの出題といった感じでした。 ポイントとしては、データの書き込みとかは大して無く、データ転送も大したことが無いので、明らかにCPU負荷をどうやって分散して下げるか、という感じの問題でした。 通信は、おもにWebSocketを使っていて、そこにクリックの動作が流れたり、同じ部屋の他のユーザに状態が同期される、という感じです。

方針決め

とりあえずゲームを動かしてみると、「部屋」の概念があり、全ての操作は部屋の中で閉じていることがすぐに分かります。これは、ゲーム関係にはよくある「世界でサーバを分ける」方針がそのまま使えるということなので、サーバそれぞれを「部屋」で良い感じに分けるという方針はすぐに決まりました。 これは、どのチームも最初に決めた方針だと思います。

サーバ分割

それで、最初に雑にサーバを割り振ってみると、最初はベンチーマーカがかけてくる負荷が小さく、作られる部屋も少ない(4〜6部屋程度)ので、部屋名でハッシュをとってサーバを分けるのようにすると、確率的に一部のサーバにアクセスが集中して、そこがささるせいでベンチマーカがなかなか負荷走行を強くしていってくれないことに気づきました。

なので、4台与えられているサーバの1台(以下、初号機と呼ぶ)に、部屋の割り振りを一任して、その1台が各サーバに作られる部屋数を均等に割り振っていくようにしました。Least Connみたいな感じです。 こうすると、4台に万遍無く負荷がかかるので、初速が出やすくなりました。

あと、トップページやcss/js等のアセット類のアクセスはほぼ初回と最後くらいで、全くといって良いほどスコア貢献が無かったので、これも初号機のみで返すようにしました。 つまり、ベンチマーカは、最初は初号機のみにアクセスし、そこでhtml/js/cssを取得してWebSocketで繋ぎにいくべきサーバを知って、あとは他のサーバにWebSocketで繋ぎにいく、という感じです。

ついにRubyを辞めた

いろいろとRubyでキャッシュを入れるようにしたり工夫していたんですが、あまりうまく出来なくて、割と直ぐに今度はメモリが溢れるようになってしまいました。 そこで、遂にRubyをやめるか〜〜〜となって、WebSocketをハンドルするサーバはGolangを選択することに。 nginxも実質パイプしているだけで要らんなってなったので、GolangのWSサーバが直接リクエストをそれぞれのサーバで受け付ける構成に変更しました。 こうしていくと、前述の「初速が出やすくなるようにleast connにした」との組み合わせもあって、割と2万点は超えるようになりました。 (たぶん同じようにWSを複数サーバで受けて、かつGolangにしているチームは多かったようですが、ベンチの最初のほうでリクエストが少ない時に部屋が偏ると初速がかなり遅くなってしまう感じの部屋割り振りガチャになってるケースもあったんじゃないかと思う。他の工夫で初速が伸びるようになって、作られる部屋数が10を超えてくると、least connの分散の意味はほとんど無くなると見込めるが、2万点前後付近ではこの効果が出やすかったんじゃないかなと思う。)

ごりごりパフォチュー

rkmathiがプロファイルとってくれて、みんなで「いろがきれいだな〜」とかいいながら眺めたりした。

これ以降は、k0kubunが持ち前の爆速エンジニアスキルを発揮して、かなりいろいろとコードの高速化を試してくれて、rkmathiがそれをプロファイルを取ったりサーバの設定項目を見直したりしてサポートするという感じ。僕は累乗の計算しまくっててエグイ計算量になっているところを、事前に計算した結果を参照するようなキャッシュ機構の実装を一生懸命していた。

ラスト30分でキャッシュ機構が出来たかと思ったけど、うまく動かず結局revert。 最終的には、地道な改善効果も重なりはして、スコアは27,304点ということで、4位でした。

全体のリポジトリはここにあります。

github.com

心残り

やっぱりもっと、キャッシュをガンガンいれれる余地はあったかなぁと思っているのと、MySQLを結局捨てきれなかったのは少し残念な気持ち*2。 構成的には、全部Golangのオンメモリで裁ける性質の問題だったので、そこまでやれたらなぁという思いはある。 全部Golangオンメモリで、コード上でtransactionもうまく管理できれば*3MySQLトランザクションのロックが結構長いなみたいな問題も解決は出来ただろうし、悔しさが残った。

なんというか、最初はRubyで頑張って、途中からGolangに切り替えるみたいな流れ自体は、最初に分かりやすい言語で動作理解が出来たりどこが重いかが分かりやすかったので、よかったと思っているけれど、切り替え後に、最近Golang書いているとはいえ、まだまだ3人とも完全に手に馴染んでは無い感じがあって、苦労があったのが悔しかった。

終わったあとに、スギャブロエックスの各位とも話していたのだけど、ISUCONみたいに極度の緊張下で時間が厳しい環境ではコンパイル時にバグにすぐ気づけるみたいなのはとてもいいねという感じで、Golang最高みたいな気持ちになりました。(スギャブロエックスさんのところはTypeScriptのようでしたが)

*1:ちなみにTHEOはAWS上に構築したKubernetesでホストしています

*2:データ量が少ないので、MySQLのパフォーマンスが問題ということは特に無く、それよりはtransactionの範囲が広くなってロック期間が長くなるとかそういう問題があった。

*3:これが難しいんだと思うけど...