ISUCON12予選の復習をしました 6
この記事は何か?
ISUCON12予選の復習記録です。
第6回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「6 adminDB visit_historyの初期データをコンパクトにする」を試します。
目次
- 「6 adminDB visit_historyの初期データをコンパクトにする」を試す
MySQLに入っているvisit_historyデータは100テナントで322万行の巨大データですが、「アクセスをしたかどうか」だけがわかれば大丈夫というアプリケーションの作りになっているため、全件を保持しておく必要はありません。
ISUCON12予選のデータソースは2つあります。AdminDBとTenantDBです。AdminDBは以下のとおり3つのテーブルで構成されており、visit_historyテーブルに300万件のデータがあるためこのテーブルを対策する必要があることが分かります。
- AdminDB初期データ件数
mysql> select count(*) from id_generator; +----------+ | count(*) | +----------+ | 1 | +----------+ 1 row in set (0.00 sec) mysql> select count(*) from tenant; +----------+ | count(*) | +----------+ | 106 | +----------+ 1 row in set (0.00 sec) mysql> select count(*) from visit_history; +----------+ | count(*) | +----------+ | 3225470 | +----------+ 1 row in set (1.33 sec)
以下のとおり、visit_historyへの参照は1つの関数から呼び出されています。SQLを確認すると「MIN(created_at)」とあり、プレイヤーのテナントID&大会IDに対して最も古い参照履歴レコードだけを利用していることがわかります。
- visit_history
■ 参照 □ 参照エンドポイント // テナント管理者向けAPI // GET /api/organizer/billing // テナント内の課金レポートを取得する func billingHandler(c echo.Context) error { // SaaS管理者用API // テナントごとの課金レポートを最大10件、テナントのid降順で取得する // GET /api/admin/tenants/billing // URL引数beforeを指定した場合、指定した値よりもidが小さいテナントの課金レポートを取得する func tenantsBillingHandler(c echo.Context) error { □ 参照エンドポイントから呼ばれるSQL // 大会ごとの課金レポートを計算する func billingReportByCompetition(ctx context.Context, tenantDB dbOrTx, tenantID int64, competitonID string) (*BillingReport, error) { "SELECT player_id, MIN(created_at) AS min_created_at FROM visit_history WHERE tenant_id = ? AND competition_id = ? GROUP BY player_id", ■ 更新 □ 更新エンドポイント // 参加者向けAPI // GET /api/player/competition/:competition_id/ranking // 大会ごとのランキングを取得する func competitionRankingHandler(c echo.Context) error { □ 更新エンドポイントから呼ばれるSQL "INSERT INTO visit_history (player_id, tenant_id, competition_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
- 不要なデータを削除する
解説のとおり不要データを削除します。結果、300万件から20万件に削減できました。
mysql> CREATE TABLE `visit_history_tmp` ( -> `player_id` VARCHAR(255) NOT NULL, -> `tenant_id` BIGINT UNSIGNED NOT NULL, -> `competition_id` VARCHAR(255) NOT NULL, -> `created_at` BIGINT NOT NULL, -> `updated_at` BIGINT NOT NULL, -> INDEX `tenant_id_idx` (`tenant_id`) -> ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4; Query OK, 0 rows affected (0.04 sec) mysql> insert into visit_history_tmp select * from visit_history; Query OK, 3226340 rows affected (24.40 sec) Records: 3226340 Duplicates: 0 Warnings: 0 mysql> insert into visit_history select player_id, tenant_id, competition_id, min(created_at) as created_at, min(updated_at) as updated_at from visit_history_tmp group by player_id, tenant_id, competition_id; Query OK, 201131 rows affected (27.66 sec) Records: 201131 Duplicates: 0 Warnings: 0 mysql> select count(*) from visit_history; +----------+ | count(*) | +----------+ | 201131 | +----------+ 1 row in set (0.02 sec)
- 変更前のスコア
22:36:19.017277 SCORE: 6390 (+6390 0(0%))
- 変更後のスコア
01:57:29.017960 SCORE: 5617 (+5617 0(0%))
- 変更後のtopコマンド結果
DBのCPU負荷が少なくなっていることが分かります。
感想
不要な初期データの削減はISUCONでよくみる問題だと思います。このあたりもコードをささっと理解できる必要がありそうです。