feature image

2022年11月10日 | ブログ記事

ISUCON12で2位になりました(織時屋)

ISUCON12本選に@eiya@oribe@tokiで「織時屋」として出場し、全体2位、学生チーム内では1位になりました。

本選リポジトリ: https://github.com/oribe1115/isucon12-final
@mazreanによる実況記事: ISUCON 12本戦 traQ内実況まとめ | 東京工業大学デジタル創作同好会traP

予選記事: ISUCON12予選を学生チーム内1位のスコアで突破しました | 東京工業大学デジタル創作同好会traP

本選ライブ用のチーム紹介画像

予選〜本選

kayac/kayac-isucon-2022を使って初見の問題を解く練習を行った。
その過程でMakefileや周辺ツールの改良(詳細)をした。
また、予選での反省を踏まえてpprofの代わりにfgprofを使うようにした。
だだ、ソースコード行ごとの実行時間が見れないのが辛かったので、準備だけしてpprofを使うで良かったかもしれない。

本選当日

10:00~ 初動 600点

サーバーの台数が5台あることに驚く。最終的にシャーディングをしたりで全サーバーを使い切ることになりそう、と推測しつつもしばらくは堅実に1台構成で進める方針で決定。

当初の予定通り分担して初動を進めた。

@oribe: 環境セットアップとリポジトリの載せ
@eiya: 計測ツールの設定(slow query log、alp、fgprof)
@toki: マニュアル読み

10:36 13,291点

user_present_all_received_historyテーブルからのSELECTがそこそこ重かったので、検索条件のuser_idpresent_all_idに複合インデックスを貼る。

11:02 16,798点

最上位のslow query: generateIDで発行されているUPDATEクエリ

id_generatorテーブルを使っての発行をやめ、アプリケーション内の変数でインクリメンタルにIDを発行するようにした。
排他制御のため、sync/atomicパッケージのatomic.AddInt64を使用して実装。
この時点では、再起動時後に重複したIDを発行してしまう適当実装だったが、急いでいたため気づかず。

11:09 18,073点

最上位のslow query: user_presentsテーブルへのSELECTクエリ

検索条件に使われていたuser_iddeleted_at、ソートに使われていたcreated_atのDESCに対しての複合インデックスを貼る。

11:18 27,927点

最上位のslow query: Prepare

interpolateParams=trueを設定。

11:30 29,008点

最上位のslow query: receivePresent内のUPDATEクエリ

N+1になっていたので、bulk updateに変更。

12:06 34,067点

最上位のslow query: drawGacha内のuser_presentsテーブルへのINSERTクエリ

drawGacha内のINSERTクエリがN+1になっていたので、bulk inertに変更。
また、上位に上がっていたobtainPresent内のSELECTクエリがN+1だったので改善。
Goを1.18から1.19に更新。

アプリ負荷に比べてまだDB負荷が大きかったが、DBを二台以上使えば誤魔化せると判断。
2台目以降を準備開始する。
@oribeが各サーバーのセットアップを行い、@eiyaがDBを分散させるための追加実装などを担当。

昼食休憩。

13:24 37,365点

app 1台、DB 2台の3台構成を試す。

クエリを投げるDBをユーザーIDによって選択することで分散を試みる。
初期実装では高速かつ簡単なxorを使ったhashを試したが、IDの生成方法と相まってDBが台のとき上手く分散されないことに気づく。
ユーザーごとに均等に分散されるように、xxhashを使用。

改善前

func xor64(x int64) int64 {
	x = x ^ (x << 13)
	x = x ^ (x >> 7)
	return x ^ (x << 17)
}

db := dbs[xor64(userID) % len(dbs)]

改善後

func userXXHash(userID int64) uint64 {
	x := xxhash.New()
	var b [8]byte
	binary.LittleEndian.PutUint64(b[:], uint64(userID))
	_, _ = x.Write(b[:])
	return x.Sum64()
}

db := dbs[userXXHash(userID) % len(dbs)]

one time tokenの削除をsoft deleteからhard deleteに変更。
user_one_time_tokensテーブルにtokentoken_typeの複合インデックスを貼る。

14:16 71,078点

4台への分割成功。

1台目: 使用せず
2台目: appとnginx
3,4台目: user_presents、user_presentsをuserIDで振り分け
5台目: その他DB

initializeを全DBサーバーでは走らせるための準備に手間取った。
もともと各サーバーごとに個別の環境変数を簡単に撒けるようにしていたので、それを使って挙動を制御した。

user_sessionsの削除をsoft deleteからhard deleteに変更。

14:50 80,408点

user_itemsのシャーディング。
item_mastersの更新をすべてのDBに反映するようにした。

15:19 108,381点

user_one_time_tokensをシャーディング。
マスターデータの更新をすべてのDBに反映するようにする。高速に反映できるよう、golang.org/x/sync/errgroup.Groupを使って並列にシャーディング先DBのマスタデータを更新する。
adminUserでもシャーディング済みのユーザーデータを参照するようにした。

15:47 147,250点

DBのmax_connectionを上げた。

5台構成に変更
2台目: appとnginx
1,3,4,5台目: DB

殆どのテーブルについて、userIDでシャーディングされている状態。mastersは全台のDBが同一のテーブルを持っており、更新時にも全台に反映する。

16:03 160,260点

ID生成方法を改善。ID生成時に外部に情報を保存しなくとも再起動時に重複しない要件を満たすためと、アプリケーションプロセスを複数台サーバーに置くことを見込んで、snowflakeを使う実装に変更。実際はアプリケーションのプロセスは最終的に1つに落ち着いた。
user_sessionテーブルのPrimary keyをuser_idに変更する。

17:40 190,894点

全体のリソースが浮いてる状態で策が尽きたので、obtainItemをbulkで処理しようと3人で頑張った。
17:30コードフリーズを目指していたので、がっつり実装するには厳し目な残り時間で実装スタート。
ItemTypeごとの処理を関数に切り出して、それぞれが各関数を担当する形で並列に実装を進める。
一通りの実装は17時ごろには終わっていたが、バグがあってfailしまくっていたため全員で総力を上げてデバッグすることに。

17:44 231,371点

ログを切る。

結果発表

最終スコアは242,653点。
全体2位&学生1位を獲得🎉

企業賞としてはNew Relic賞、freee賞を受賞。

ISUCON12 本選の結果発表と全チームのスコア (追記あり) : ISUCON公式Blog

ツール準備

ツールの整備は主に@oribeが担当。予選時に使っていた環境を改良して使用した。

Makefile

isucon12-final/Makefile at main · oribe1115/isucon12-final

競技中に使用するコマンドは基本的にMakefileで管理。
ちゃんとしたスクリプトを書くことも考えたが、メンバー全員が簡単に実行内容を確認できること、回ごとに微妙に違う環境への対応のしやすさからMakefileを使用し続けることにした。

Discordへのpost

チームではボイスチャット・テキストチャットのツールとしてDiscordを使用。
ベンチマーク実行時の情報とalpやpt-query-digestによる計測情報を、discocatによりDiscordに送信するようにしていた。

Discordに送信されたベンチマーク実行時の情報と計測情報

ブランチの切り替え忘れや最新コミットをpullしていないといった作業ミスが予選や練習で数回発生していた。
ベンチマーク実行時の環境情報としてgitのブランチ名やコミットメッセージを送信するようにしたことで、これらのミスを他のメンバーでも気づけるようになった。

また、この詳細なベンチマーク実行ログは本選が終わって2ヶ月以上経ってからブログを書くのにも大変役に立った。

同一コミットでのベンチ実行の警告(未使用)

最新コミットへの変更忘れの対策として、前回ベンチマークを実行したのと同じコミットでベンチマークを実行しようとしたときに警告を出す、というmakeコマンドを追加した。

***************************************************************
*                  !!!!!!!! WARNING !!!!!!!!                  *
*  The current HEAD is the same commit with the last deploy!  *
***************************************************************

これにより作業者が即座に作業ミスを気づけるようにしたかったのだが、このmakeコマンドを常用するコマンドに組み込むのを忘れていたため未使用で終わった。

netdataのカスタムダッシュボード

サーバー上のリソースの監視にはnetdataを使用。
デフォルトのダッシュボードは情報が多すぎてベンチマーク実行中に全体を監視するのは難しかった。
そこで、カスタムダッシュボード機能を使って特に重要な情報をまとめたダッシュボードページを作成した。

Custom dashboards | Learn Netdata
isucon12-final/isucon.html at main · oribe1115/isucon12-final

カスタムダッシュボードの一部

サーバー間で共通のコード、個別の環境変数と設定

リポジトリでは、全サーバー間で共通して使用するソースコードと、各サーバーごとに設定可能な環境変数と設定ファイルを管理した。

サーバーごとの個別のファイルはs1などのディレクトリ以下で管理する。

.
├── Makefile
├── README.md
├── s1
│   ├── etc
│   │   ├── mysql/
│   │   ├── nginx/
│   │   └── systemd/
│   └── home
│       └── isucon
│           └── env
├── s2/
├── s3/
├── s4/
├── s5/
├── tool-config/
└── webapp/

環境変数は各envファイルに記述し、それをMakefile内でも読み込んだ。

s1/home/isucon/envISUCON_DB_USER=isucon
ISUCON_DB_PASSWORD=isucon
ISUCON_DB_HOST=127.0.0.1
ISUCON_DB_PORT=3306
ISUCON_DB_NAME=isucon
SERVER_APP_PORT=8080
PERL5LIB=/home/isucon/webapp/perl/local/lib/perl5
SERVER_ID=s1

ISUCON_DB_HOST2=133.152.6.66
ISUCON_DB_HOST3=133.152.6.67
Makefileinclude env
ENV_FILE:=env
# 変数定義 ------------------------

# SERVER_ID: ENV_FILE内で定義

...

.PHONY: deploy-db-conf
deploy-db-conf:
	sudo cp -R ~/$(SERVER_ID)/etc/mysql/* $(DB_PATH)

...

環境変数のSERVER_IDは、サーバーに展開する設定ファイルの制御やDiscordの送信情報に使用した。

サーバーごとに簡単に環境変数を設定できるこの構成は、サーバーごとにアプリケーションの振る舞いを変える実装に活用された。

余談

commit

oribe icon
この記事を書いた人
oribe

お絵かきする時間が欲しい

eiya icon
この記事を書いた人
eiya

プログラミングをやっています。 メイン言語はC++14 競プロ/ゲーム制作/CTF

toki icon
この記事を書いた人
toki

Java, Go, JS/TSなどをいじっています

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年11月26日
ISUCON13にツールの力で勝ちたかった(mazrean)
mazrean icon mazrean
2021年9月21日
ISUCON11 traP CM制作についての小話
dan_dan icon dan_dan
2023年11月26日
【 #ISUCON 】 最近の若者は ssh しないらしいですよ
ikura-hamu icon ikura-hamu
2022年7月24日
ISUCON12に向けてダッシュボードを自作してログを可視化しました
Ras icon Ras
2023年12月5日
部内ISUCON「PISCON」を開催しました!
cp20 icon cp20
2023年11月22日
ISUCON初心者向け講習会のテキストを作り、開催しました!
pikachu icon pikachu
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記