最終ベンチ 30164点 参考順位 20位 failでした。構成はサーバー1台です(分割に失敗)
追試でのスコアが最終スコアより低かったのがfail理由のようでした。が、競技後の感想戦で何も手を付けずにベンチを回したところ75%以上スコアが出ていて、fail理由は迷宮入りしています。(困った……)
各FAIL理由の詳細については以下のとおりです。
...
2.負荷走行NG : 追試における3回の負荷走行が全てFAILであるか最終スコアの75%以下でした30,164 KatoMegumi 2.負荷走行NG
https://isucon.net/archives/58837992.html
分担
NaruseJunの分担を参考に、各メンバーの初動を何となく決めていました。
- CFn担当・MySQL,Nginxのとりあえずの設定変更
- Ansible 仕込み・pprotein・MySQL・Nginxサービスの管理
- レギュレーション・当日マニュアルを読む, クライアントアプリの動作確認
やったこと
Ansible 流し込み
書いておいた Ansible でせっせと各サーバーに設定を流し込みます。なにをしたかと言うと
- 計測ツールの導入
- 各サーバーに pprotein-agent、テレメトリ収集サーバーで pprotein を起動
- pprotein と pprotein-agent の間の SSH ポートフォワーディング用の鍵を仕込む
- メンバーの GitHub の公開鍵を authorized_key に差し込む
- カーネルに関連する秘伝のタレを仕込む
- リポジトリ持ってくる
んな感じです。これ以外のリポジトリに内容を書き込むといった実装に依存するところは温かみのある手作業でやってあげます。
あとパスワード書くとこだるいな~って思ってたら今回はパスワードなしで入れてビビりました。
pprotein設定
pprotein用にnginxやmysqlのログを設定。
pprotein用のコードをアプリに仕込む必要があったのでその作業も実施。echoやmuxが使用されていた場合、1行echoInt.Integrate(e)
と書いて終わりだったはずが、chiという知らないルーターが使われていたため以下のように対応(standalone.Integrate
でうまく動かせなかった)。
{
mux := chi.NewRouter()
mux.Handle("/debug/log/httplog", NewTailHandler("/var/log/nginx/access.log"))
mux.Handle("/debug/log/slowlog", NewTailHandler("/var/log/mysql/mysql-slow.log"))
mux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
mux.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
mux.Handle("/debug/fgprof", fgprof.Handler())
go http.ListenAndServe(":3000", mux)
}
これによりpprof, http log(alp), slow query log(slp)の計測結果をweb uiで簡単に見ることができるようになりました。本来はgitRepositoryMiddleware
によりその計測時のビルド元コミットへのリンクも表示されるのですが、chiルーターでこれを使うことができず断念。
後でわかったのですがどうやら以下のようにすればよかったようです。
pproteinHandler := integration.NewDebugHandler()
go http.ListenAndServe(":3000", pproteinHandler)
appGetRides → 1110
o1くんがN+1を消してくれました。
⚡️ appGetRides by trasta298 · Pull Request #4 · KatoMegumi24/isucon14
calculateDiscountedFareがN+1になってたのも解消したのだが、改善してる数字が見えず保留にしていたら最後まで忘れていた。
⚡️ appGetRides N+1消した by trasta298 · Pull Request #6 · KatoMegumi24/isucon14
ライド情報周りのindex → 3700
この後も継続的に気付いたタイミングで slow query log を見て使用できるメモリ量とか write 優勢になってないかを確認しながら index を仕込むことを繰り返してました。
https://github.com/KatoMegumi24/isucon14/pull/5
https://github.com/KatoMegumi24/isucon14/pull/8
appGetNearbyChairsのN+1解消(?) → 3390
o1使役係であるが担当。この辺からo1を信頼しすぎて全然レビューしてなかったので、for文1個分残ってるのに気づかず。あとで回収。
⚡️ appGetNearbyChairs N+1 by trasta298 · Pull Request #9 · KatoMegumi24/isucon14
appPostRides の N+1解消 → 3440
o1くんがやってくれました。
⚡️ appPostRides の N+1 解消 by trasta298 · Pull Request #10 · KatoMegumi24/isucon14
notificationのポーリングを2000ms毎に → 4600
クライアントアプリの動作確認時に大量のGETリクエストが見えていたので要改善メモをしてました。そのままが確認。
マニュアルを読むと
状態が変更されてから3秒以内に通知されていることが期待されます。
とあったので一旦2秒に変更しました(後に変更される)。
ownerGetChairs → 4200
slowlog見ると一番上に
SELECT `id`,`owner_id`,`name`,`access_token`,`model`,`is_active`,`created_at`,`updated_at`,IFNULL(`total_distance`, N) AS `total_distance`,`total_distance_updated_at` FROM `chairs` LEFT JOIN (SELECT `chair_id`,SUM(IFNULL(`distance`, N)) AS `total_distance`,MAX(`created_at`) AS `total_distance_updated_at` FROM (SELECT `chair_id`,`created_at`,ABS(`latitude` - LAG(`latitude`) OVER (PARTITION BY `chair_id` ORDER BY `created_at`)) + ABS(`longitude` - LAG(`longitude`) OVER (PARTITION BY `chair_id` ORDER BY `created_at`)) AS `distance` FROM `chair_locations`) AS `tmp` GROUP BY `chair_id`) AS `distance_table` ON `distance_table`.`chair_id` = `chairs`.`id` WHERE `owner_id` = 'S'
が見えてヤバかったので対応。最初はが対応(WIP: 総移動距離をchair_locationsに入れるように by eyemono-moe · Pull Request #7 · KatoMegumi24/isucon14)。
chair_locations
テーブルに最新緯度経度とそれまでの総移動距離を入れて常に各椅子につき1つのchair_location
レコードが存在するようにした。
が、initial dataのlocationsのマイグレーションがめんどくさかったため一旦放置。trastaに引継ぎ(⚡️ ownerGetChairs by trasta298 · Pull Request #12 · KatoMegumi24/isucon14)。
引き継がれたもの、initで既存のデータも加工しなきゃいけないことに気づかず時間溶かす (後悔ポイント)
chairテーブルに total_distance_updated_at
last_longitude
を持ってpost時に計算するようにした。
chairPostCoordinateのクエリ削減 → 7090
pprof見ると POST /api/chair/chairs
のリクエストが多く、ネックになっていたので、クエリを減らして高速化を図った。o1だけだとうまい感じにならなかったので、方針は採用しつつ、cursorでclaudeとかを使っていい感じに書いた気がする。
chairテーブルにlast_longitude
last_latitude
を持っておくことで、chair_locationsテーブルへのクエリを減らした。
クエリを減らす by trasta298 · Pull Request #16 · KatoMegumi24/isucon14
ride_status のキャッシュ → 7400
getLatestStatus がさまざまなところで呼ばれていてキャッシュした方が早そうというノリで実装。そんなに増えなくてサーバー分割したときバグるのが怖いので入れずに放置。
ただこの後サーバー分割に失敗。別サーバーでも isuride-matcher が起動していてそれと競合したのが原因。終了後に試したらスコア 36000 だったそう。くやしい。
マッチングアルゴリズムのやつ → 20000
スコア計算などが書かれているマニュアル等もコンテキストとして含めて、o1に投げるとコードを書いてくれた。最初はうまい感じにいかなかったが、僕の考える配送戦略等をコンテキストとして含めるといい感じになった。internalGetMatchingは左をride、右をchairとした割当問題(二部グラフの最小重み最大マッチング)なのでハンガリアン法を使うとよい。
My Algorithm : kopricky アルゴリズムライブラリ
出してくれたコードを反映させるとベンチマークでエラーが出るので、プログラムのミスを疑っていろいろ調べたが、単純に通知のポーリング間隔が長すぎるのが原因なことに気づくまでに時間を1時間以上溶かして大後悔ポイント。o1くんごめんなさい。
⚡️ 楽しい椅子マッチング by trasta298 · Pull Request #18 · KatoMegumi24/isucon14
appGetNearbyChairs の N+1 → 24000
終了30分くらい前に N+1が残っていることに d_etteiu8383 が気づいたので trasta が対応。o1で一発。
scでchairsのキャッシュ実装 → 29000
pprofを見るとchairAuthMiddleware
でのsqlx.GetContext
が重そうなことが分かったため実装を確認。chairAuthMiddleware
内で毎回SELECT * FROM chairs WHERE access_token = ?
してたのでaccess_tokenをkeyにしたchairのキャッシュを持つことに。
はじめmap
で雑に実装したら排他制御できてなくて壊れたので素直にmotoki317/scを使わせて頂きました。@tokiありがとう。めちゃくちゃ使いやすかったです。
ChatGPT o1 活用方法
単純なN+1解消や処理の一元化など、変更する範囲が狭いときに主に力を発揮する気がする (調べたわけではない) ので主にそういうタスクを任せた。
本番直前にChatGPT o1 pro modeが公開され、o1 proがGUIでしか使えなかったので、今回のために、プロンプトの作成用の repo to textツールを自作した。 (o1に適当に作ってもらっただけなので公開予定なし)
これでコンテキストに含めたいファイルを選んでやってもらいたいタスクや相談したいことを書くだけでいつでも使えるようにした。
ぶっつけ本番で試したのでどうなるかな~と思っていたが、蓋を開けてみれば神だった。途中から出力されたコード見ずにベンチ回して一発で通ったからヨシってやってました。
日進月歩で進化しているので来年はどこまで活用できるか楽しみです。
使用例: https://chatgpt.com/share/6755a15a-67e4-8013-9f4e-c36121d75678
感想
trasta
普通に悔しいです。事前練習やらなかったつけが初動の遅さにでた気がします。あと変なポイントで躓きすぎた。リベンジします。あとLLMあれば僕いらない。
d_etteiu8383
今回が初参加でした。とにかく僕の実装速度が遅すぎました。練習が足りなかった。あまり大きな改善ができない&インフラ周りを2人に丸投げしちゃってたので申し訳ない気持ちでいっぱいです。
anko
練習すれば時短になったこと数えられないほどあってそれは反省なんですが、割と雰囲気よくできてメンバーも楽しんでいてよかった~って思ってます。次回は終了後にメンバーとグータッチできるように頑張りたいです。