序
2020/09/12に開催されたISUCON10にチーム”SunPro”として参加して、11位で予選通過した記録です。使用言語はGo、最終スコアは2844でした。
チームメンバー
- hiromu: 筑波大 M2。チームで一番年下だが、チームを束ねている。
- hakatashi: G社エンジニア。昨年は学生だったが今年は就職したため、チームも学生チームではなくなった。
- mine: 慶應大 D1。この記事の筆者。チーム最年長だがISUCON歴は一番短い。
チーム構成
ISUCON8,9のときと同じメンバーで出場した。ISUCON8では予選通過したが、9ではすべての作業を巻き戻す必要に迫られほぼ初期スコアに留まったため予選落ちした。ISUCON10では昨年の反省を活かそう、という話をしていたが、hiromu以外は当日ギリギリに集まるまで何も準備をしていなかった。反省とは…。
このチームではメンバー毎にDBやアプリを担当したりといったことはしていない。全員全部触れる前提でやっている。一応筆者であるmineがDB周りのマニアックな知識を持ってて、コーディング能力やら分析周りは他の2人が強いなど多少得意不得意はあるが、ISUCONでなにが必要になるかは当日になってみないとわからないので、その場で作業を振り分けている。振り分けはなんとなくhiromuがやる流れになってる。(毎度丸投げ申し訳ねえ)
ちなみに、チーム名はチームメンバーが構成している同人技術書を出したりしているサークル”SunPro“からとっている。本当はもっとひねった名前にしたかったが、申し込むのが遅く枠が埋まりそうだったので、急いでこれで申し込んだ。
ソースコードなど
https://github.com/hiromu/isucon10-qual
当日の様子
09:40頃 3人揃う。言語を何にするか相談し、Goにする。なお、昨年まではRubyで出ていた。
hiromuが昨年のをベースにした作業メモを共有してくれる。hakatashiとmineは何も準備してなかった。この光景、昨年も見たぞ?
今年は「レギュレーション類を読む担当」を廃止して、ISUCONが始まったら全員でレギュレーションの読み合わせを行うことを決める。昨年まではレギュレーション類をきちんと共有できてなかったせいで、スコア算出方法を見失った最適化を行って時間を溶かしていた。キャプテンしかルールを知らないサッカーチームはありえないように、ISUCONでも「レギュレーション類を読む担当」が存在するチームはありえないらしい。とにかく今年はレギュレーション類は全員が頭に叩き込むことにした。
10:00頃 本来の大会開始時間。急いでGoの環境構築を進めプロファイラ(net/http/pprof)の使い方を確認する。この時間に開始していたら確実に死んでいたであろうペースである。ちなみに、ほとんどの作業をISUCON側のサーバーで行ったので、ここで構築した環境は大会中は使わなかった。
Goの経験自体は全員たいして豊富というわけではないはず?今振り返るとこのあたり確認せずに「やればできるやろ」的なのりで突き進んでいた気がする。ちなみに、私mineはGoのチュートリアルをこなした以降は特に使う機会がなかった。
net/http/pprofは死ぬほど便利なので、おすすめです。
11:00頃 昼飯を食べる
12:20頃 大会開始。ポータルが不安定だったが予定通り規則の読みあわせを行う。
12:50頃 Gitリポジトリの作成、各種設定ファイルをリポジトリに追加、pprof、kataribeのセットアップなどなどの基本的な環境構築
14:00頃 環境構築終わってベンチマークがギリギリ走ったので3人でpprofの計測結果を検討して、まずは改善できそうなところを可能な限り列挙して割り振る。
この間、ベンチマークが止まっていたので、ひたすらにpprofを眺める。本当に運営お疲れさまです。毎度だが、一番ハードなISUCONをしているのは参加者ではなく運営…。感謝です。
なお、MySQLのスロークエリログだけ設定漏れてたので、データベース周りはベンチマークの再開を待つ
14:52 ベンチマーク動き始めたので、すべての計測仕組んだ状態での初期スコア取得 スコア: 402
14:58 searchEstateNazotte の N+1を解消 スコア: 555
15:07 JSONレスポンスの整形をなくす スコア: 572
pprofの分析からGo実装ではJSONのフォーマットに時間を割と食われてたので、とりあえずサクッと削減
15:10 getLowestPriceChair, getLowestPriceEstate みたいな価格でソートするものは index を貼る スコア: 712
このとき rent, id という順番で複合インデックスにするのが大事
15:46 searchRecommendedEstateWithChair で椅子にあう物件を探すときに min(w, h, d)[:2] で比較すれば比較条件がマシになる スコア: 733
15:54 botからのリクエストをnginxを使ってRegexで弾く スコア: 886
16:17 postChair, postEstate の INSERT を行毎ではなく一括で行う スコア: 841
スコア落ちたけど、ちょっとなので誤差かな?と思い残す
16:31 postChair, postEstate のtransactionを削除 スコア: 880
INSERT一括にしたので、トランザクション使わなくてもDirty Readは起きない
17:17 query cache 有効化 スコア: 1422
WebでUIをいじってみるとわかるがestateの検索パターン数はいうほど種類が多くないのでかなり偏りがある。INSERTの頻度も多くないのでサクッとドバっとクエリキャッシュ
17:22 buyChair で、SELECT→UPDATEとしているのを、1つのUPDATEに集約 スコア: 1556
初期実装ではトランザクションを使って、SELECT FOR UPDATEで行ロックを掛けた後にUPDATEで更新をかけている。だが、SELECTの方のWHERE句をUPDATEのWHERE句に入れれば同じことができるので、トランザクションを廃してUPDATE 1つに統合。なお、当該するレコードがあったかどうかはAffected Rowsの数値を見ればわかる。
17:36 NazotteでアプリケーションでLIMITしているのをSQLに移す スコア: 1593
17:41 chairsとestateのfeaturesをbitsetにする スコア: 1467
ちょっと落ちたけど誤差かな?と思い残す
18:00 MySQLのgeometry型に格納することでなぞって検索を高速化 スコア: 1216
「高速化」と書いたが高速化に失敗したので、これはrevertした
18:09 getLowPricedChairの高速化のために、クラスターインデックスで価格順に整頓 スコア: 1939
getLowPricedChairに限らないが、価格帯を絞ってデータを取るクエリの数がかなり多かったので、先に設定した複合インデックスをPRIMARY KEYにして、逆にid単体をUNIQUEなセカンダリインデックスにする。MySQL InnoDBはPRIMARY KEYが設定されている場合、それがクラスターインデックスになる(参考)。クラスターインデックスの1つ目の項目で範囲を絞り込んだ場合、ディスクアクセスも特定領域に集中させやすいので速くなる。最低でもインデックス2度読みが起きない。
18:16 longtitude latitude に index を貼る スコア: 1893
ちょっと落ちたけど誤差かな?と思い残す
18:31 postEstateRequestDocumentのSELECT * をSELECT COUNT(*)にする スコア: 2223
18:40 door_min, door_max を持って index を追加する スコア: 2180
スコア減少は微小だが、ベンチマークで吐かれるエラーが増えたのでrevert
19:09 nginxのconfigをチューニング スコア: 2254
(このあたりから記録が怪しい)
mysqlのinnnodb_pool_sizeを増やす。(これは最後まで採用)
色々試すがことごとくスコアが下がってやり直し
20:00頃 App2台(うち1台がリバースプロキシ)、DB1台構成にする作業に着手。(逆に言うとここまでずっと1台で計測)
20:30頃 複数台構成が安定し始める。
20:45頃 プロファイラやログ出力を可能な限り止めて、再起動してサーバーを清める。
ガチャ
20:57 2844をマークしたので、終了
実は20:44あたりに2938をマークしていたが、スコア的に予選通りそうだっったのでガチャで無理はしなかった。
作業環境
メンバー各々勝手に構築してた。hiromu、hakatashiがmacOS、mineがWindowsだった。
マシンが3台あったので、なんとなくマシンをそれぞれに割り振って作業していた。mineはRLoginを愛用しているので、踏み台サーバーにSOCKSv5 1本とローカルポートフォワード3本を一気に刺しつつ、3台にシェルを確保する。GNU screenのコンフィグをコピーして3画面分割を構築する。コーディングはVSCodeを使いたかったので、別途VSCode Remote向けのコンフィグを用意する。