feature image

2025年2月22日 | ブログ記事

冬ハッカソンでISUCON用 database driver 作りました

これは2024年冬ハッカソン参加記事です。

作ったもの

GitHub - traP-jp/isuc: Auto caching SQL database driver for ISUCON
Auto caching SQL database driver for ISUCON. Contribute to traP-jp/isuc development by creating an account on GitHub.


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 を使うには前準備が必要です。

の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 型がないことにひたすらキレていました。誰かベストプラクティスを教えてくださいお願いします。

pirosiki icon
この記事を書いた人
pirosiki

23B。SysAd班でがんばってます

cp20 icon
この記事を書いた人
cp20

23B / icon: https://twitter.com/sora_douhu

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2025年2月4日
冬ハッカソン2024 22班 「Queen Bee」
YHz_ikiri icon YHz_ikiri
2025年2月3日
ユナ (You and) / 𝑩𝑰𝑮 𝑳𝑶𝑽𝑬 feat. 初音ミク 【2024年冬ハッカソン1班】
hijoushiki icon hijoushiki
2024年12月18日
ISUCON14にツールの力で勝ちたかった
mazrean icon mazrean
2023年11月26日
ISUCON13にツールの力で勝ちたかった(mazrean)
mazrean icon mazrean
2021年9月21日
ISUCON11 traP CM制作についての小話
dan_dan icon dan_dan
2025年2月9日
冬ハッカソンで定理証明支援系 「lapisla-prover」(and more !) を開発しました!
abap34 icon abap34
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記