ISUCON12 予選通過しました

isucon.net

ISUCON12に「第7西東京市」で参加し、最終スコア86470 (1位!) で予選を通過しました このチームで参加するのは7回目らしく、そろそろ常連チームを名乗ってもよいかもしれない

ISUCONの知名度は近年めちゃくちゃ上がっており、なんと3回募集がある参加枠のうち2回は開始3分以内、3回目も20分経たず埋まるという大人気ぶりです。人気アーティストのチケット争奪戦かよ

以下、詳細です

日時

  • 2022/07/23 10:00 - 18:00

メンバー

  • whywaita(インフラ全般)
  • icchy(応援係)
  • nomeaning(アプリ全般)

本当は3人集まって参加する予定だったのですが、急遽whywaitaが来れなくなったのでicchy, nomeaningのみオフィスで集まり、whywaitaはDiscordでリモート参加という形になりました。 会議室を提供してくれた弊社に感謝

基本方針

  • Golang
  • ベンチマーカーをデバッガー代わりに使う(ローカル環境構築は行わない)
  • 終了約1時間前(17:00以降)は破壊的な変更を行わず、再起動試験に徹する

主な感想

  • ポータル・ベンチマーカー共にノートラブルだった
  • 問題紹介動画のクオリティがすごい

レポジトリ

最終的なコード

github.com

タイムライン

※ commit logとSlackの発言をベースにしています

時間 できごと スコア
10:00 CloudFormationでVM作成開始
当日マニュアル精読開始
初期ベンチ計測
2598
10:10 VM作成完了
初期のコードをpush
コードリーディング開始
ボトルネック計測環境の準備
-
11:00 コードを大体読み終わる
SQLite3をMySQLに載せ替えるのを決意
一行ずつだとクソ遅いので変換スクリプトを書き始める
-
11:05 billingReportByCompetitionを大会終了時のみ計算するように変更 3602
11:40 SQLite3変換スクリプト書き終わる
アプリケーション側のMySQL対応
3338
12:18 playersAddHandlerをbulk insertにする 2905
12:30 SQLite3から移植してきたテナントデータが不完全であることが判明 -
12:35 変換スクリプトのバグに気づく(後述) -
12:40 正しく変換すると /initialize が間に合わないことに気づく 0
~13:30 bulk insert→ダメ
mysqldump→ダメ
LOAD DATA INFILE→ダメ
テーブルごとバックアップしておいてSELECT INSERT→ダメ
で頭を抱える、MySQL移行作業撤退も検討
0
~14:50 虚無の時間
SQLite3に戻しながらちょくちょく計測
この辺りで予選落ちも覚悟
3000~4000
14:50 /initialize が呼ばれる度に毎回/var/lib/mysqlをリストアすることを思いつく
competitionRankingHandlerの1+N解消
/initialize が通る
6923
15:20 playerHandlerの1+N解消 7025
15:25 competitionScoreHandlerをbulk insertにする 7972
15:29 flockを消す 8662
15:31 MySQLのコネクション数を50まで引き上げる 9506
15:35 competitionScoreHandlerの1+N解消 11346
15:38 competitionScoreHandlerでdead lockが発生していたので修正 23121
15:45 App+DBの2台構成に変更 45857
15:57 dispenseIDを高速化
ユーザーが異常に増えたことによりスコアは下がる
20212
~17:00 虚無の時間2 -
17:10 player_scoreで最新の一件のみに注目するように修正
これが最後の破壊的な変更
66960
17:19 SQLLoggerとechoのDEBUGログを無効化 76721
17:19~17:29 再起動試験 77990~81167
17:31 デフォルトのencoding/jsonは遅いのでgo-jsonを使うように修正 86872
17:33 マニュアルを再度読む -
17:36 dispenseIDの高速化をもう一度試す
(ここでulidをそのまま使うと文字種がhexadecimal ([0-9a-f]) からBase32 ([A-Z2-7=]) に変わるため、レギュレーションに抵触するのではないかと思い"%x"に修正)
やはりスコアが下がるのでLastInsertIdで行くことを決定
80859
17:38~ ひたすらベンチを回し続ける 85941~86999
17:53 86470が出たところでフィニッシュ 86470

15時以降の伸びがすごいですね。

その他

SQLite3→MySQL変換スクリプトのバグ

#!/usr/bin/env python3
import sqlite3


values_competition = []
values_player = []
values_player_score = []


for i in (1, 100):
    conn = sqlite3.connect(f'file:{i}.db?mode=ro', uri=True)
    cur = conn.cursor()

    for r in cur.execute('select * from competition'):
        values_competition.append(r)
    for r in cur.execute('select * from player'):
        values_player.append(r)
    for r in cur.execute('select * from player_score'):
        values_player_score.append(r)

    conn.close()


sql = """
DROP TABLE IF EXISTS competition;
DROP TABLE IF EXISTS player;
DROP TABLE IF EXISTS player_score;
CREATE TABLE IF NOT EXISTS competition (
  id VARCHAR(255) NOT NULL PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  title TEXT NOT NULL,
  finished_at BIGINT NULL,
  created_at BIGINT NOT NULL,
  updated_at BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS player (
  id VARCHAR(255) NOT NULL PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  display_name TEXT NOT NULL,
  is_disqualified BOOLEAN NOT NULL,
  created_at BIGINT NOT NULL,
  updated_at BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS player_score (
  id VARCHAR(255) NOT NULL PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  player_id VARCHAR(255) NOT NULL,
  competition_id VARCHAR(255) NOT NULL,
  score BIGINT NOT NULL,
  row_num BIGINT NOT NULL,
  created_at BIGINT NOT NULL,
  updated_at BIGINT NOT NULL
);
"""[1:]

CHUNK_SIZE = 100000

for i in range(0, len(values_competition), CHUNK_SIZE):
    sql += "INSERT INTO competition VALUES " + ', '.join(f"{d}".replace('None', 'NULL') for d in values_competition[i:i+CHUNK_SIZE]) + ';' + '\n'

for i in range(0, len(values_player), CHUNK_SIZE):
    sql += "INSERT INTO player VALUES " + ', '.join(f"{d}".replace('None', 'NULL') for d in values_player[i:i+CHUNK_SIZE]) + ';' + '\n'

for i in range(0, len(values_player_score), CHUNK_SIZE):
    sql += "INSERT INTO player_score VALUES " + ', '.join(f"{d}".replace('None', 'NULL') for d in values_player_score[i:i+CHUNK_SIZE]) + ';' + '\n'

print(sql)

このスクリプトは正しく動きません。なぜでしょうか?

答え

...

for i in (1, 100):
    conn = sqlite3.connect(f'file:{i}.db?mode=ro', uri=True)

...

1.dbと100.dbしか読まないため

for i in (1, 100+1):

って書いたときに101.dbが見つからないよと言われた時点で気づくべきだったね

最後に

  • 運営の皆さん本当にありがとうございました。過去最高に快適な予選だったと思います
    • ベンチマーカーもポータルもトラブルなくて本当にすごい
  • 実は今回で5回連続予選通過しているらしい
    • ありがとうwhywaita、ありがとうnomeaning
    • 本選は3人で集まって参加したい
  • 本選に参加される方よろしくお願いします