IUSCON10予選に参加してきました
お久しぶりです、へたれです。
今年も開催されました、ISUCON予選!!
ということで自分も「チーム豚キムチ2307」として社会人になって初めて参加してきたので、どんなことをしてきたのかを簡単に記事にしようと思います!
(記事を書くためにスクショ等を残していなかったので、記憶があやふやになってしまうのですが...)
ちなみに予選レギュレーションはこんな感じです。
インフラ構成
実際にやったこと
チーム内では自分がアプリケーション方面をメインに、相方のはひふへほ氏がインフラ/ミドルウェア方面をメインに作業を進めていきました。
(ちなみに時間配分については特に記録に残していないので、アヤフヤなキヲクを辿っていく...)
相方がやったこと
(今回まともに自分が動けたのも去年から相方がこの辺の準備をメチャクチャ頑張ってくれたからです...感謝感謝です :bow:)
- 環境構築
- ツール入れ
- コードをプライベートリポジトリ化
- 初期環境のバックアップ
- ssh設定
- 秘伝のタレ流し込み
- nginx複数台構成 (やってみたけどアプリが遊んでて意味ないからやめた)
- DBを分離
- MySQLのテーブルへIndexを貼る
- User-Agentを用いたbot対策
- レプリケーション (非同期、準同期まで、結局同期ができずにやめた)
自分がやったこと
初動
レギュレーション/マニュアル読む
ISUCON最初の作業、マニュアル通読です。
- インフラの構成
- アプリケーションのシナリオ (コードを読みながら処理の流れを理解するのに大事)
- スコアの採点方法 (これを理解しないとスコアの伸ばし方が分からないので大事)
- その他注意点 (今回ここで記載されていたUser-AgentでBotを弾くというのが非常に大きなスコアになりました)
を確認しました。
改変前にファイル分割
本当はこの前にベンチを回しておきたかったのですが、アクシデントのためベンチが回せず...
まずは相方が用意してくれていたリポジトリを手元にクローンしてファイルを分けました。
最初は main.go
しかないのですが、このままではマージの際にコンフリクトの嵐となります。
したがって最初に役割ごとにファイルを分割し、手を加えるものに関しては個別のファイルに抜き出して変更するという運用にしました。
main.go
: 初期化関数とグローバル変数handlers.go
: ハンドラ関数types.go
: 構造体とそのメソッド
newrelic infraの導入
今年はisucon参加者へnewrelicの無料アカウントが付与されていたので、newrelic infraを導入しました。
(ちなみにnewrelicのgoエージェントは「コードを差し込む」「最後のベンチ前にコードを抜く」という作業がすごく工数がかかりそうだったので採用を見送りました)
より詳細なデータは得られるようになるものの、どの処理が重いかの確認はpprofで十分確認できると判断したというのもあります。
コードの読み込み
まだベンチが回せなかったので、計測ができず...
とりあえずハンドラ関数をざっと見渡しながら
- N+1問題はないか
- SQLの
WHERE
やORDER BY
でどんなフィールドが指定されているか 0_Schema.sql
を読んで、どんなインデックスが貼られているか
をチェックしつつ、気になるところをリストアップしていきました。
初回ベンチ回す
問題が解消され、初回ベンチが回せるようになったのでとりあえず回す。
Score: 484
スコアアップに向けた作業
searchEstateNazotteのN+1改善
ここでpprofやkataribeの結果から searchEstateNazotte
関数が一番重いということが発覚しました。
そこでコードを読むとN+1問題が発生していることができ、ここの改善に着手することに。
元は「画面内の物件を一通りSQLで取得して、一件ごとに手書き図形の中に入っているかをSQLで確認する」という方式でした。
そこで処理を「画面内の物件を一通りSQLで取得して、IN句を使って手書き図形の中に入っているかでフィルタをかけた物件ををSQLで取得する」という方式に変更する形で実装。
しかし互換性のチェックでこけ続ける形に...
(ISUCONのコード変更で一番辛いのは互換性のチェックだと思います)
ここでisuconのアプリケーションはsystemdで動作しているのですが、なぜかログが表示されず...そこを直すことに決めました。
ログの調査に難航していた頃、相方が「pprof見たらJSONのエンコーディングがめっちゃ時間かかっとるよ」との発見を共有してくれました。
正直作業が難航していて心がやつれてきたので、一旦今の作業は中断してJSONエンコーディングの高速化へ。
echoのコードを見ると、JSONのエンコーディングには標準パッケージの encoding/json
を使用していました。
そのため、JSONのエンコーディングを jonitor に変更することで、高速化を図ることに。
具体的には
c.JSON(http.StatusOK, res)
を
import jsoniter "github.com/json-iterator/go" var json = jsoniter.ConfigCompatibleWithStandardLibrary ... resStr, _ := json.MarshalToString(res) c.String(http.StatusOK, resStr)
みたいな感じにしました。
相方の作業により既に大きくスコアを伸ばしていたため、スコアはそこまで大きく変わりませんでしたが、pprofを除くと明らかにJSONエンコードが占める時間が減っており、効果は確かでした。
Score: 992
ログが見えるように
ログが確認できるように色々と試行錯誤した結果、pprofの導入と一緒に入れたコードによる仕業だと判明したのでそちらを削除、無事ログの確認ができるようになりました。
searchEstateNazotteのN+1改善
ログが確認できるようになったことで、searchEstateNazotteのデバッグが行えるようになり、ここでバグの修正を完了してベンチの完走まで持っていくことができました。
450点ほどスコアが伸びたので、非常に嬉しかったです。
Socre: 1454
ここで次に何に取り組もうかを考えていたところ、相方が「Nginx + AppサーバってほとんどCPUもメモリも使いきれていないけど、DBサーバのロードアベレージがずっと高いところで張り付いている」と気付きをシェアしてくれました。
(本当に相方が優秀すぎて、今回ほとんど相方のいいなりに修正しているだけな気がします...)
「DBを2台にして負荷分散を行おう」という話になったのですが、今回のソースを見ると様々なクエリでWHERE句やORDER BY句が使われていて、searchEstateNazotte
に至っては座標計算まで行われてしまっていました。
そこでRedisへのデータ移行は実質不可能と判断し、MySQLをマスタースレーブ構成にしてレプリケーションするという方針にしました。
作業としては
するといった分担にしました。
自分はmssqlxというslqx互換のライブラリを見つけ「これひょっとして運営の意図した通りじゃね!」と喜んで組み込んでいました。
一方で相方はここでも優秀で、初めての試みでちゃんとMySQLのレプリケーションを組んでくれました!
しかしベンチを回すと、Updateが頻繁に発生し、Readした際に不整合が発生するという問題が発覚しました。
この不整合をどう解決しようかと探っていたところ、タイムアップ。
最後にnewrelic infraやpprofを外し、最後にベンチを回して終了しました。
感想
最後のDBがボトルネックになっている問題が解決できなくて非常に悔しかったです、これさえクリアできれば予選突破できそう!と思っていただけに残念でした。
ですがチーム内で連携して、議論して、少しずつスコアを上げていけたので体験や学びとしては非常によかったです!
あとはちゃんと「何がボトルネックになっているのか」を見抜けるよう監視の読み方を頑張るのと、サービスを開発/運用していく上でのノウハウをいっぱい貯めていかなきゃという感じです。
ISUCON10予選参加者のみなさん、8時間お疲れ様でした!