出身サークルのISUCONに雑に参加した話
目次
なにこれ
主催者の記事: https://goryudyuma.hatenablog.jp/entry/2021/05/10/005033
主催者に誘われたので他人のリソースでISUCONして優秀な後輩に負けてきた。
大体以下のコミットログを元に雑に説明する。
参加記録リポジトリ(Github)

結論・感想
5位。7万点ぐらい。リポジトリを見ればいつ何をやったかわかる。終盤の変更は全て改悪で最終スコアは5万点だったが、わざと提出しなかったため7万点となっている。
敗因は、雑なペース配分、貧弱なプロファイリング、RubyでのISUCON経験値不足と見ている。
まず、雑なペース配分。せめて48時間のハッカソンをしなければならないのに、24時間で体力がほぼ尽きるペース配分をいていて24時間やって、4時間仮眠した後はほぼ脳死の上、計測・分析・対処という基本が疎かになっており打ち手の精彩を欠いた。
次に、貧弱なプロファイリング、topコマンドとrack-lineprofだけ。つまり限られたリソースでどこが詰まっているか系は勘でやったでやっていた。そのため、スコアが明らかに上がるはずの施策で下がった原因を正しく理解できなかった。
最後に、RubyでのISUCON経験値不足。殆どの高速化をRedisに依存しまくったし、なんならRedisのチューニングはしたことがない。本当は同じマシン上に別プロセス立てて依存するだけでCPUのコンテキストスイッチが増えて負ける。なので、GoみたくオンメモリDBをお手製で作って永続化したい。しかし、Rubyのメモリをかなり理解していないとできないし、Sinatra上で付け焼き刃でやろうとするとまずできない。
参加経緯
- 主催者に煽られた
- Ruby(Sinatra)+Redisという初期方針でどこまでいけるか試したかった
- 後輩相手にイキりたかった(ポジティブな意味で煽りたかった)
学び
- 最初に計測は仕込んで徹底的に分析しろ
 金曜日の退勤後、雑なノリで着手したのでこの段階で詰んでた
 途中からやると木こりのジレンマに陥って敗北する
- トータルシステムで見ろ
 エディタ開いて見ているのはリクエストを受けてから返すまでの一部分に過ぎない
 見ていない場所がボトルネックだったらその変更は無意味だ
- メモリ管理を意のままにできる言語・環境でやれ
 メモリ上で無理やり永続化する必要に迫られるのでこれができないと負ける
 RedisでキャッシュしてやってもAPとRedis間通信のオーバーヘッドで負ける
 今回は、Ruby(Sinatra)のメモリ管理を理解していなかったので負けた
- Rubyがウィークポイントだと思ったならnginxでできることをもっとやれ
 単純な文字列組み立てなら下手するとAPに行くまでにnginxのLuaでやったほうが早い
 ユーザー名を無暗号化状態でクッキーに仕込んでLuaでレンダリングもアリだった
 例えばこんな感じの合せ技
 
- やったことがないことはできない: Rubyでもメモリキャッシュ利用実装ができたはずだができなかった
何をやったか
大体リポジトリのコミットログに言い訳をつけたもの
ここと一緒に見ること: Commits · fono09/kstm-isucon-2021-gw
- /home/ishocon配下をgitリポジトリに沈める
 VCS使わないのは敗因になりうるのでまずそこはやる
- 15,555点: MySQLに勘でindexを貼る
勘とはいうものの、一応発行されるクエリは見てやった
 SQLのslow logをなぜ見なかったかは今となってはわからない
- 15,938点:ページネーションをproducts.idのBETWEWENにする
 LIMIT OFFSETしてやがったのでやめさせた
- 15,805点: セッションストアをRedisにした
 usersあたりの頻繁に引かれている情報をRedisに載せ、クッキーの肥大を抑える
- 18,858点: indexにあるproductsとcommentsのN+1を解消 それでもクエリが重たいことには変わりない
- 20,223点: current_userの情報はRedisに聞く
 問い合わせている頻度が高そうなのでとりあえずやった
 懺悔:ここらでusersのRedisへの格納をHASHでやれば属性をたくさん生やすことができた
- 点数不明: publicをnginxで返し、last_loginを廃止
 ここらからオペレーションに綻びが出始めた
 やっていることは妥当なはず。この差分でnginxのライトチューンもした
- 点数不明: histriesにproduct_id, user_idの複合index
 これミスじゃないかな……とりあえず露骨な悪影響なさそうだけど
- 70,893点: コメントの件数をRedisに入れてキャッシュ
 明らかにMySQLを重たくしているしRedisで良いと判断
 row_number振って上位5件とか言うのをリクエスト毎やっていたのでどう見ても重い
- 71,018点: MAX(product.id) をDBで引かない
 当初の設計では何も考えずやったが、productsの更新系ないのに気がついた
- 73,112点: 過去の購入判定をRedis化
 historiesをuser_idで舐めて集計させてたのをやめた
- 74,705点: cache-controlを吐かせてみる
 ここらは勘。適切なヘッダを吐かせる努力おしていなかったので多分あまり効果がない
 なんなら、キャッシュ戦略をするならフラグメントキャッシュでない限りnginxに任せるべきだった
- 45,371点: コメントをRedisに入れたが遅くなった(これは直後にRevert)
 入れ方が悪いのでやめた普通に時系列なのでusers.namecomments.contentの2つのリストで良い
- 69,411点: 大量のキーでRedisが潰れたので初期化を redis.flushallする
 明らかに重たくなっていたのでなにかおかしいと思ったらこれだった
- 点数不明: ドメインソケットでRedisに繋ぐ
 そろそろMySQLの肩代わりをしたRedisが重たくなると思ってここでやった
- 71,073点: /products/:product_idでcommentを見ていないので外した
 ほぼ無意味。イテレータで舐めているから実際にクエリは飛んでないのかも ここらで提出をやめた
- 52,255点: コメントをRedis化
 LISTでRedis化したが悪化したが、点数が上がらない。
 原因の計測・分析をする心身の余裕はもうなく、ここからは定期的に奇声を上げながらデスマーチしていた。
- 43,277点: mypageのproductをRedis化
 productsに更新系はない。ならすべてキャッシュしてしまえば良い。
 という発想だったが、更新がないならばRedisではなく本当はAPのオンメモリにしたかった。
 しかし、Ruby(Sinatra)でやる方法がわからん。freezeしたり参照をApplicationのクラスに持たせたりとやったがデストラクトされてしまう。
- 56,222点: 説明文を切り詰めた
 切り詰めた説明文を用意した。70文字目までしか見ていないならば切った状態でRedisに格納すれば良い。
- 51,012点: フラグメントキャッシュを入れた
 productsの更新がないならば、一部はレンダリング済みのページを配信すれば良い。
 が、今思うとこれはnginxでのテンプレート処理まで落とせたかもしれない。
- 51,193点: productsのMySQLの参照を一つ減らした Redis依存を強めた分だけちょっと早くなった