これは2024年冬ハッカソン参加記事です。
作ったもの
ISUCON 用の cache 付き SQL database driver 「isuc」 を作りました。
普段GoでMySQLに接続する時、こんなコードを書きます。
db, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/mydb")
これを
db, err := sql.Open("mysql+cache", "user:pass@tcp(127.0.0.1:3306)/mydb")
にするだけで、クエリの結果が cache されるようになる driver です。
ISUCONとは
「Iikanjini Speed Up Contest」です。与えられたWebサービスを制限時間内にどれだけ高速化できるかを競うコンテストです。
https://isucon.net/
使い方
isuc を使うには前準備が必要です。
- Install
- Extract SQL
- Generate Cache Plan
- Generate Driver
の4ステップです。
Install
go install github.com/traP-jp/isuc/cli/isuc
Extract SQL
アプリ内でどんなクエリが実行されているかを取得します。静的に取得する方法と動的に取得する方法の2種類あります。
static extractor
isuc extract --out extracted.sql ./...
package内のクエリを静的に取ってきます。今のところ動的な LIMIT
の追加などには対応できてないです。
dynamic extractor
static extractor よりも確実な方法です。
import (
"database/sql"
dynamic_extractor "github.com/traP-jp/isuc/extractor/dynamic"
)
func main() {
db, err := sql.Open("mysql+analyzer", "user:pass@tcp(127.0.0.1:3306)/mydb")
dynamic_extractor.StartServer()
// ...
}
ベンチマークを回したあと
curl -sSf http://127.0.0.1:10000 -o extracted.sql
でクエリを取得できます。
Generate Cache Plan
取得したSQLとテーブル定義から cache plan を生成します。
isuc analyze --sql extracted.sql --schema schema.sql --out isuc.yaml
生成される cache plan 例 (ISUCON 13)
queries:
- query: SELECT * FROM tags;
type: select
table: tags
cache: true
targets:
- id
- name
- query: SELECT * FROM users WHERE id = ?;
type: select
table: users
cache: true
targets:
- description
- display_name
- id
- name
- password
conditions:
- column: id
operator: eq
placeholder:
index: 0
- query: INSERT INTO ng_words (user_id, livestream_id, word, created_at) VALUES (?);
type: insert
table: ng_words
columns:
- user_id
- livestream_id
- word
- created_at
Generate Driver
cache plan から driver 用のコードを生成します。
isuc generate --plan isuc.yaml --schema schema.sql cache
cache
の部分は生成したい package 名です。
生成した driver を使う
package main
import (
"database/sql"
_ "my_module/cache"
)
func main() {
db, err := sql.Open("mysql+cache", "user:pass@tcp(127.0.0.1:3306)/mydb?interpolateParams=true")
}
普段 "mysql"
としている箇所を "mysql+cache"
とするだけです。
キャッシュヒット率を取得したい場合は、cache.ExportMetrics
関数を使用することができます。
func main() {
// ...
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, cache.ExportMetrics())
})
http.ListenAndServe(":10000", mux)
}()
// ...
}
こんな感じで使うとベンチマーク後に curl http://localhost:10000/metrics
でキャッシュヒット率を取得できます。
うれしさ
時間節約
ISUCONに参加したことのある方ならば分かると思いますが、ISUCONは圧倒的に時間が足りません。isuc を使えば比較的短時間ですべてのクエリにキャッシュを入れることができます。
安全なキャッシュ
自分でキャッシュを入れると、実装ミスによってベンチマークの整合性チェックが通らなくなることもあります(n敗)。isuc を使えば安全にキャッシュをいれることができます。
点数が上がる(かも)
isucを使うだけでスコア爆増! となればいいですが、ISUCONはそこまで簡単ではありません。基本的にキャッシュが効くとアプリは高速化しますが、点数が上がるかは問題とキャッシュの相性や、改善を入れるタイミングによります。
点数が上がるとうれしいですが、たとえ上がらなかったとしても、その後の改善でキャッシュのことをあまり考えなくてよくなるのが大きいと思っています。
比較的キャッシュと相性がいいISUCON13で使ってみるとこのくらいヒットします。
query: "SELECT * FROM tags;"
[###################-] (98.68% - 75/76)
1 replace (272.026µs) / size = 1
query: "SELECT COUNT(*) FROM livestream_viewers_history WHERE livestream_id = ?;"
[########------------] (44.44% - 8/18)
10 replace (1.552671ms) / size = 0
query: "SELECT * FROM livecomments WHERE livestream_id = ?;"
[########------------] (44.44% - 8/18)
10 replace (1.572854ms) / size = 0
query: "SELECT * FROM livecomments;"
[--------------------] (0.00% - 0/2)
2 replace (5.221333ms) / size = 0
query: "SELECT * FROM tags WHERE id = ?;"
[###################-] (99.78% - 45773/45876)
103 replace (13.414767ms) / size = 103
query: "SELECT * FROM livestreams;"
[##########----------] (50.00% - 1/2)
1 replace (19.456195ms) / size = 0
query: "SELECT id FROM users WHERE name = ?;"
[###-----------------] (16.67% - 1/6)
5 replace (35.187649ms) / size = 5
query: "SELECT id FROM tags WHERE name = ?;"
[#-------------------] (6.67% - 1/15)
14 replace (43.526712ms) / size = 14
query: "SELECT * FROM livecomment_reports WHERE livestream_id = ?;"
[##############------] (70.91% - 39/55)
16 replace (85.882744ms) / size = 8
query: "SELECT * FROM ng_words WHERE user_id = ? AND livestream_id = ? ORDER BY created_at DESC;"
[##########----------] (53.12% - 17/32)
15 replace (91.781124ms) / size = 1
query: "SELECT * FROM users;"
[##------------------] (14.29% - 1/7)
6 replace (94.627667ms) / size = 0
query: "SELECT * FROM livestreams WHERE id = ? AND user_id = ?;"
[--------------------] (0.00% - 0/12)
12 replace (137.94508ms) / size = 0
query: "SELECT * FROM livestreams WHERE user_id = ?;"
[##############------] (70.64% - 77/109)
32 replace (193.585147ms) / size = 11
query: "SELECT * FROM livecomments WHERE id = ?;"
[#####---------------] (26.67% - 16/60)
44 replace (302.164968ms) / size = 9
query: "SELECT id, user_id, livestream_id, word FROM ng_words WHERE user_id = ? AND livestream_id = ?;"
[###############-----] (79.62% - 293/368)
75 replace (409.374539ms) / size = 11
query: "SELECT * FROM themes WHERE user_id = ?;"
[###################-] (95.33% - 18285/19180)
895 replace (1.491563714s) / size = 895
query: "SELECT * FROM users WHERE id = ?;"
[###################-] (95.33% - 18278/19173)
895 replace (1.622740948s) / size = 895
query: "SELECT * FROM livecomments WHERE livestream_id = ? ORDER BY created_at DESC;"
[--------------------] (0.00% - 0/363)
363 replace (2.736529973s) / size = 4
query: "SELECT * FROM reactions WHERE livestream_id = ? ORDER BY created_at DESC;"
[--------------------] (0.00% - 0/399)
399 replace (2.767039106s) / size = 23
query: "SELECT image FROM icons WHERE user_id = ?;"
[###################-] (95.41% - 22073/23134)
1061 replace (3.035136299s) / size = 901
query: "SELECT * FROM livestreams WHERE id = ?;"
[################----] (84.68% - 8255/9748)
1493 replace (3.495382206s) / size = 1493
query: "SELECT * FROM livestream_tags WHERE livestream_id = ?;"
[#################---] (86.26% - 9820/11384)
1562 replace (3.817947626s) / size = 1562
query: "SELECT * FROM users WHERE name = ?;"
[################----] (84.10% - 3349/3982)
633 replace (4.875192413s) / size = 633
query: "SELECT slot FROM reservation_slots WHERE start_at = ? AND end_at = ?;"
[--------------------] (0.00% - 0/769)
769 replace (7.532755294s) / size = 18
今後について
ハッカソンの1週間+延長 で作ったものなので、作りが粗い部分が残ってしまっています。private-isuとISUCON13で動くことは確認していますが、他ではまだ試せていないので、次回ISUCONまでに信頼して使えるようにしたいです。キャッシュヒット率の改善も行っていきたいです。
感想
pirosiki
キャッシュ部分を担当しました。ハッカソン期間中の開発ではテストを一切書かず、動くやろの気持ちでいたのですが、実際動かしてみると見事にトランザクション周りでバグらせてしまいました。テストは大事。ハッカソン後にはテストも書いてトランザクションへの対応もばっちりなはずです多分。パフォーマンスに関してはまだまだ改善の余地があるので、頑張っていきます。
開発が始まる前は難しそうで本当に作れるか不安でしたが、とても面白いものができてよかったです。みなさんもぜひ使ってみてください。
cp20
冬ハッカソンの応募段階でISUCON用のツールを作ることは決まっていて、テーマが決まる前に自動でキャッシュしてくれるツールを作ることを決めました。テーマ全無視です。すまんかった。
でもかなり面白いものが作れたと思っています。キャッシュするだけで点数が爆上がりすることを期待していたんですが、世の中はそんなに甘くなかったですね。今回のプロダクトの良いところはかなり安全側に倒している点で、とりあえず入れておけばいい感じにキャッシュしてくれます。誰でも使えるようになっているので、ぜひ使ってみてください!!!!
最後に感想っぽいことを書くと、Go をひたすら書いていたわけですが、union 型がないことにひたすらキレていました。誰かベストプラクティスを教えてくださいお願いします。
