ISUCON練習実況ログ 2024/10/10
2024/10/10にISUCON13の練習をisunarabeを使って行った時のログを記録する
- 友人と勉強会でやったので時間がなく以下をskip
- gitの導入とGitHubへのバックアップ
- ブラウザ動作確認
- エディタ周りの環境設定
- ファイル編集は何も入れてないvimを使い、コード検索はfindやgrepを使った。
セットアップ
isunarabe でISUCON13 v1.0を選択してAWS CloudFormationを使って環境を立てた。
2024/10/10現在、isunarabeのAMIに含まれる証明書が古くベンチマークが失敗する。そのため以下の手順で更新する。
手元のマシン $ ssh isu1 # EC2の画面から確認したpublic ipに対してsshする。鍵はGitHubに登録しているものを使う
ec2 $ vim update-certs.sh
# --- 以下の内容を書き込む ---
#!/bin/bash
sudo curl -L -s -o /etc/nginx/tls/_.t.isucon.pw.crt https://github.com/KOBA789/t.isucon.pw/releases/latest/download/fullchain.pem
sudo curl -L -s -o /etc/nginx/tls/_.t.isucon.pw.key https://github.com/KOBA789/t.isucon.pw/releases/latest/download/key.pem
sudo chmod 0600 /etc/nginx/tls/_.t.isucon.pw.crt
sudo chmod 0600 /etc/nginx/tls/_.t.isucon.pw.key
sudo systemctl reload nginx
# --- 内容終わり ---
ec2 $ bash update-certs.sh
# これで証明書が更新され、ベンチマークが通るようになる
初回ベンチマークは 3183 点
計測ツールを入れる
インストール
cd /usr/bin
curl -L https://github.com/tkuchiki/alp/releases/download/v1.0.21/alp_linux_amd64.tar.gz | sudo tar -xz
sudo apt-get update -y
sudo apt-get install -y percona-toolkit dstat
有効化
htop
元から入っている。htopと打てばOK
alp
/etc/nginx/nginx.conf
を編集
- access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
+ log_format ltsv "time:$time_local"
+ "\thost:$remote_addr"
+ "\tforwardedfor:$http_x_forwarded_for"
+ "\treq:$request"
+ "\tstatus:$status"
+ "\tmethod:$request_method"
+ "\turi:$request_uri"
+ "\tsize:$body_bytes_sent"
+ "\treferer:$http_referer"
+ "\tua:$http_user_agent"
+ "\treqtime:$request_time"
+ "\tcache:$upstream_http_x_cache"
+ "\truntime:$upstream_http_x_runtime"
+ "\tapptime:$upstream_response_time"
+ "\tvhost:$host";
+ access_log /var/log/nginx/access.log ltsv;
nginxをリロード
sudo nginx -t
sudo systemctl reload nginx
pt-query-digest
/etc/mysql/mysql.conf.d/mysqld.cnf
を編集
[mysqld]
slow_query_log=1
slow_query_log_file=/var/log/mysql/mysql-slow.log
long_query_time=0
確認
$ sudo mysql
mysql > show variables like 'slow%';
+---------------------+-------------------------------+
| Variable_name | Value |
+---------------------+-------------------------------+
| slow_launch_time | 2 |
| slow_query_log | ON |
| slow_query_log_file | /var/log/mysql/mysql-slow.log |
+---------------------+-------------------------------+
3 rows in set (0.01 sec)
ONになっていてよさそう
/var/log/mysql/mysql-slow.log
にログがある
これで pt-query-digest や alp が使えるようになった。pprofはまだ使い方が分からず入れてない。
livestream_tags
へのインデックス
htopをみると以下のようになった。DBが重い。
- DB: mysqld 140%
- APP: isupipe 30%
- 水責め: pdns_server 10%
pt-query-digestをみる。
ベンチ回した後に末尾1000行を取得
$ mkdir ~/result
$ cd ~/result
$ sudo tail -n 1000 /var/log/mysql/mysql-slow.log > ./slow-1.log
$ pt-query-digest ./slow-1.log
pt-query-digestの結果
# 1 0xF7144185D9A142A426A36DC... 119.4592 26.4% 5143 0.0232 0.01 SELECT livestream_tags
# 2 0x84B457C910C4A79FC9EBECB... 56.6751 12.5% 9354 0.0061 0.01 SELECT icons
# 3 0xDA556F9115773A1A99AA016... 45.7546 10.1% 125402 0.0004 0.00 ADMIN PREPARE
重いやつ
SELECT * FROM livestream_tags WHERE livestream_id = 7524\G
mysqlでEXPLAINする
mysql> EXPLAIN SELECT * FROM livestream_tags WHERE livestream_id = 7524;
+----+-------------+-----------------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| 1 | SIMPLE | livestream_tags | NULL | ALL | NULL | NULL | NULL | NULL | 11017 | 10.00 | Using where |
+----+-------------+-----------------+------------+------+---------------+------+---------+------+-------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
rowsが11017で、livestream_tags
の数をみると、だいたい同じ数
mysql> SELECT COUNT(*) FROM livestream_tags;
+----------+
| COUNT(*) |
+----------+
| 11343 |
+----------+
1 row in set (0.00 sec)
というわけで livestream_id
にインデックスを貼る
mysql> SHOW INDEX FROM livestream_tags; # インデックスがないことを確認
mysql> desc livestream_tags; # スキーマ確認
+---------------+--------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| livestream_id | bigint | NO | | NULL | |
| tag_id | bigint | NO | | NULL | |
+---------------+--------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
mysql> ALTER TABLE livestream_tags ADD INDEX livestream_idx(livestream_id); # インデックス貼った
ベンチ回してみたらボトルネックが移動した。
reaction改善
DBがまだ重いのでpt-query-digestを見る
# 1 0xDB74D52D39A7090F224C4DEEAF3... 0.0977 47.2% 1 0.0977 0.00 SELECT users livestreams reactions
# 2 0x84B457C910C4A79FC9EBECB8B10... 0.0458 22.1% 2 0.0229 0.00 SELECT icons
# 3 0xDA556F9115773A1A99AA0165670... 0.0184 8.9% 26 0.0007 0.01 ADMIN PREPARE
対象クエリ
SELECT COUNT(*) FROM users u
INNER JOIN livestreams l ON l.user_id = u.id
INNER JOIN reactions r ON r.livestream_id = l.id
WHERE u.id = 242\G
クエリは SELECT COUNT… みたいな感じだったので、コード検索
find ./ -type f -print | xargs grep -A 3 -B 3 "COUNT"
スキーマを見ると、以下のようになっている
mysql> desc users;
+--------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | UNI | NULL | |
| display_name | varchar(255) | NO | | NULL | |
| password | varchar(255) | NO | | NULL | |
| description | text | NO | | NULL | |
+--------------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)
mysql> desc livestreams;
+---------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| user_id | bigint | NO | | NULL | |
| title | varchar(255) | NO | | NULL | |
| description | text | NO | | NULL | |
| playlist_url | varchar(255) | NO | | NULL | |
| thumbnail_url | varchar(255) | NO | | NULL | |
| start_at | bigint | NO | | NULL | |
| end_at | bigint | NO | | NULL | |
+---------------+--------------+------+-----+---------+----------------+
8 rows in set (0.00 sec)
mysql> desc reactions;
+---------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| user_id | bigint | NO | | NULL | |
| livestream_id | bigint | NO | | NULL | |
| emoji_name | varchar(255) | NO | | NULL | |
| created_at | bigint | NO | | NULL | |
+---------------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)
userに紐づくlivestreamを全部撮ってきて、それぞれに紐づくreactionの数を数えている。user idは固定値として与えられるので、usersを連結させる必要がない。
以下のように書き換えられる。
SELECT COUNT(*) FROM livestreams l
INNER JOIN reactions r ON r.livestream_id = l.id
WHERE l.user_id = ?
ベンチ回してみたらボトルネックが移動した。
iconsをDBから静的ファイルとして返す
まだDBが重い
# 1 0x84B457C910C4A79FC9EBECB8B10... 0.0425 44.6% 4 0.0106 0.00 SELECT icons
# 2 0xDA556F9115773A1A99AA0165670... 0.0147 15.5% 22 0.0007 0.00 ADMIN PREPARE
# 3 0xFFFCA4D67EA0A788813031B8BBC... 0.0084 8.8% 1 0.0084 0.00 COMMIT
対象クエリ
SELECT image FROM icons WHERE user_id = 1049\G
iconsのスキーマ
mysql> desc icons;
+---------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+----------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| user_id | bigint | NO | | NULL | |
| image | longblob | NO | | NULL | |
+---------+----------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
longblobで画像がそのままDBに入っている。
iconsに関わるところを抜き出す
isucon@ip-192-168-0-11:~/webapp/go$ find ./ -type f -print | xargs grep -A 3 -B 3 " icons"
./user_handler.go- }
./user_handler.go-
./user_handler.go- var image []byte
./user_handler.go: if err := tx.GetContext(ctx, &image, "SELECT image FROM icons WHERE user_id = ?", user.ID); err != nil {
./user_handler.go- if errors.Is(err, sql.ErrNoRows) {
./user_handler.go- return c.File(fallbackImage)
./user_handler.go- } else {
--
./user_handler.go- }
./user_handler.go- defer tx.Rollback()
./user_handler.go-
./user_handler.go: if _, err := tx.ExecContext(ctx, "DELETE FROM icons WHERE user_id = ?", userID); err != nil {
./user_handler.go- return echo.NewHTTPError(http.StatusInternalServerError, "failed to delete old user icon: "+err.Error())
./user_handler.go- }
./user_handler.go-
./user_handler.go: rs, err := tx.ExecContext(ctx, "INSERT INTO icons (user_id, image) VALUES (?, ?)", userID, req.Image)
./user_handler.go- if err != nil {
./user_handler.go- return echo.NewHTTPError(http.StatusInternalServerError, "failed to insert new user icon: "+err.Error())
./user_handler.go- }
--
./user_handler.go- }
./user_handler.go-
./user_handler.go- var image []byte
./user_handler.go: if err := tx.GetContext(ctx, &image, "SELECT image FROM icons WHERE user_id = ?", userModel.ID); err != nil {
./user_handler.go- if !errors.Is(err, sql.ErrNoRows) {
./user_handler.go- return User{}, err
./user_handler.go- }
grep: ./isupipe: binary file matches
GETが2件、DELETEとINSERTが1件ずつある。
c.File(fallbackImage)
みたいに返したい。
GETについては以下のようにした
image_path := fmt.Sprintf("../icons/%s.jpg", user.ID)
if _, err := os.Stat(image_path); err != nil {
return c.File(fallbackImage)
} else {
return c.File(image_path)
}
image_path := fmt.Sprintf("../icons/%d.jpg", userModel.ID)
if _, err := os.Stat(image_path); err != nil {
return User{}, err
}
image, err := os.ReadFile(image_path)
みたいなコードを書いた(後にuser.IDがint64なことに気づき %d.jpg
に修正)
DELETE, INSERTの時はlastInsertIdが必要だったので、以下のようなコードを書いた(記録がなくてうろ覚え)
var user_id int64 = 0 // globalに宣言
// DELETE, INSERT
image_path := fmt.Sprintf("../icons/%d.jpg", userID)
var f *os.File
if _, err := os.Stat(image_path); err == nil {
f, err = os.Open(image_path)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to open user icon: "+err.Error())
}
} else {
f, err = os.Create(image_path)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to create user icon: "+err.Error())
}
}
_, err = f.Write(req.Image)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to write user icon: "+err.Error())
}
user_id++
iconsディレクトリを作ってベンチ
これでDB 120%、APP 45%になった。
まだDBが重いが、ボトルネックが移動した。
初期化失敗
icons/*
を消す処理をinitializeに追加
rm /home/isucon/webapp/icons/*
整合性チェック通るようになった。
ng_words
にインデックス
# 1 0x64CC8A4E8E4B390203375597CE4... 0.0388 42.0% 1 0.0388 0.00 SELECT ng_words
# 2 0xDA556F9115773A1A99AA0165670... 0.0109 11.9% 30 0.0004 0.00 ADMIN PREPARE
# 3 0xFFFCA4D67EA0A788813031B8BBC... 0.0091 9.9% 3 0.0030 0.00 COMMIT
SELECT id, user_id, livestream_id, word FROM ng_words WHERE user_id = 1088 AND livestream_id = 7555\G
EXPLAINの結果、 user_id
にとりあえずインデックスを貼ることにした(本当は livestream_id
もインデックス貼った方がいいけど、一旦1つだけの変更を入れることに)
ボトルネックが移動した
ADMIN PREPARE
# 1 0xDA556F9115773A1A99AA0165670... 0.0161 30.0% 28 0.0006 0.00 ADMIN PREPARE
# 2 0x38BC86A45F31C6B1EE324671506... 0.0084 15.7% 4 0.0021 0.00 SELECT themes
よく分からんが検索したら以下の記事を見つけた
ISUCON10予選で12位になり本選進出を決めました - Gマイナー志向
main.go
をいじる
conf.DBName = "isupipe"
conf.ParseTime = true
conf.InterpolateParams = true // 追加
この改善で スコア: 7871
ボトルネックが移動
reservation_slots
にインデックス
# 1 0x7F9C0C0BA9473953B723EE16C08... 0.0845 39.3% 1 0.0845 0.00 SELECT reservation_slots
# 2 0xC3F9945902750E1ADB88136F895... 0.0569 26.5% 2 0.0284 0.00 SELECT livestreams reactions
# 3 0xF1B8EF06D6CA63B24BFF433E06C... 0.0168 7.8% 2 0.0084 0.00 SELECT users livestreams livecomments
クエリ
SELECT * FROM reservation_slots WHERE start_at >= 1701648000 AND end_at <= 1701651600 FOR UPDATE\G
start_at, end_at にインデックスを貼る
スコア: 8607
ボトルネックが移動
livecomments のクエリ改善
# 1 0xF1B8EF06D6CA63B24BFF433E06C... 0.0926 42.9% 3 0.0309 0.02 SELECT users livestreams livecomments
# 2 0xC3F9945902750E1ADB88136F895... 0.0417 19.3% 3 0.0139 0.00 SELECT livestreams reactions
# 3 0xFFFCA4D67EA0A788813031B8BBC... 0.0274 12.7% 3 0.0091 0.00 COMMIT
クエリ
SELECT IFNULL(SUM(l2.tip), 0) FROM users u
INNER JOIN livestreams l ON l.user_id = u.id
INNER JOIN livecomments l2 ON l2.livestream_id = l.id
WHERE u.id = 111\G
これもよく見るとusersがいらない。以下のように書き換える
SELECT IFNULL(SUM(l2.tip), 0) FROM livestreams l
INNER JOIN livecomments l2 ON l2.livestream_id = l.id
WHERE l.user_id = ?
ボトルネックが移動
スコア 8558
livestream_id
インデックス
以下の2つが25%ずつで重い。まだDBが重いけど、正直alpとか他のやつ見た方がいい気もしてくる。どこでDBから離れるべきか分からない…
SELECT IFNULL(SUM(l2.tip), 0) FROM livestreams l
INNER JOIN livecomments l2 ON l2.livestream_id = l.id
WHERE l.user_id = 109\G
SELECT COUNT(*) FROM livestreams l
INNER JOIN reactions r ON r.livestream_id = l.id
WHERE l.user_id = 76\G
よく見ると、 l2
や r
にインデックスが貼られてない。
livecommentsの方
mysql> show index from livecomments
-> ;
+--------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+--------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| livecomments | 0 | PRIMARY | 1 | id | A | 1520 | NULL | NULL | | BTREE | | | YES | NULL |
+--------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
1 row in set (0.01 sec)
alter table livecomments add index livestream_id_idx(livestream_id);
を入れたらボトルネックが移動
alter table reactions add index livestream_id_idx(livestream_id);
でreactionも対応
ここでスコアが10000を超えた。ボトルネックが移動
themeにインデックス
# 1 0x38BC86A45F31C6B1EE324671506... 0.0361 28.6% 10 0.0036 0.00 SELECT themes
# 2 0xF7144185D9A142A426A36DC55C1... 0.0189 15.0% 6 0.0032 0.00 SELECT livestream_tags
# 3 0x4ADE2DC90689F1C4891749AF54F... 0.0142 11.3% 11 0.0013 0.00 DELETE SELECT livecomments
SELECT * FROM themes WHERE user_id = 1135\G
user_id
にインデックスを貼る
ボトルネックが移動
切り上げる
# 1 0x4ADE2DC90689F1C4891749AF54F... 0.0298 51.4% 41 0.0007 0.00 DELETE SELECT livecomments
# 2 0xFD38427AE3D09E3883A680F7BAF... 0.0100 17.2% 17 0.0006 0.00 SELECT livestreams livecomments
# 3 0xC499D81D570D361DB61FC43A94B... 0.0084 14.4% 16 0.0005 0.00 SELECT livestreams reactions
DELETE FROM livecomments
WHERE
id = 551 AND
livestream_id = 7626 AND
(SELECT COUNT(*)
FROM
(SELECT '***' AS text) AS texts
INNER JOIN
(SELECT CONCAT('%', '***', '%') AS pattern) AS patterns
ON texts.text LIKE patterns.pattern) >= 1\G
厳ついクエリ
// NGワードにヒットする過去の投稿も全削除する
for _, ngword := range ngwords {
// ライブコメント一覧取得
var livecomments []*LivecommentModel
if err := tx.SelectContext(ctx, &livecomments, "SELECT * FROM livecomments"); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get livecomments: "+err.Error())
}
for _, livecomment := range livecomments {
query := `
DELETE FROM livecomments
WHERE
id = ? AND
livestream_id = ? AND
(SELECT COUNT(*)
FROM
(SELECT ? AS text) AS texts
INNER JOIN
(SELECT CONCAT('%', ?, '%') AS pattern) AS patterns
ON texts.text LIKE patterns.pattern) >= 1;
`
if _, err := tx.ExecContext(ctx, query, livecomment.ID, livestreamID, livecomment.Comment, ngword.Word); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to delete old livecomments that hit spams: "+err.Error())
}
}
}
ここの箇所だと分かったけどここまでで切り上げた。
スコア: 12173
まとめ
- DBがどれくらい下がったらpt-query-digestから離れていいんだろうか。分からん
- 去年のISUCONではメンバーが強くて何してるか分からんけどスコアが上がっていくという状態だったので、なかなかそのスコア(11000点くらい)を超えられず困っていた。でも今回は超えられて嬉しい(友人と一緒にやってはいるが、全部の改善点を理解している点で成長した)
- インデックス貼るの慣れてきた。次の手札が欲しい。
- コードがっつり改善とか、複数台構成とか、まだやれてない