private-isu での素振り

概要
を参考にprivate-isu の練習のメモをしていくスクラップ
環境設定
AWSをちゃんと使ったことがないし、ここらへんの知識薄いので地味に手こずった

最初のスコアの確認
/home/isucon/private_isu.git/benchmarker/bin/benchmarker -u /home/isucon/private_isu.git/benchmarker/userdata -t http://XX
{"pass":true,"score":1016,"success":879,"fail":0,"messages":[]}
まずは計測をしてみる
mac からベンチマーカー
$ ab -c 1 -t 30 http://XX
競技用インスタンスでtopで計測
MySQLが90%になっていることを確認
MySQLの調査をする
MySQL でスロークエリを把握できるようにする
設定をする
競技用インスタンスで
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
負荷をかける
$ ab -c 1 -t 30 http://XX
スロークエリログを確認
sudo mysqldumpslow /var/log/mysql/mysql-slow.log
Reading mysql slow query log from /var/log/mysql/mysql-slow.log
Count: 714 Time=0.03s (20s) Lock=0.00s (0s) Rows=1.9 (1326), isuconp[isuconp]@localhost
SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N
Count: 34 Time=0.01s (0s) Lock=0.00s (0s) Rows=10008.0 (340272), isuconp[isuconp]@localhost
SELECT `id`, `user_id`, `body`, `created_at`, `mime` FROM `posts` ORDER BY `created_at` DESC
Count: 714 Time=0.01s (7s) Lock=0.00s (0s) Rows=1.0 (714), isuconp[isuconp]@localhost
SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = N
Count: 1 Time=0.00s (0s) Lock=0.00s (0s) Rows=0.0 (0), isuconp[isuconp]@localhost
SET NAMES utf8mb4
Count: 2040 Time=0.00s (0s) Lock=0.00s (0s) Rows=1.0 (2040), isuconp[isuconp]@localhost
SELECT * FROM `users` WHERE `id` = N
Count: 6732 Time=0.00s (0s) Lock=0.00s (0s) Rows=4852.4 (32666112), isuconp[isuconp]@localhost
#
Count: 3264 Time=0.00s (0s) Lock=0.00s (0s) Rows=0.0 (0), 0users@0hosts
administrator command: Close stmt
Count: 3468 Time=0.00s (0s) Lock=0.00s (0s) Rows=0.0 (0), 0users@0hosts
administrator command: Prepare
MySQL に入って事情確認
mysql -uisuconp -pisuconp isuconp
use isuconp
mysql> show create table comments;
+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| comments | CREATE TABLE `comments` (
`id` int NOT NULL AUTO_INCREMENT,
`post_id` int NOT NULL,
`user_id` int NOT NULL,
`comment` text NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=100025 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
index は特になし
mysql> EXPLAIN SELECT * FROM `comments` WHERE `post_id` = 9995 ORDER BY `created_at`
DESC LIMIT 3;
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
| 1 | SIMPLE | comments | NULL | ALL | NULL | NULL | NULL | NULL | 99666 | 10.00 | Using where; Using filesort |
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
1 row in set, 1 warning (0.00 sec)
ためしのEXPEXPLAIN
key がなくて大量の行の読み込み
INDEX を貼る
mysql> ALTER TABLE comments ADD INDEX post_id_idx(post_id);
Query OK, 0 rows affected (0.54 sec)
Records: 0 Duplicates: 0 Warnings: 0
EXPLAIN
おー劇的な変化
チューニングの成果確認
ログの削除
sudo rm /var/log/mysql/mysql-slow.log
sudo mysqladmin flush-logs
これをしないと結果が混ざってしまうらしい
ベンチマーカーインスタンスより実行
結果
{"pass":true,"score":12093,"success":10232,"fail":0,"messages":[]}
1016→12093
初手感想
ほー、こんなかんじでやっていくのか
スコアが劇的に上がったり、すごく読み込みが早くなるのはいいね
基本的には計測→実行→計測 というような流れか
今日はここまででインスタンスを終了しておく

インスタンスの停止

再起動
IPやセキュリティグループなどの再設定
IPが変わっているのでセキュリティグループなどを再設定しました
また私は環境を変えたのでSSHなどのセキュリティグループの許可アドレスも追加しました
SGのdescription は日本語でかけなかった

Go で起動するように修正
起動する実装をGoに切り替えるには、以下の操作を行います。
$ sudo systemctl stop isu-ruby
$ sudo systemctl disable isu-ruby
$ sudo systemctl start isu-go
$ sudo systemctl enable isu-go
https://github.com/catatsuy/private-isu/blob/master/manual.md#goへの切り替え方 に書いてある

アクセスログを集計する
そもそもなぜアクセスログを集計する手続きが必要なのか?
各URLのレスポンスタイムはWeb サービスの性能を測る重要な指標。
ボトルネックの発見のため、どのURIにアクセスが多いのか、処理に時間がかかっているURIや返却するサイズが大きいURIはどこか、などを把握して解消に繋げられるため。
ISUCONの基本はボトルネックの発見→解消→効果測定のサイクルだと思っている。その発見のプロセス。
nginx のログの形式
combined という形
172.31.18.32 - - [24/Nov/2024:00:01:21 +0900] "GET /image/10242.jpg HTTP/1.1" 200 106929 "-" "benchmarker"
この形式はWeb サービスが出力するログの形式としては一般的だが、処理や人間の目で見るのには不便→JSON形式で出るようにしよう!
アクセスログをJSON 形式で出力する
EC2 インスタンスで行っている場合はココ
sudo vim /etc/nginx/nginx.conf
http {} 内のLogging Settings のコメントの下に追加
##
# Logging Settings
##
### 追加 ####
# ログファイルのフォーマットをjson形式に設定
log_format json escape=json '{'
'"time":"$time_iso8601",'
'"host":"$remote_addr",'
'"port":"$remote_port",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":"$status",'
'"body_bytes":"$body_bytes_sent",'
'"referer":"$http_referer",'
'"ua":"$http_user_agent",'
'"request_time":"$request_time",'
'"respons_time":"$upstream_response_time"'
'}';
# access.logにjson形式で出力
access_log /var/log/nginx/access.log json;
##############
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
設定ファイルに異常がないか確認する。OK!
nginx のリロード
sudo systemctl reload nginx

既存のアクセスログはローテート
sudo mv /var/log/nginx/access.log /var/log/nginx/access.log.old
sudo systemctl restart nginx

alp のインストール
システムのアーキテクチャ確認
uname -m
インストール
適切なalpバイナリをダウンロード
• x86_64 の場合: alp_linux_amd64
• aarch64 の場合: alp_linux_arm64
sudo wget https://github.com/tkuchiki/alp/releases/download/v1.0.21/alp_linux_amd64.zip
sudo unzip alp_linux_amd64.zip
sudo install ./alp /usr/local/bin
確認
$ alp --version
1.0.21

手元からabコマンドを用いてベンチマーク
ab -c 1 -t 30 http://XXX/
$ cat /var/log/nginx/access.log
cat: /var/log/nginx/access.log: No such file or directory
あれ、アクセスログがないぞ。
どこかでなにか間違えたか
→
sudo systemctl reload nginx
を忘れていた再度実行してJSONログを確認
sudo cat /var/log/nginx/access.log
:
{"time":"2024-11-24T00:51:01+09:00","host":"175.177.46.114","port":"52080","method":"GET","uri":"/image/10261.png","status":"200","body_bytes":"882711","referer":"http://35.74.1.249/posts/10261","ua":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36","request_time":"0.170","respons_time":"0.002"}
OK
alp で確認
sudo cat /var/log/nginx/access.log | alp json
こんなかんじなのねー。

見やすい形式
more /var/log/nginx/access.log | alp json --sort sum -r -m "/posts/[0-9]+,/@\w+" -o count,method,uri,min,avg,max,sum

アクセスログのローテション
ローテート…アクセスログをファイルシステム上で別名に変更すること
アクセスログの保存場所は同じ場所であるので複数回の試行結果を同じファイルにまとめてしまうと適切に処理することができない。
ローテーションのスクリプトを作成
cat << 'EOF' > rotate.sh
#!/bin/sh
mv /var/log/nginx/access.log /var/log/nginx/access.log.$(date +%Y%m%d-%H%M%S)
nginx -s reopen
EOF
chmod +x rotate.sh
これを都度実行する
bash rotate.sh
コマンド化しても良いかもしれない
自動でやって欲しいけどなー

ここまでやってみた感想
データ(計測結果)を起点にリーンに開発していく印象
ただしデータの計測方法が最初の状態では整っていない部分があるのでそこの地盤固めを最初にする必要がある

スロークエリを解析するツールを導入する
install
pt-query-digest
mkdir ~/tools
cd ~/tools
wget https://github.com/percona/percona-toolkit/archive/refs/tags/v3.6.0.tar.gz
tar zxvf v3.6.0.tar.gz
./percona-toolkit-3.6.0/bin/pt-query-digest --version
sudo install ./percona-toolkit-3.6.0/bin/pt-query-digest /usr/local/bin
pt-query-digest --version
実行
sudo pt-query-digest /var/log/mysql/mysql-slow.log
最初、まあまあ時間かかるとおもったらパスが違っただけだった
こんな感じで出るけどちょっとわかりにくい
ファイル出力しておく
sudo pt-query-digest /var/log/mysql/mysql-slow.log | tee digest_$(date +%Y%m%d%H%M).txt

ファイルの見方
「全体的な統計」
「ランキング」
「各クエリの詳細」
パフォーマンスチューニングの文脈で、まず注目するのがランキング
この結果は負荷与える前だったのでSHOW DATABASES が1位だった

負荷後の結果
とんでもなく時間がかかっているクエリを発見

## クエリの特定
このクエリ。
どう処理していくか。
概要を見ていく
このクエリは1302回実行されて、合計で39秒かかっている。
Lock time は他のスレッドの影響を受けている可能性はない。
1249万行返すために、2498万行の処理をしている。
とにかく実行回数が多い
post テーブルについて調査してみる
mysql> SHOW CREATE TABLE posts;
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| posts | CREATE TABLE `posts` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`mime` varchar(64) NOT NULL,
`imgdata` mediumblob NOT NULL,
`body` text NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10383 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
問題なし
これがN+1問題のヒントだと気がつく
DB設計的に問題がなさそうに見えるが、実行数が多くスロー判定されるクエリはアプリケーション側で悪い設計になっている可能性がある。
実際のコード部分を見てみる
func getIndex(w http.ResponseWriter, r *http.Request) {
me := getSessionUser(r)
results := []Post{}
err := db.Select(&results, "SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC")
if err != nil {
log.Print(err)
return
}
posts, err := makePosts(results, getCSRFToken(r), false)
:
これが該当の箇所。
アプリケーション側からアプローチする場合もあれば、スロークエリからアプローチする場合もある。
どちらにせよ計測が必要
どう直す?
本では以下の3点が紹介されていた
- キャッシュを使う方法
- 別クエリでプリロードする方法
- SQLクエリのJOINを使う方法
どの方法が最適化かは、web サービスの運用の状況によって異なる

vscode ssh 接続
config の設定
Host private-isucon
HostName xxx.ap-northeast-1.compute.amazonaws.com
IdentityFile /Users/XXX/private-isu-key.pem
User ubuntu

やべVSCODEから編集するための方法がわからん
SSHのときにsu で入れたら良いんだけど

N+1の解消するように修正
err := db.Select(&results,
"SELECT posts.id, posts.user_id, posts.body, posts.mime, posts.created_at FROM posts"+
" JOIN users ON users.id = posts.user_id"+
" WHERE users.del_flg = 0"+
" ORDER BY posts.created_at DESC LIMIT 20")
再起動
sudo systemctl reload nginx
計測
36766→