初めてのISUCON参加して680点あたりで終わった話です。
eqval
という3人チームでGo
言語で挑みまして、僕はアプリ側を担当しました。
🚧 予選前の準備
自分はインフラ周りはあんまり詳しくなかったので, チームメンバーのnoraさんが全部準備しました。
秘伝のタレとも呼ばれているもの
環境変数, エイリアス, MySQL, nginxの設定ファイルなど
Makefileに各種コマンドのまとめ
- 各種設定ファイルの変更を適
- アプリのbuild, deploy
- slow query log, access logをSlackに送る
- pt-query-digest, alpでlogを集計した結果を送ってます
👨🏻💻 予選でやったこと
Google Cloud Profiler
pprof
を使うとベンチマークが回るタイミングに合わせて, プロファイリングかけることや
結果ファイルの変換処理などにも手間がかかると判断して, Google Cloud Profilerを使いました。
ISUCON9予選問題で模擬テストをしたときに, かなりわかりやすかったので今回も迷わず入れました。
ISUCON10のちょっと改善した結果もちょっと混じっているけどこんな感じ
なぞって検索と物件, 椅子検索処理が累計時間が多いことはわかりますし, SQLを2回以上実行していることがわかります。
(キャプチャでは分かりづらいですが, 詳細を見るとわかります)
ただ, 実行時間はアクセスログでも把握できましたので, Profilerはソースコードのどこを先に見るべきかの目安として使いました。
Goのバージョン上げ
既存の1.14.7
から1.15.1
に上げましたが, 特にベンチマーク点数は変動しませんでした。
DBのインスタンス分け
- Server 1 : Webサーバー, アプリ
- Server 2 : DB
- Server 3 : 予備
アプリ修正
なぞって検索の修正 (ベンチマーク結果680)
- GIS関連メソッド呼び出しを1回のみに変更
- 検索結果の上限
for _, estate := range estatesInBoundingBox {
...
// 改善 : coordinates.coordinatesToText()は同じ結果なのでforのそとで処理し変数で渡す
query := fmt.Sprintf(`SELECT * FROM estate WHERE id = ? AND ST_Contains(ST_PolygonFromText(%s), ST_GeomFromText(%s))`, coordinates.coordinatesToText(), point)
err = db.Get(&validatedEstate, query, estate.ID)
if err != nil {
...
} else {
estatesInPolygon = append(estatesInPolygon, validatedEstate)
}
}
var re EstateSearchResponse
re.Estates = []Estate{}
// 改善 : NazotteLimitが50固定なので, forで50件上限を達したらbreakするように変更
if len(estatesInPolygon) > NazotteLimit {
re.Estates = estatesInPolygon[:NazotteLimit]
} else {
re.Estates = estatesInPolygon
}
JSONエンコーダーをfrancoispqt/gojay
に変更したが, やめた
Profilerを見た感じ, DBからのデータ取得よりJSON Encodeに時間がかかっていたので
searchEstates
とsearchChairs
にGoJayでEncodeするように変更しましたが, ベンチマークとProfilerの変動がなかったので取り消しました。
もっとパフォーマンスが良いJSONエンコーダーを知っておくべきでした。
DB connection数上限削除
connection数が10になっていたので, 削除して無制限にしましたが, 効果が得られませんでした。
db.SetMaxOpenConns(10)
アプリのインスタンス分け
- Server 1 : Webサーバー, アプリ
- Server 2 : DB
- Server 3 : アプリ
一部処理は3番サーバーでするように、アプリの負荷を分散しましたが、ベンチマークの結果は変わりませんでした。
👾 失敗したこと
DB replication
DBのCPU使用率が非常に高く、Server 2
とServer 3
でレプリカし3番はSelect専用にしました。
ベンチマークをまわした結果、
post処理のCSVデータinsert、在庫を一つ減らすupdate処理のあとにデータ同期が終わる前に
get処理でのSlave DBからのselectが走り、不正データになりました。
INSERT時はレスポンス前に0.5秒sleepさせるなど色々と試してみましたが, 結果は同じでした。
安い椅子一覧のキャッシュ
getLowPricedChair()
もアクセス数が多かったので、レスポンスを早くするために
安い椅子一覧の結果をメモリ上にキャッシュしました。
insert, update処理があるときにスライスに再格納するようにしました。
var StockChairs []Chair
func loadChairs() {
StockChairs = make([]Chair, 0)
query := `SELECT * FROM chair WHERE stock > 0 ORDER BY price ASC, id ASC LIMIT ?`
db.Select(&StockChairs, query, Limit)
}
アプリが動いてWeb上では結果が確認できましたが、ベンチマークでは不正データになり、0点の結果でした。
COUNT(*) SQLの改善
物件, 椅子の検索結果で該当ページデータと, 全ページ数計算用のためにcount(*)
を返していたのを一つのSQLにしました。
SELECT ... FROM estate WHERE {同じ条件} LIMIT ? OFFSET ?;
SELECT COUNT(*) FROM estate WHERE {同じ条件};
SELECT ..., estate_cnt.cnt
FROM estate
JOIN (SELECT COUNT(*) cnt FROM estate WHERE {同じ条件}) estate_cnt
WHERE {同じ条件} LIMIT ? OFFSET ?;
このときも、アプリは動いてたので、これでよし!と思いましたが
ベンチマークでは不正データと判断されて0点になってしまいました。
ついでに、SQLは既存に戻し、COUNT(*)だけ goroutineで並行処理してみてもアプリが動きましたが、ベンチマークは0点になりました。
SQLのOR条件を 5つから1つに減らす (対応できなかった)
自分がcount(*)
問題に時間を使いすぎてしまい、
メンバーから分析して改善余地がある部分だと共有はもらった、
椅子にあうおすすめ物件のOR
を一つに減らすところに対応できませんでした。
もっと早い段階で、自分が手こずっていたところを諦めて、先にこの対応をすれば良かったなと後悔します・・
var estates []Estate
w := chair.Width
h := chair.Height
d := chair.Depth
query = `SELECT * FROM estate WHERE (door_width >= ? AND door_height >= ?) OR (door_width >= ? AND door_height >= ?) OR (door_width >= ? AND door_height >= ?) OR (door_width >= ? AND door_height >= ?) OR (door_width >= ? AND door_height >= ?) OR (door_width >= ? AND door_height >= ?) ORDER BY popularity DESC, id ASC LIMIT ?`
err = db.Select(&estates, query, w, h, w, d, h, w, h, d, d, w, d, h, Limit)
💺 感想
初めての参加で、自分の知識と力不足をものすごく感じて悔しかったですが、それ以上にすごくドキドキして楽しかった一日でした!
テーブルも2つですごくシンプルで、0.1秒基準のSlow queryもなかったので、ボトルネック探すのが大変でした。
また、本格的にアプリ側で手を動かし始めたのが中盤が超えてからだったので、慌てたり頭が回らなかったりもしました。
競技がおわったあとに気づきましたが、アプリ側でやれることは多かったと思います。
まだまだ、自分がgoになれてなかったのも原因かと思います。
インフラの知識をつけることと、もっとgoをすらすら書けるようにして、来年のISUCON11にも参加します!