📈

なつやすみ isucon11 さんかした! #isucon

2021/08/24に公開

この記事は10分程で読むことができます!

今回はISUCON11に学生枠として参加しましたので、そちらのまとめを執筆したいと思っています!!

この記事では、ISUCONに出た経緯や当日までの準備、当日での動きを中心に記載しています。具体的な問題内容や技術的な解説等はISUCON公式サイトや他の方々が網羅されていますので、ここでは部分的な内容を記載します。(僕か書くよりよっぽど有益ですので。笑)

ISUCONとはそもそもなんぞやの人や、ISUCONって聞いたことあるけどガチ勢多くて怖そう...という人に、少しでも届けられればと思います!(僕も実際そうでした!笑)

僕自身、ISUCON11によって多くを学び楽しませて頂いた身です。運営の方々にはこのような機会を作ってくださり感謝しきれないです..!

ISUCON11に出た経緯

ここでは、ISUCONの参加目的を記載します。何をやるにも目的ありきですが、僕の目的と近い人がいる方の参考になれればと思います。

そもそもISUCONとは

ISUCONとは、お題となるWEBアプリのパフォーマンスを向上させ、アプリの利用者がどれだけ快適になったかを競うコンテストです!

ISUCONのお題となるデフォルトのアプリの実装を見ると、N+1問題、二重forループ、大規模レコードに対してのOFFSET,LIMITの利用 など実務でのパフォーマンスでボトルネックになりえるアンチパターンが多数確認されます。

それらを改善し、ユーザーのリクエストに対してどれだけ素早く正確にレスポンスを返したか(どれだけ快適になったか)で得点を競います。

詳しくはISUCON11事前講習資料を確認するとわかりやすいです!

なぜISUCONなのか

僕のISUCONの参加目的は3つありました。

1つ目めは、純粋に学生のうちにパフォーマンスチューニングの技術を学びたかったためです。この技術は、基本的に完成したプロダクトを運用している際に取り組まれる技術であると解釈しています。そのため、学生のうちに触れることが比較的難しい印象があり、ISUCONに参加することで技術理解の場にしたいと考えました。

2つ目は、パフォーマンスチューニングを学んだ経歴を作りたいためです。こちらは、今後業務に入る際に、何かしら学んだ経歴を少しでも作ることで、パフォーマンスチューニング業務のタスクも振る契機になると考えたためです。作ってないより、作ったほうが明らかに意欲が伝わると考えました。

3つ目は、他の同期達2人とチーム参加し、信頼を深めたいと考えたためです。今後何かしら業務する際にも、チームとして活動することで個々の強みがシナジーを生み 1+1 > 2 のような現象が起こります。それを実現するには信頼構築が必要と考えますが、ISUCONはその中で、辛い部分や楽しい部分が沢山あり、互いに認め助け合う精神が構築されると考えました。


当日に向けて準備したこと

この章では、ISUCON11に向けて行ったことを記載します。準備の進め方の一方法論として参考になれれば幸いです。

当日の内容が知りたい方は、次の「当日の内容」という章へスキップしてください。

立ち上げ

登録をした後、3人で話し合いを設け、各自の参加目的を確認したり、ISUCONに対して各々が調べ解像度を上げる作業をしたり、解像度から今後のの方針を考えたりしました。

各自の目的の確認をする

まずなぜISUCON11に参加しようと思ったか目的をお互いに共有しました。お互いの目的を知らないと、今後の方針と各自の目的の間にズレが生じモチベーションを保ちづらくなるためです。

各自の目的は前章の「なぜISUCONなのか」という節で概ね収まりました。

ISUCONに対して調べ解像度を上げる

目的を理解した次は、ISUCONに対しての解像度を上げる作業をしました。ISUCONに対して精度の高い理解をしていないと、今後競技を行うにあたって見当違いな努力をしてしまう可能性があるためです。

前年度のISUCONのマニュアルの確認、レギュレーションなどを簡単に確認したのち、下記のようにメモをみんなで過剰書きをしました。(ここでは丁寧にメモるというより、まずは分散する思考のままに書き、その後頭を整理することを目的にしました。)

Image from Gyazo

そこから出た不安部分をもとに、再度を調査を行い解像度を上げることを繰り返しました。

解像度から今後の方針を考える

上記の解像度をもとに後の方針を立て、そこから具体的にスケジュールなどを決めました。

ここでは、ISUCON10の問題(過去問)を実際に解く会(3日間の合宿)を行い、そこから見えた課題をもとに当日まで何かしら素振りをするという方針のもと、お互いが時間取れる日を募りスケジュールを立てました。

他方、チーム参加では基本的にアプリやDB, インフラなど担当を分けた方が良いので、実際に解く会で得られたフィードバックから役割を決めるという方針で進みました。

3日間のオンライン合宿

3日間の合宿では、ISUCON10の過去問を調査か実際に解きフィードバックを得たり、また優勝者などのブログを確認しどういうような戦略で当日を迎えたのか確認したりしました。

ここでは、下記の画像のようなスケジュールで3日間で解きました。メモ用であったため汚いです🙏
Image from Gyazo

大枠としては、

1.問題を解いて慣れる
2.上手い人の方法論を学ぶ
3.当日までの事前準備を各自何やるのかを決める

といったことをしました。

そして、ISUCON合宿を終えたのち、他のメンバーと話し合い、役割を決めたりしました。

ISUCON合宿はさまざまなドラマがありましたが割愛します。(当時の僕は以下の感想を述べてました!笑)
https://twitter.com/FitnessDensuke/status/1427292402495102976?s=20

当日までの僕の準備(素振り)

僕は主にSQLを担当することを決め、そのための学習を励みました。まずは、過去問でSQLが関連する箇所をできるだけ探し、課題と対処を学びました。これは、SQLのパフォーマンスチューニングの本を学習するのではなくいきなりISUCONの過去問を見ることで、ISUCONにおけるSQL力を直接的に上げるだけでなく、SQL力の基礎として何が足りないのかをあらうことも意識していました。

下記のように、過去のISUCONの解説を遡り、自分でなぜそのカラムにINDEXを貼ることが良いのか、そもそものボトルネックは何かなどを理解することに努めました。
Image from Gyazo

その後、SQLの基礎を上げるため主に以下の教材を使いました。(思っていた以上にSQLを知っていた気になっていたので初心に戻るなどもしました。笑)

これらをこなし、当日を迎えました。

当日の内容

ここでは、当日の戦略や、最終的な結果と反省、印象に残った問題などを記載します。

ISUCON11の内容

今回は、ユーザー, ユーザーが登録するイス, アプリ, 外部のAPIという3つで構成されたプロダクトを改善しスコアを競うモノで、かなり実務寄りの印象でした。学べる部分が多く非常にありがたい問題でした。

機能視点で見ると、

1.ユーザーがアプリ経由で外部API利用によるログイン認証を行う
2.ユーザーがアプリ経由で自身のイスを外部APIのアクティベート処理により登録する
3.登録されたイスの更新情報を逐次確認する

といった内容でした。

ここでは深く話しませんので、興味がある方は当日マニュアルなどを確認してください。

全体としての戦略

ISUCON11では所要時間が8時間と時間に制約が意識しやすかったので、時系列でどのように動くかで戦略を考えました。

具体的には、

1.マニュアルを読む
2.環境構築をする
3.改善サイクル(ログの計測=>ボトルネックの改修=>ベンチマークで採点=>バグがない状態でgitコミット)を行う

その後制限時間が近づいたら、
4.ログを停止する
5.再起動試験に備える(DBとアプリの起動依存関係に注意する等)

といったことを主に意識しました。

Image from Gyazo

ちなみにですが、実際にエンドポイントのコードを読みどこにボトルネックがあるか話し合うなどもしました。
Image from Gyazo

他にも、他のメンバーが自動デプロイのスクリプトやログ計測自動化などオペレーション構築をしてくれてありがたかったのもありましたが、ここでは具体的な技術的は割愛します。

結果と反省

結果としては7000点代で予選敗退でした。こちらがチームスコアです。

初回にしては、個人的に健闘したと思っています。各メンバーが、課題に対して対処を事前に準備し、アプリやインフラの修正を頑張った結果だと感じています。メンバーには本当に感謝しています!

一方、"nginxのaccess_logの切り方わからんから調べるロスが..!"といった実際に深く問題を解かないと見えてこない無駄や、"JOIN句でテーブルをまとめてN+1を対処したいけどsqlが欲している構造体的にできないから実装どうするねん!"といった単純な知識不足などの課題も見えてきました。ここに関しては、継続的なISUCONの参加と技術習得が重要だと再認識しました。

もちろん前提として、ISUCONそのものが今回の目的ではなかったのですが、ISUCONを取り組む過程の中で目的となる要素を体験し、学びそして楽しむことができました。
来年は、ISUCONでスコアを上げることを目的を最大目的にするのが、よりシンプルな目的でかつ今回の目的の要素が過程で含まれるので良いなと思いました。

印象に残った問題

ここでは僕が印象に残った問題を記載します。他にも多く問題がありますが、その解説などは多くの叡智ある方々が記載していますので省きます。

N+1問題

GET /api/trendのgetTrend()内でのINDEXを貼る問題です。ログを計測すると、getTrend()の処理にボトルネックがあることを確認し、コードを見た際に観測しました。(N+1問題は他にGET api/isuにも存在していました。どちらのN+1問題も構造的にほぼ同じでした。)
実際のコードは下記の通りです。

 //中略

	for _, character := range characterList {
		isuList := []Isu{}
		err = db.Select(&isuList,
			"SELECT * FROM `isu` WHERE `character` = ?",
			character.Character,
		)
		if err != nil {
			c.Logger().Errorf("db error: %v", err)
			return c.NoContent(http.StatusInternalServerError)
		}

		characterInfoIsuConditions := []*TrendCondition{}
		characterWarningIsuConditions := []*TrendCondition{}
		characterCriticalIsuConditions := []*TrendCondition{}
		for _, isu := range isuList {
			conditions := []IsuCondition{}
			err = db.Select(&conditions,
				"SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = ? ORDER BY timestamp DESC",
				isu.JIAIsuUUID,
			)
			if err != nil {
				c.Logger().Errorf("db error: %v", err)
				return c.NoContent(http.StatusInternalServerError)
			}
  
  //以下省略

ここでは、SELECT文が二つあり、また上のselect文が下のSELECT文で必要としている値がuuidであるため、JOIN句でuuidをキーに繋いで対処しようとしましたが、Goの構造体の仕様等で実現ができませんでした。

最終的には、最初のforのSELECT文はそのままにし(where句のcharacterもカーディナリティが低かったため放置)、次のforでは、whereのin句を使い、"WHERE jia_isu_uuid IN(番号1, 番号2, 番号3...)"をstrings.join(配列, ",")での実装をベースに、徐々に対処しました。

追記: 最初のクエリ部分のisuList変数では、imageファイル(重い処理の原因)が格納されているので、SELECT * 〜ではなくimageのカラムをはずした状態でクエリを飛ばすのも有効です。

INDEXを貼る問題

ここでもGET /api/trendのgetTrend()内でのN+1内に記載されたコード部分での、INDEXを貼る問題です。pt-query-digestでmysqlのスロークエリログを確認したところボトルネック箇所になっていました。実際のコードは下記の通りです。

err = db.Select(&conditions,
  "SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = ? ORDER BY timestamp DESC",
  isu.JIAIsuUUID,
)

ここでは、timestampをORDER BYでソートするものに対して、INDEXを貼ることが求められました。しかし、ORDER BYの指定はDESCであり、今回のDB環境のmariadbでは対応していませんでした。そのため、generated columnでtimestampをunixtime*-1したものを追加して、 INDEXを貼ることで対処する必要がありました。

追記: ORDER BY timestamp DESC の部分は、サブクエリを用いて下記のようにも指定が可能です。

  SELECT * 
	FROM `isu_condition` 
	WHERE `id` = (
		SELECT MAX(id) 
		FROM isu_condition 
		WHERE jia_isu_uuid = ? 
	)

こちらはISUCON10でも同じような問題があり、まさに対策していた箇所が出たものだと思われます。

今後の展開など

上記でも述べたとおり、"継続的なISUCONの参加と技術習得が重要"がですので、来年も同じメンバーで参加したいですし、来年に向けてよりレベルアップしたいと考えています..!!
来年からは社会人枠としての参加ですので、より引き締めたいなと思います、、!!笑

ISUCONは僕らに学習の必要性を認識させ強くしてる場所です!少しでも興味がある方はとりあえず参加ボタンをポチると意識が変わると思います!誰でも最初は初心者です。笑

ここまで読んでくださりありがとうございました!!

ISUCON参考資料

Discussion