ISUCON11本戦に出場して学生一位になった
ISUCON11の本戦に学生三人チームの雑談係で参加してきました。
結果は全体9位、学生1位でした。
いかすチームメンバー
名前(役割)
とさ(app)
まし(人力DB+app)
さんぽし(人力CI/CD)
みんな仕事を終えてappの改善に取り掛かっていたのでapp係の自分は実質無職です。
当日やったこと
app担当の自分が主に実装していた部分です。
alp, pprof, slow query
を参考にしながら、ボトルネックを発見していきます。
- pprofと再起動試験対策のdb接続poolingのスクリプトを仕込む
- シークを法を実装してページングの改善
getAnnouncementList GET /api/announcements お知らせ一覧取得
- ましくんが実装したこと聞いていたので思いつく(そして教えてもらいながら実装
- https://mesimasi.com/posts/backend-tuning-ca
- GetGradeのN+1を一つ削除
- IN句を用いて一度に取得するように変更
- 後半は適当な部分にlimitいれたりbulk insertを実装してみたりいろいろしましたが、決定打にならず
中盤、ページングクエリを解決したあたりで、GET /api/users/me/grades
からのレスポンスが遅くてベンチマークが落ちるケースがありました。
終盤もGET /api/users/me/grades
がnginxのログを見る限りかなり重かったのですが決定的な改善はできず。N+1をこまごま解決するなどで少しは速くなりましたが潰し切りたかったです。
3台構成にしていたものの、mysqldがほぼcpuをもっていっていたので、app側でキャッシュや計算処理を担当することができればもう少し点数が上がったかもしれません。
ところどころ、改善案やエラーの解消方法がわからなくなったところは画面を共有してペアプロをしてもらっていました。
感想
チームメンバーと練習を重ねてきたので、ボトルネックになっている箇所を特定する流れが非常にスムーズだったと思います。また、最終的に一番点数が高かった箇所のcommitまで戻すなど冷静な判断ができたのも非常に良かったと思います。
あとは、見つけたボトルネックをどのように改善したらいいのかの引き出しを増やしくこと、実装できるように馬力をつけることを意識していきたいと思いました。
最終結果としては75000点で学生一位、全体九位でした!
学生最後の出場で一位をとれてとてもうれしかったです!
他のチームメイトのブログ
自分用メモ
文字数が少なくて寂しいので、予選が始まる前に、心構え的なメモを書いていたので放流します。
心構え編
どうせ当日には全て忘れるけど
- 基本的な部分の解消がテンポよくできれば予選は突破できる(と思う)
- 本質的な改善を先に(最初から安易にmemcacheに走らない)
- alp -> pprofを見る
- まずは全体で遅い箇所->Appで対応が必要な箇所
- 改善できたからと言って点数が伸びるとは限らない
- 複数ボトルネックを解消する必要がある
- 改善できたかどうかは点数ではなくchromeのnetworkタブのレスポンス速度で確認すべし
- 速度=点数ではない場合があるので当日マニュアルをよく読もう
- 修正する箇所は関数に分割しよう
- すぐに戻れるように
- コメントを書いてコードを理解しよう
- 概要把握が大切
- ローカルで動作させられるようにしよう
- 時間をかけても大丈夫
- 最初の1~2時間はこちらに集中してもいいくらい
- mysqlにデータが入ればクエリ書き放題
- 2~3台目で練習もあり
- [コメント]今回は本戦も予選も準備しなかったです。時間がかかりすぎるのも問題なのでできる範囲の方がいいと思います。
- 大きいボトルネックは簡単な改善を積み重ねていく
- N+1はまずは複数回クエリを発行しているところを全取得->app側で処理に変えてみる
- その後に効率よく解決していけばいい
- リクエストが見られるならリスポンスをみよう
- 実装後に再度叩いてみて同じレスポンスが帰ってくるか確認する
- 速度は変わっているかどうか確認する
- 余裕があればテストを書く
具体的行動編
考えるべき箇所
- メモリ
- 帯域
- ディスクI/O
- フロントエンドCache(nginxでpublicつけるといけるらしい)
再起動対策
- DBにはpingで問い合わせる
- openは参照のみなので注意
MySQLのデータの内容を確かめる
- rows数が多いいテーブルをselect * from hogeしていないか
- memoryに載せるならサイズは大丈夫か
DBのサイズ
SELECT
table_schema, sum(data_length) /1024/1024 AS mb
FROM
information_schema.tables
GROUP BY
table_schema
ORDER BY
sum(data_length+index_length) DESC;
Tableのサイズ
SELECT
table_name, engine, table_rows AS tbl_rows,
avg_row_length AS rlen,
floor((data_length)/1024/1024) AS dmb,
floor((index_length)/1024/1024) AS imb
FROM
information_schema.tables
WHERE
table_schema=database()
ORDER BY
(data_length+index_length) DESC;
どんなクエリが書かれているか確かめる
queryを出してくれる
みるべきはupdateとinsertが走っているテーブル
それ以外は不変なので楽にキャッシュができる(再起動に注意)
top
メモリやCPU使用量をみる
top -s 4
とか良さそう
memoryは
top -s 1 -o mem
でソートできるそう
余計なこと編
- httpsならhttp2, http3にすることで高速化できる
コード編
再起動対策
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local",
user,
password,
host,
port,
dbname,
)
dbx, err = sqlx.Open("mysql", dsn)
if err != nil {
log.Fatalf("failed to connect to DB: %s.", err.Error())
}
defer dbx.Close()
for {
err := dbx.Ping()
if err == nil {
break
}
log.Println(err)
time.Sleep(time.Second * 1)
}
pprof
import (
_ "net/http/pprof"
)
go func() {
log.Println(http.ListenAndServe("0.0.0.0:6060", nil))
}()
Discussion