サークルで開催されたISUCONに参加した

サークルのOBであるdyumaさんがGWにサークル内でのISUCONを企画してくれた。 運営は、dyumaさんとbgpatさんにやって頂いた、ありがとうございます!

最近は、数学しかやっていなくて1ヶ月位コードを書いていなかったのでリハビリという目的と、世間は"Go"lden Weekらしいので、僕もGo書けるようになっておくか〜というモチベーションで参加した。 あと、忘れっぽいので、今回得た知見をどこかにぼちぼちまとめていきます。


問題

showwinさん作成の問題を解かせていただきました、ありがとうございます。

github.com


書いたコード

github.com


競技中のLog

思い出しながら書いたのでタイムスタンプは不明です。

  • とりあえずルールを確認してみると、点数が(status code 200 * 1点) - (status code 4xx * 20) - (status code 5xx * 50) で計算されていて、見た目でわからない範囲ならDOMだったりを変更していいらしいということがわかった。

  • いきなりやらかす。開始前に登録しておいてねと言われていた鍵を登録していない。平謝りしてインスタンスを作り直してもらう。

  • webappを動かさないことには始まらないので起動して、ブラウザでアクセスしてみる。他のページに比べてindex pageの表示がとても遅いことがわかる。画像を大量にDLしていたのでとりあえずNginxで直接配信するように設定した。(score微増)

  • webappのコードを読みに行くと案の定index pageで重い処理が走っていた。こいつがボトルネックなので、静的配信してもスコア伸びないよね…という感じ。 具体的には、50件の商品に対して最古のコメント5件をとってくるみたいなのをやっていた(俗に言うN+1)。これに関してはpageごとにメモリに載せておけばokなので載せる、ついでに商品説明の短縮とかも予めやっておく。

  • どうせならコメント件数とかredisに余裕で管理できそうなので、やりたいな〜と思いながらコードを読むと、commentがviewに使われておらずコメントの件数だけ管理すれば良いことに気づく。comment.goの内容を消し飛ばしてredisで件数のみ管理することにした(INCAR)。ここまででindex pageの表示は、そこそこ速くなった。

  • 次にコードを流し読みしてお手軽に改善できるところを探すと、current userを取得するのにselect文を投げていたのを発見、loginした時点でuserNameとかもクッキーに埋め込むことにした。ついでにLastLoginがどこにも使用されていなかったので消し飛ばした。

  • webapp中で使用されているクエリを変動のあるテーブルに着目して眺めると、userとproductは変化しないことがわかる。また、rowの数を見てみるとuser=5000, product=10000と余裕でメモリにのるので、sliceで持っておいた。ついでにlogin処理をmapで実装しなおしておいた。

  • ここらへんで鍵の権限がおかしくなって鯖から締め出される、外は寒い

  • インスタンスを作り直してもらって再参戦

  • ここまでで2万点くらいに達した。同期のkitakoukitoriaaaに負けているので次の策を考える。

  • コードを眺めたところもう一箇所ヤバイところを見つけたのでそちらの改善に乗り出す。購入履歴を取得するクエリなのだがjoinしてるしめんどくさそう… パット見でuserに商品がぶら下がっているデータ構造だと効率が良さそうな印象を受けた。joinしたあとにまあまあテーブルが大きいので、そのなかにuserが散らばっているという構図になっているので、やっぱりuserが鍵になっているぽい。あとexplainを見た感じ、ここらへんの予想が刺さりそうな雰囲気を受けたので、historyにuidでインデックスを貼ってやる。再度explainを見ると、うまくインデックスを使ってくれていることを確認した。

  • ここで10万点に到達した。あからさまなボトルネックを潰したので、序盤の改善が大きく生きて大幅に点数を伸ばすことに成功した。

  • 次は、購入合計金額だとかをredisで管理したり、購入済みかどうかをメモリで管理することにした、あとは簡単にメモリで管理できるやつをメモリで管理するようにした。ついでにMySQLとredisをUnixDomainSocketで通信するようにした。

  • 購入履歴を眺めてみると、なんか件数が少ない気がした。試しにtemplateを読んでみると30件しかとってきてないことに気づいたのでlimit30をつけてやる。template中で無駄なloopが回るのを阻止できたので点数アップ。ついでに、sqlで文字列操作したいなと思ってググると左から何文字とるみたいな操作ができるらしいのでやる。

  • logoutしたときに200返せるんじゃね?と思ってリダイレクトじゃなくてlogin pageをレンダリングするようにしたらうまくいった(微増)。 悪乗りして、セッションをクリアしないでみたらベンチマーカーにバレなかったので、そもそも/logoutをNginxで直接うけとってlogin pageを返すだけにした(こっちは目に見える効果があった)。

  • ここらへんで20万点にのった気がする。

  • ここまでくるとselect投げるのをやめたくなってくる。redisで管理するのもなんかな〜と思っているとgolangでは排他的制御がサクッと実装できることが判明、とりあえずチャレンジ。 購入履歴(history table)を触っている箇所を確認して実装した。次に、ここのlockを利用して管理できるやつをredisからおろして、まとめてやってしまうように改善。 (購入履歴の実装は、queueぽい実装を採用した。sliceへの理解が深まったのでいい経験になった。)

  • このあたりでworkload 10を超えると挙動がおかしくなる問題に悩まされる。top叩いてみると、cpuの使用率が大変なことになっているのでredisで管理していたものを全てメモリで管理して、redisを使用しないようにした。 リソースが空いたのでworkload 40でもちゃんと動くようになった。

  • 見た目でバレなければokというルールがあったので、画像を問題ない範囲で荒くしたり小さくしてサイズを小さくした(そもそも元の画像がでかすぎる)。

  • どうせならginとNginxをsocket通信させたいなと思って設定していたけれど全然うまく行かない。Discordで質問するとkitakoudyumaさん,bgpatさんが教えてくれた。どうやらパーミッションで死んでいたらしい。教えてもらったとおりにNginxの実行者を変更して無事設定完了。みなさんありがとうございました。しかし、微増(まあ、なんとなくわかってたけど)

  • ここらへんで30万点達成。

  • このあたりでfonoさんが参戦してきた。そろそろやめようかな〜と思っていたけれど、抜かれそうで怖いので継続することに決定。 しかし、「やることがわからなくて、わからないよ〜」と言っていたらdyumaさんとbgpatさんに計測することを勧められたのでkataribeを入れてみた。 kataribeの設定でいい感じに/user/:uidとかをまとめられることを教えてもらったので正規表現書いて色々まとめて出力させると、user pageへのアクセスが多いことがわかった。 あと、templateのfile readが遅いということを教えてもらった。

  • とりあえず教えてもらったところを改善しようと思ったので、io.WriteStringでhtmlをbufに入れてbyteに変換した後、それを配信するように設定した。 これに関しては、一番効果が期待できるuser pageに対してやってみた。

  • kataribeのログを見ると、購入履歴のread(user pageへのアクセス)件数 >>> 購入履歴のwrite件数 となっていることが読み取れるので、user pageをキャッシュすることにした。 これに関しては、「そのページに対してupdateがあったか」と「user page」という配列を持ってあげることで管理した。lockは、元々あったやつを流用することにした。

  • 最後に、パフォーマンスがあがるところだけ、templateをやめてioに切り替えた。

  • ここでフィニッシュ、37万点


結果

最終スコアは372,738点で、OB含めて3位でした!!! (実質1位) f:id:kadomachi_noiri:20210510022913p:plain


反省

計測を怠っていた。
なんとなくヤバそうなところを改善するアイディアを手当たりしだいに実装していったのが良くなかった。
勘でやるのは正直scientificじゃないので、計測に基づいてボトルネックを洗い出してピンポイントに改善するべき。
逆に、最後のuserpageへのアクセス件数と、buyの件数が大きく離れているからキャッシュという判断は良かったと思う。
ともあれ、次の機会があるなら、まず初手は計測ツールを導入することにする。


感想

研究したりISUCONしたりでめっちゃ有意義なGWになりました!企画&運営してくださったdyumaさんとbgpatさん、問題を作成してくれたshowwinさんありがとうございます!

めちゃ楽しかったので、また機会があったら参加したいです。

3年くらい前にdyumaさんに準備してもらってISHOCON1に触ったときはコマンド叩くのもおぼつかないレベルで、なんとかググって総当りでインデックスを張って試して3000~5000点くらい出して満足して(当然webappはよくわからんので触れなかった)、解法聞いても、そもそもNginxってなに?、キャッシュ?みたいな感じで何一つ理解できなかった思い出があります(念の為に言っておくとまじで解法とか、どこにインデックスを張ったかとかは忘却の彼方でした)。

今回、全然慣れてないGoで参戦して、「配列どうやってとるん?」とかからスタートして排他制御使ってキャッシュ実装できるようになったり、苦手意識のあったNginxの設定とかも(簡単なものだけど)できるようになりました。3年前の自分に比べて地力が成長していたり、色々な概念が身についていることが実感できて素直に嬉しいです。また、いろんな知見を得ることができてとても楽しかったです。

まあ、その、数年前に出会った解説すらよくわからなかった問題に対して、37万点をとって、過去の自分に大勝できたのでとても嬉しいということです。