Y

ISUCON12予選の復習をしました 6

この記事は何か?

ISUCON12予選の復習記録です。

第6回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「6 adminDB visit_historyの初期データをコンパクトにする」を試します。

目次

  1. 「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に対して最も古い参照履歴レコードだけを利用していることがわかります。

■ 参照

□ 参照エンドポイント

// テナント管理者向け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でよくみる問題だと思います。このあたりもコードをささっと理解できる必要がありそうです。

参考

1 ISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)