isucon本

流石にログを残しながらやった方が良さげと思ったので残しながら。
探偵ごっこみたいで面白い。

3. 基礎的な負荷試験
まず、30秒リクエスト負荷を与え続けると、
ab -c 1 -t 30 http://localhost/
docker stats
で各コンテナのCPU使用率を見たところ、、
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
a7aba7142c98 private-isu-nginx-1 0.09% 7.559MiB / 7.654GiB 0.10% 629kB / 631kB 0B / 4.1kB 9
88267faceb9b private-isu-app-1 4.09% 22.17MiB / 1GiB 2.16% 21.1MB / 1.29MB 0B / 0B 10
7dd9c0346cab private-isu-mysql-1 96.68% 455.5MiB / 1GiB 44.48% 676kB / 21.1MB 44.4MB / 16.7MB 36
e4fa51d6d1fc private-isu-memcached-1 0.01% 2.152MiB / 7.654GiB 0.03% 1.63kB / 126B 0B / 0B 10
ダントツでmysqlコンテナがすごいことに。
なので、mysqlあたりを調査すると。
その際に、スロークエリを吐かせるようにした。
[mysqld]
slow_query_log=1
slow_query_log_file=/var/log/mysql/mysql-slow.log
long_query_time=0
このcnfファイルやログファイルをコンテナにマウントするようにして、再度実行。
すると、以下のようなログが大量に
/usr/sbin/mysqld, Version: 8.4.6 (MySQL Community Server - GPL). started with:
Tcp port: 3306 Unix socket: /var/lib/mysql/mysql.sock
Time Id Command Argument
# Time: 2025-09-13T10:23:26.418087Z
# User@Host: root[root] @ private-isu-app-1.private-isu_my_network [172.20.0.4] Id: 8
# Query_time: 0.000233 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0
use isuconp;
SET timestamp=1757759006;
SET NAMES utf8mb4;
# Time: 2025-09-13T10:23:26.471272Z
# User@Host: root[root] @ private-isu-app-1.private-isu_my_network [172.20.0.4] Id: 8
# Query_time: 0.052447 Lock_time: 0.000014 Rows_sent: 10001 Rows_examined: 20002
SET timestamp=1757759006;
SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC;
# Time: 2025-09-13T10:23:26.487160Z
# User@Host: root[root] @ private-isu-app-1.private-isu_my_network [172.20.0.4] Id: 8
# Query_time: 0.001284 Lock_time: 0.000006 Rows_sent: 0 Rows_examined: 0
SET timestamp=1757759006;
# administrator command: Prepare;
# Time: 2025-09-13T10:23:26.508029Z
# User@Host: root[root] @ private-isu-app-1.private-isu_my_network [172.20.0.4] Id: 8
# Query_time: 0.020730 Lock_time: 0.000001 Rows_sent: 1 Rows_examined: 100002
SET timestamp=1757759006;
SELECT COUNT(*) AS `count` FROM `comments` WHERE `pos
で、これだと辛いので、ホストマシンからmysqldumpslowを実行し、実行時間が長いクエリを並べてもらう。
# マウントされたホストマシンのログに対して実行
mysqldumpslow webapp/logs/mysql/mysql-slow.log
Reading mysql slow query log from webapp/logs/mysql/mysql-slow.log
Can't open webapp/logs/mysql/mysql-slow.log: No such file or directory at /opt/homebrew/bin/mysqldumpslow line 93.
yutakakiki@kakinokiyuunoMacBook-Air webapp % mysqldumpslow /logs/mysql/mysql-slow.log
Reading mysql slow query log from /logs/mysql/mysql-slow.log
Can't open /logs/mysql/mysql-slow.log: No such file or directory at /opt/homebrew/bin/mysqldumpslow line 93.
yutakakiki@kakinokiyuunoMacBook-Air webapp % mysqldumpslow logs/mysql/mysql-slow.log
Reading mysql slow query log from logs/mysql/mysql-slow.log
Count: 836 Time=0.02s (20s) Lock=0.00s (0s) Rows=3.0 (2470), root[root]@private-isu-app-1.private-isu_my_network
SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N
Count: 38 Time=0.01s (0s) Lock=0.00s (0s) Rows=10001.0 (380038), root[root]@private-isu-app-1.private-isu_my_network
SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC
Count: 836 Time=0.01s (7s) Lock=0.00s (0s) Rows=1.0 (836), root[root]@private-isu-app-1.private-isu_my_network
SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = N
Count: 1 Time=0.00s (0s) Lock=0.00s (0s) Rows=0.0 (0), root[root]@private-isu-app-1.private-isu_my_network
SET NAMES utf8mb4

が、いったんのこれまでのログ。

生のスロークエリを見ると、そのクエリはこうなっていた。
100005行探索して、返しているのが3行。これは苦しい。
# administrator command: Prepare;
# Time: 2025-09-13T10:23:26.569123Z
# User@Host: root[root] @ private-isu-app-1.private-isu_my_network [172.20.0.4] Id: 8
# Query_time: 0.024489 Lock_time: 0.000001 Rows_sent: 3 Rows_examined: 100005 # ここ
SET timestamp=1757759006;
SELECT * FROM `comments` WHERE `post_id` = 10000 ORDER BY `created_at` DESC LIMIT 3;

テーブルのスキーマを見る。
docker exec -it private-isu-mysql-1 bash
でターミナルに入り、
mysql -u root -p
でmysqlに入る。
descで見たスキーマ
mysql> desc comments
-> ;
+------------+-----------+------+-----+-------------------+-------------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------+------+-----+-------------------+-------------------+
| id | int | NO | PRI | NULL | auto_increment |
| post_id | int | NO | | NULL | |
| user_id | int | NO | | NULL | |
| comment | text | NO | | NULL | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
+------------+-----------+------+-----+-------------------+-------------------+
5 rows in set (0.00 sec)

PRIMARY KEYにはINDEXが貼られる。
しかし、今回whereで検索かけてるpost_idにはindexingされていない。
これだと、全レコード調べる必要が出てきてしまうy

EXPLAINでクエリ実行計画を確認。
key == null、これはindexingされていない
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 | 98217 | 10.00 | Using where; Using filesort |
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+

type == ALL
これは、indexなしで全検索してる。

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 | ref | post_id_idx | post_id_idx | 4 | const | 6 | 100.00 | Using filesort |
+----+-------------+----------+------------+------+---------------+-------------+---------+-------+------+----------+----------------+

実行結果は、
# Time: 2025-09-14T01:34:44.892650Z
# User@Host: root[root] @ localhost [] Id: 12
# Query_time: 0.001142 Lock_time: 0.000008 Rows_sent: 3 Rows_examined: 9
SET timestamp=1757813684;
select * from comments where post_id = 9995 order by created_at desc limit 3;
explainはあくまで実行計画なので、実際と若干ズレる。

indexを改善したところで、再度負荷をかけると
docker stats
では、
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
a7aba7142c98 private-isu-nginx-1 0.81% 7.629MiB / 7.654GiB 0.10% 10.9MB / 10.9MB 0B / 4.1kB 9
88267faceb9b private-isu-app-1 41.72% 21.94MiB / 1GiB 2.14% 349MB / 22.5MB 36.9kB / 0B 10
7dd9c0346cab private-isu-mysql-1 49.74% 489.9MiB / 1GiB 47.84% 11.9MB / 348MB 45.4MB / 26.7MB 37
e4fa51d6d1fc private-isu-memcached-1 0.01% 2.152MiB / 7.654GiB 0.03% 1.71kB / 126B 0B / 0B 10
mysqlの割合は減っている。
ただ、appが結構占めているのがわかる。
また、abコマンドの結果
Requests per second: 18.75 [#/sec] (mean)
Time per request: 53.335 [ms] (mean)
あちゃ、当初のrpsとレイテンシを記録し忘れている。たぶん良くなっただろう。

appが結構占めているのがわかるようになったのは、mysqlのボトルネックが1つ解消され、rpsが増えたことで、よりwebappの稼働が増えたから

mysqldumpslowは以下
Count: 563 Time=0.01s (4s) Lock=0.00s (0s) Rows=10001.0 (5630563), root[root]@private-isu-app-1.private-isu_my_network
SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC
Count: 12386 Time=0.00s (0s) Lock=0.00s (0s) Rows=3.0 (36595), root[root]@private-isu-app-1.private-isu_my_network
SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N
Count: 12386 Time=0.00s (0s) Lock=0.00s (0s) Rows=1.0 (12386), root[root]@private-isu-app-1.private-isu_my_network
SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = N
Count: 48981 Time=0.00s (1s) Lock=0.00s (0s) Rows=1.0 (48981), root[root]@private-isu-app-1.private-isu_my_network
SELECT * FROM `users` WHERE `id` = N
Count: 147498 Time=0.00s (2s) Lock=0.00s (0s) Rows=0.7 (97952), root[root]@2hosts
#
Count: 73743 Time=0.00s (0s) Lock=0.00s (0s) Rows=0.0 (0), 0users@0hosts
administrator command: Close stmt
Count: 73753 Time=0.00s (0s) Lock=0.00s (0s) Rows=0.0 (0), 0users@0hosts
administrator command: Prepare
Count: 2 Time=0.00s (0s) Lock=0.00s (0s) Rows=0.0 (0), 0users@0hosts
administrator command: Quit

上のmysqldumpslowを見ると、commentsテーブルではなく、postsテーブルに対するクエリが上に来ている。
Count: 563 Time=0.01s (4s) Lock=0.00s (0s) Rows=10001.0 (5630563), root[root]@private-isu-app-1.private-isu_my_network
SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC
しかし依然として前のクエリが大量発行されているのも気になる、
Count: 12386 Time=0.00s (0s) Lock=0.00s (0s) Rows=3.0 (36595), root[root]@private-isu-app-1.private-isu_my_network
SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N
30sに12386ということは、1sに412回叩かれている。
abコマンドによれば 18.75rpsだから、412/18で、1リクエストあたり22回クエリを叩いていることになる。
これはN+1の予感?

並列度を上げて負荷をかける。
2並列でabコマンドすると以下の通り、rpsは上がり、レイテンシが増えた。
レイテンシが20msくらい増えている。
Requests per second: 27.25 [#/sec] (mean)
Time per request: 73.403 [ms] (mean)
4並列にしてみると、rpsは変わらんがレイテンシがかなり上がっている。
Requests per second: 29.30 [#/sec] (mean)
Time per request: 136.537 [ms] (mean)
サーバーの処理能力が飽和している(捌けるreq数が限度きた)から、並列度が上がる分レイテンシーが上がっているということ。

ちゃんとリソース(CPU)は使い切ってそれなのか?

でdocker statsで見ると、40パーは使ってないのがわかる。
88267faceb9b private-isu-app-1 64.29% 48.68MiB / 1GiB 4.75% 2.96GB / 192MB 36.9kB / 0B 11
これは多分、mysqlとかI/O通信でCPUの遊びが一部あることが原因かと思われれる。

rubyではCPU数より多くworkerプロセスを立てれば、IO待ちで遊んでる時にCPUが暇しないみたいなことが書いてあった。Goはこれをgoroutineで解決する感じ?
copilot >
はい、その理解で正しいです。
Ruby(特にPumaやUnicornなどのWebサーバー)は、CPUコア数より多くのワーカープロセスを立てることで、I/O待ちの間も他のプロセスがCPUを使えるようにしています。
これはRubyのスレッドやプロセスがI/O待ちでブロックされやすい(GILやVMの制約)ためです。
一方、Goはgoroutineという軽量スレッドを大量に使い、I/O待ちの間も他のgoroutineが同じプロセス内でどんどんCPUを使えるようになっています。
Goランタイムが自動的にI/O待ちのgoroutineをスケジューリングし、CPUを無駄なく使う設計です。
まとめ:
Ruby:プロセスやスレッドを多重化してI/O待ちをカバー
Go:goroutineとランタイムのスケジューリングでI/O待ちをカバー(プロセスを増やさなくてもOK)
Goは「1プロセス+大量goroutine」でI/OバウンドもCPUバウンドも効率よく処理できるのが強みです。

3. シナリオを使った負荷試験
k6でe2e的にシナリオをjsで書いて負荷をかけれる。

yutakakiki@kakinokiyuunoMacBook-Air private-isu % cat webapp/logs/nginx/access.log | alp json --sort=sum -r -m "/posts/[0-9]+,/@\w"
+-------+-----+-----+-----+-----+-----+--------+---------------+-------+-------+--------+-------+-------+-------+-------+--------+-----------+-----------+--------------+-----------+
| COUNT | 1XX | 2XX | 3XX | 4XX | 5XX | METHOD | URI | MIN | MAX | SUM | AVG | P90 | P95 | P99 | STDDEV | MIN(BODY) | MAX(BODY) | SUM(BODY) | AVG(BODY) |
+-------+-----+-----+-----+-----+-----+--------+---------------+-------+-------+--------+-------+-------+-------+-------+--------+-----------+-----------+--------------+-----------+
| 414 | 0 | 414 | 0 | 0 | 0 | GET | /posts/[0-9]+ | 0.007 | 0.644 | 66.525 | 0.161 | 0.358 | 0.397 | 0.461 | 0.148 | 1661.000 | 78814.000 | 11979591.000 | 28936.210 |
| 414 | 0 | 414 | 0 | 0 | 0 | GET | / | 0.063 | 0.325 | 52.663 | 0.127 | 0.180 | 0.211 | 0.251 | 0.042 | 21836.000 | 21864.000 | 9047220.000 | 21853.188 |
| 183 | 0 | 183 | 0 | 0 | 0 | GET | /@\w | 0.085 | 0.273 | 26.113 | 0.143 | 0.196 | 0.219 | 0.250 | 0.038 | 33838.000 | 33847.000 | 6193254.000 | 33842.918 |
| 414 | 0 | 0 | 414 | 0 | 0 | POST | /login | 0.010 | 0.186 | 18.175 | 0.044 | 0.094 | 0.109 | 0.130 | 0.031 | 0.000 | 0.000 | 0.000 | 0.000 |
| 231 | 0 | 0 | 231 | 0 | 0 | POST | / | 0.006 | 0.171 | 9.831 | 0.043 | 0.084 | 0.094 | 0.134 | 0.031 | 0.000 | 0.000 | 0.000 | 0.000 |
| 183 | 0 | 0 | 183 | 0 | 0 | POST | /comment | 0.005 | 0.139 | 5.547 | 0.030 | 0.077 | 0.091 | 0.123 | 0.027 | 0.000 | 0.000 | 0.000 | 0.000 |
+-------+-----+-----+-----+-----+-----+--------+---------------+-------+-------+--------+-------+-------+-------+-------+--------+-----------+-----------+--------------+-----------+

そういえばalpのカラムまとめ
カラム名 | 意味 |
---|---|
count | 集計対象となったリクエストの件数 |
1xx, 2xx, 3xx, 4xx, 5xx | HTTPステータスコードごとの件数(例: 2xx = 成功、4xx = クライアントエラー) |
method | HTTPメソッド(GET, POST, PUT, DELETE など) |
uri | リクエスト先の URI(パス)。alp ではクエリパラメータ部分を正規化可能 |
min | 該当リクエストの処理時間の最小値 |
max | 該当リクエストの処理時間の最大値 |
sum | 該当リクエストの処理時間の合計 |
avg | 該当リクエストの処理時間の平均 |
p99 / p95 / p90 | 処理時間のパーセンタイル値(遅い方のボトルネック把握に使う) |
stddev | 処理時間の標準偏差(処理時間のばらつき具合) |

5. データベースのチューニング
先のintegration.jsをk6でrunし、docker statsで見てみる。
appも結構だけど、第一にボトルネックはDBぽい
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
df2096f936fc private-isu-nginx-1 2.98% 7.668MiB / 7.654GiB 0.10% 36.5MB / 36.1MB 0B / 4.1kB 9
eca7327833b8 private-isu-app-1 81.30% 56.91MiB / 1GiB 5.56% 639MB / 59.9MB 0B / 0B 13
8eb147bd6c99 private-isu-mysql-1 102.04% 505.1MiB / 1GiB 49.33% 37.6MB / 624MB 64.7MB / 96.7MB 42
ccef14907104 private-isu-memcached-1 0.58% 3.75MiB / 7.654GiB 0.05% 417kB / 651kB 0B / 0B 10

mysqlのプロセスlistをみる
mysql> show full processlist;
+-----+-----------------+------------------------------------------------+---------+---------+------+------------------------+-------------------------------------------------------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+-----+-----------------+------------------------------------------------+---------+---------+------+------------------------+-------------------------------------------------------------------------------------+
| 5 | event_scheduler | localhost | NULL | Daemon | 665 | Waiting on empty queue | NULL |
| 471 | root | localhost | NULL | Query | 0 | init | show full processlist |
| 756 | root | private-isu-app-1.private-isu_my_network:60496 | isuconp | Execute | 0 | logging slow query | SELECT * FROM `comments` WHERE `post_id` = 11785 ORDER BY `created_at` DESC LIMIT 3 |
| 759 | root | private-isu-app-1.private-isu_my_network:60530 | isuconp | Prepare | 0 | logging slow query | NULL |
| 764 | root | private-isu-app-1.private-isu_my_network:60570 | isuconp | Execute | 0 | logging slow query | SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = 11769 |
| 765 | root | private-isu-app-1.private-isu_my_network:60586 | isuconp | Prepare | 0 | logging slow query | NULL |
| 766 | root | private-isu-app-1.private-isu_my_network:60598 | isuconp | Execute | 0 | logging slow query | SELECT * FROM `users` WHERE `id` = 999 |
| 768 | root | private-isu-app-1.private-isu_my_network:60622 | isuconp | Execute | 0 | logging slow query | SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = 11795 |
+-----+-----------------+------------------------------------------------+---------+---------+------+------------------------+-------------------------------------------------------------------------------------+
8 rows in set, 1 warning (0.00 sec)
8スレッドで実行されていた。

クエリランキングが。
今のとこだと、SELECT posts のクエリが最も時間が長い。
# Profile
# Rank Query ID Response time Calls R/Call V/M
# ==== =================================== ============= ===== ====== ====
# 1 0x4858CF4D8CAA743E839C127C71B69E75 11.9731 42.0% 665 0.0180 0.02 SELECT posts
# 2 0xCDEB1AFF2AE2BE51B2ED5CF03D4E749F 4.3609 15.3% 334 0.0131 0.01 SELECT comments
# 3 0xDA556F9115773A1A99AA0165670CE848 2.9153 10.2% 78579 0.0000 0.00 ADMIN PREPARE
# 4 0x396201721CD58410E070DA9421CA8C8D 1.7570 6.2% 33625 0.0001 0.01 SELECT users
# 5 0xE83DA93257C7B787C67B1B05D2469241 1.4883 5.2% 334 0.0045 0.00 SELECT posts
# 6 0x009A61E5EFBD5A5E4097914B4DBD1C07 1.4503 5.1% 331 0.0044 0.00 INSERT posts
# 7 0x624863D30DAC59FA16849282195BE09F 1.2097 4.2% 19980 0.0001 0.00 SELECT comments
# 8 0x422390B42D4DD86C7539A5F45EB76A80 1.1721 4.1% 20645 0.0001 0.00 SELECT comments
# 9 0x9F2038550F51B0A3AB05CA526E3FEDDC 0.7333 2.6% 334 0.0022 0.01 INSERT comments
# 10 0xC9383ACA6FF14C29E819735F00B6DBDF 0.6099 2.1% 334 0.0018 0.01 SELECT posts
# MISC 0xMISC

先ほどpost_idに関してindexを貼ったが、ソートもなかなか負荷のかかる処理らしい。
ExtraカラムのUsing filesortがそれ。
mysql> explain select * from comments where post_id = 100 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 | ref | post_id_idx | post_id_idx | 4 | const | 5 | 100.00 | Using filesort |
+----+-------------+----------+------------+------+---------------+-------------+---------+-------+------+----------+----------------+
ソートにも対応できるように、post_id , created_atの複合indexを作り直そう。
すると、EXPLAINも変わった。「indexを逆向きに読んだ」とある。これは、標準がascでindexが作成されてしまうからか。
mysql> explain select * from comments where post_id = 100 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 | ref | post_id_idx | post_id_idx | 4 | const | 5 | 100.00 | Backward index scan |
+----+-------------+----------+------------+------+---------------+-------------+---------+-------+------+----------+---------------------+
なら、そもそもdescで複合indexを作ろう。
ALTER TABLE `comments` DROP INDEX post_id_idx, ADD INDEX post_id_idx(`post_id`,`created_at` DESC);
するとExtraが消えた
mysql> explain select * from comments where post_id = 100 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 | ref | post_id_idx | post_id_idx | 4 | const | 5 | 100.00 | NULL |
+----+-------------+----------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

クラスターインデックスについてちょっとまとめ。
MySQLはクラスターインデックスという構造をとる。
PRIMARY INDEX :
主キーにつけられる。複合でも、それがuniqueであるならつけれる。テーブルにつき1つまで作成可能
SECONDARY INDEX:
PRIMARY INDEX以外のindexはこれ。ノードには、プライマリインデックスのキー値が含まれている。
だから、たとえばCountとかは、わざわざプライマリーインデックスを参照しなくても良かったりする。
SELECT COUNT(*) FROM comments WHERE user_id = 123;
のクエリがあったとして、user_udにセカンダリインデックスがはられていれば、プライマリーキーであるidの数はセカンダリーインデックスのツリー上で判別できる。
こういうセカンダリインデックスだけで済むのを Covering Indexというらしい。
わかりやすい図があった
これを実行
ALTER TABLE `comments` ADD INDEX `idx_user_id` (`user_id`);

インデックスの張りすぎにも注意。
- 1クエリに対して使用されるインデックスは1つである
- データの追加があったとき、インデックスは更新されるので、そこがオーバーヘッドである
必要なところだけ見極めて使う

mysqlのようなBツリーindexだと、全文検索的なのが弱い。
LIKE句だと、hoge%
のような前方一致しかインデックスが効かない。
なので文字の形態要素解析などが施された全文検索エンジンを利用したりする。ElasticSearchとか。
一応、mysqlも全文検索indexは対応している。

空間インデックスというものもあるらしい。が、難しいなよくわからん。
緯度経度を使うやつらしいが、、、
わかりやすくまとめると、geminiによれば
空間インデックスは、
1. 地理的な空間を、四角形(タイル)などの単位で階層的に分割・管理します。
2. 各データは、自分がどの「タイル」に属するかという情報を持つようになります。
3. 検索時には、まず該当する「タイル」を特定し、そのタイル内のデータだけを効率的に調べます。
これにより、地図上での「近い」「範囲内にある」といった検索が、飛躍的に高速になります。

出たーN+1について。
digestによるランキングだと、以下は、レスポンスタイムとして短いのに実行数が多くて、ランキング上位に上がっている。
4 0x396201721CD58410E070DA9421CA8C8D 1.7570 6.2% 33625 0.0001 0.01 SELECT users
クエリ自体はPRIMARY INDEXだから高速なのか。
# Query 4: 1.12k QPS, 0.06x concurrency, ID 0x396201721CD58410E070DA9421CA8C8D at byte 14103398
# Scores: V/M = 0.01
# Time range: 2025-09-14T10:16:17 to 2025-09-14T10:16:47
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count 14 33625
# Exec time 6 2s 20us 65ms 52us 76us 533us 38us
# Lock time 42 27ms 0 623us 0 1us 3us 1us
# Rows sent 0 32.84k 1 1 1 1 0 1
# Rows examine 0 32.84k 1 1 1 1 0 1
# Query size 4 1.22M 38 38 38 38 0 38
# String:
# Hosts private-isu-app-1.private-isu_my_network
# Users root
# Query_time distribution
# 1us
# 10us ################################################################
# 100us ##
# 1ms #
# 10ms #
# 100ms
# 1s
# 10s+
# Tables
# SHOW TABLE STATUS LIKE 'users'\G
# SHOW CREATE TABLE `users`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT * FROM `users` WHERE `id` = 999\G
1回のクエリで得た結果の件数(これをN件とする)に対して、関連する情報を集めるため、N回以上のクエリを実行してしまうことでアプリケーションのレスポンス速度の低下やデータベースの負荷の原因になることをN+1問題と言います。
このN+1を特定するためには、スロークエリから実行数のおおい箇所を特定するのがいい。らしい。
それで、アプリケーションコードの中でloop処理の中でクエリを発行している箇所を洗い出す必要がある。こればっかりはアプリケーションからのクエリ発行の仕方の問題だから、そっちをみるしかない。
で、特定できた後も、それがアプリケーションにとって本当にボトルネックなのかを調べる。
N+1の対処には以下のような方法がある。
- JOINで一気にとる
- キャッシュを使う
- middlewareを追加する必要
- 別クエリでpreloadする
- 複雑なSQL不要

キャッシュを使って解消する方法。
postを30件取ったとして、そこにあるuser_idからまたUser情報を引き直す、典型的なN+1があったとする。
これには、
1. 1回目はN+1を許容する
- postsレコードとる
- その件数分users引き直す
- usersをKVSにキャッシュ
2. 2回目からは、usersに関してはキャッシュからひく
しかし、DBのN+1は起きづらくなったにせよ、キャッシュのN+1は残ってる。(結局Users数分引いてるんで)
DBとしてのボトルネックは減らせるが、キャッシュサーバーとアプリケーションサーバーがそれはそれで通信が発生するのでうんまあ。
この通信に関しては、KVSはMulti keysでバルク参照が可能なので、N件分の参照の通信は減らすということは可能。

「別クエリによる」preloadを使う方法。
IN句を使うと、2回のクエリに抑えられる。
- postsを取得
- user_idsをIN句に配列で指定して一気にusersを取得
preloadについて簡潔にまとめると
preload(プリロード)は、アプリケーションが必要なデータを事前にまとめて読み込んでおくこと。
これにより、後でデータを個別に何度もデータベースに問い合わせる(N+1問題)のを防ぎ、処理を効率化できる。
IN句などを使って、関連データを一括で取得するのが典型的な例。
まとめて情報を取得する、preloadはJOINを使うやり方もあるが、INはクエリがシンプルという利点はある。
「事前に」という文脈がなんか強く聞こえるが、geminiによれば
preloadとは、アプリケーションが必要になる前に、関連データをまとめてデータベースから取得しておくことです。
アプリケーションとして必要になる「前」に、効率よく取得するってことね。

N+1はDBに限ったことではなく、先に見たキャッシュや、APIコールにも言える。
必要な情報をバルク取得できるAPIもケースバイケースでいるよね。

JOINした時に、オプティマイザが思い通りにindexを使ってくれないパターンがある。
(統計情報とか見て判断してるから)
そういう時は、FORCE INDEXを使ってあげれば使ってくれる。
こうしないと、意図せずオプティマイザが、indexを使わずにフルスキャンしてくれちゃう(?)可能性もありうる。

また、INNER JOINを使ってる場合は、STRAIGHT_JOIN も使える。
これは、クエリの順番通りにクエリを実行してくれるというもの。

不要な select * from ...
の*には気を付ける。

アプリケーションサーバーとデータベースサーバーは基本TCP通信。
だけど、このTCP通信はコストが高い。
3way handshake.....確かにそれは高くなりそうだな。
さらにTLS暗号化のパケットのやり取りも入ってくるとなると、接続時の通信はボトルネックとなりうる。
そこで、接続を永続化したい。・
goのmysqlドライバーだと
db.SetMaxOpenConns(8) # 同時接続数MAX
db.SetMaxIdleConns(8) # 同時使ってない接続MAX
永続化の文脈だと、SetMaxIdleConnsa
が大きい。
デフォルトは2なので、使ってないと2を超えると毎回接続し直す。

storage量が多かったり、read/writeのI/O負荷が高い場合は、io-waitもボトルネックとなる。
読み取りの高速化対策として、
- OSはディスクキャッシュとして、一回読んだデータはメモリにキャッシュとして格納して次回からはそっから高速に読み出す機構がある
- それとは別に、MySQLにもInnoDB Buffer Poolという同様なキャッシュ機構がある
この二つのキャッシュが二重で起こると無駄なので、mysqlのcnfファイルとかに以下を記しておくといい。
innodb_flush_method=O_DIRECT

そして、更新(write)に関して、以下は初しり。
- データベースの更新クエリ(INSERT、UPDATE、DELETEなど)は、まずディスクキャッシュ(バッファプール) に書き込まれる。
ディスクキャッシュに書き込まれた後、fsyncという機能で、ストレージに同期される。これはOSの命令。
しかし、これにはmsくらいかかったりしてまあまあ時間がかかったりしてるっぽい。
そのため、同期的なfsyncをやめて、非同期のflushに任せる手法もある。
しかし、flushは時間間隔が長い(s~10s)ため、リスキーでもある。
mysqlの設定としてinnodb_flush_log_at_trx_commit
という設定値がある。説明は以下。
1: コミットごとにディスクへ同期書き込み。最も安全だが遅い。fsyncと同義
0: 約1秒ごとにディスクへまとめて書き込み。最も速いが、直近1秒のデータが失われる可能性がある。
2: コミットごとにOSキャッシュへ書き込み、約1秒ごとにディスクへ物理書き込み。速さと安全さの中間。

また、MySQL8.0では更新ログがデフォルトで記録される。
これは、リードレプリカなどに非同期で同期するために必要不可欠。レプリケーションには必須。
だけど、ISUCONとかで冗長構成を取らないなら、
disable-log-bin = 1
として無効化してもいいかもしれない。

このレプリカDBへのログのflushも制御可能。
sync_binlog = 1: 最も安全。トランザクションコミットごとにバイナリログをディスクにfsyncします。デフォルト。
sync_binlog = 0: 最も高速。バイナリログはOSのキャッシュに書き込まれ、約1秒ごとにディスクにflushされます。
sync_binlog = N (N>1): トランザクションコミットごとにN回バッファリングされ、N回目にfsync が実行されます。1より高速で、0より安全な中間的な設定です。つまり、N回分のログへの書き込みごとにflushされる

6. リバースプロキシの利用
-
マルチプロセス・シングルスレッド
- 1リクエスト1 プロセス
- プロセスを増やせば捌けるリクエストは増えるが、リソースに限界がある
- プロセスが多すぎるとスケジューラが無駄に切り替えを行い、そのたびにコンテキストスイッチが発生してキャッシュもリセットされ、性能が下がる(C10K問題
- でもまあシンプル
-
シングルプロセス・マルチスレッド
- 同じプロセス上のメモリ空間をスレッド間で共有できるため、メモリ節約
- しかしスレッドの切り替えにはコンテキストスイッチが。
- これ解決するのは、goroutineかも。
- 要は、マシンスレッドを意識せずにアプリケーションランタイムの中の軽量スレッドを扱えるから。
- これ解決するのは、goroutineかも。
- 順序が保証されない(スケジューラが決めること)、スレッド間のメモリ共有/競合とか、考えるところは多くなる

静的ファイル等は、nginx等のproxyから配信するようにして、アプリケーションサーバーへの負荷を減らすのがいい。

C10K問題とは。
ハードウェアに問題はなくても、クライアントの同時接続数が多すぎると性能が落ちる問題。
マルチプロセスだと、クライアント接続が増えるほどスレッドが増え、メモリを食いまくるし、コンテキストスイッチのオーバーヘッドも辛い。
nginxはこの問題を解決しているらしい。

nginxは、マルチプロセス / シングルスレッドらしい。基本的には。
でもそれぞれのプロセスは並行処理を行う。
nginxの worker procesはネットワークI/Oがnon-blockingなんだって。golangと一緒やな。
プロセスは以下の感じだから、workerをうまくスケジューリングしてそう。
master:worker = 1:n
そして多重I/0もサポートしていると。
これは一気にI/Oを行って、readyなものだけ教えてくれたりする効率的なやつっぽい。
とりまnginx、効率的そうだよと言うはなし。