この記事は every Tech Blog Advent Calendar 2024 5 日目の記事です。
はじめに
こんにちは、DELISH KITCHEN 開発部でソフトウェアエンジニアをしている24新卒の新谷です。
今回は12/8開催のISUCON14に向けて、ISUCON初参加の私が勉強したことについてまとめていきます。
また、everyはISUポンサーとして協賛しており、詳しくは以下をご覧ください。
初参加に向けたざっくりの戦略
今回参加したチームは、日本CTO協会の新卒合同研修で知り合った新卒メンバーで出場しました。 (日本CTO協会の新卒合同研修についてのブログはこちら)
全員がISUCON初参加ということで、それぞれ役割を決めて、それに向けて勉強を進めました。 そのうち私は、DB周りのインデックス担当ということで、DBのインデックスの張り方を中心に勉強しました。
また、役割はあるもののチーム全員で共通して勉強したこととして、以下があります。
- Go言語
- N+1の対策
- オンメモリキャッシュのやり方
- JOINなどのSQL構文をスラスラ読める&書けるようにする
- 過去問を解く
特にN+1の解消に関しては、ISUCONでは頻出するパターンのため、JOINして解決するのかIN句で解決するのかキャッシュで回避するのかなど、事前にかなり話し合いました。 それぞれ詳しく勉強したことについては、以下で紹介していきます。
DBのインデックスについて
なぜインデックスの勉強が必要か
インデックス・ショットガンと呼ばれるアンチパターンがあるように、無闇にインデックスを張るとパフォーマンスが悪化することがあります。 特に、INSERTやUPDATEが多いテーブルは、書き込みのオーバーヘッドが大きくなるため、インデックスを張る際には注意が必要です。
MySQLのインデックス
ISUCONでは、DBにMySQLを使用することが多いため、MySQLのインデックスについて勉強しました。
以下の記事は、インデックスの基礎を学ぶのに参考になりました。
こちらはInnoDBにおけるインデックスの基礎知識を学べる他、インデックスを張るときのよくある間違いについても解説されています。 techlife.cookpad.com
こちらは、MySQLのクエリーライフサイクルやUsing filesort, Using whereが何をしているのかをトランプを例に解説されています。 www.slideshare.net
上記を勉強するとEXPLAINの結果の意味がわかるようになるのと、インデックスを張るときの注意点がわかるようになります。
Go言語
こちらに関しては私は普段から業務でGoを書いているので特段勉強はしませんでした。 ただ、ISUCONではDBを操作する際にsqlxを使うことが多いため、sqlxの使い方については事前に勉強しました。
全てのメソッドは覚えませんでしたが、以下についてはスラスラ書けるようにしました。
- 1行を取得するときの
Get
if err := db.Get(&user, "SELECT * FROM users WHERE id = ?", id); err != nil { return err }
- 複数行を取得するときの
Select
if err := db.Select(&users, "SELECT * FROM users WHERE age = ?", age); err != nil { return err }
- In句などを使うときの
In
query, args, err := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids) if err != nil { return err } query = db.Rebind(query) if err := db.Select(&users, query, args...); err != nil { return err }
- Bulk Insertもできる
NamedExec
_, err := db.NamedExec("INSERT INTO users (name, age) VALUES (:name, :age)", users) if err != nil { return err }
N+1の対策
N+1に関しては、大きく分けて以下の解決方法があると考えています。
- JOINしてN個のクエリを1つにまとめる
- IN句を使用してN個のクエリを1つにまとめる
- クエリで取得しているデータをキャッシュする
基本的にJOINをする方が効率的ですが、実装が大変です。また、キャッシュは実装は簡単ですが、書き込みや更新があるデータに関しては注意が必要です。
そこで私たちのチームでは以下の方針で決めていました。
- 基本的にはJOINを使う方針
- 書き込みや更新がないデータは、キャッシュで実装する
- 1対多の関係にあるデータは、IN句を使う
- 書き込みや更新があるが、ユースケース的に書き込み処理などが少ないデータは、キャッシュで実装する
また、上記以外に、アプリケーション側でデータを絞っているにも関わらず、LIMIT句をつけていない場合は優先してLIMIT句をつけることも重要です。N+1自体の解消にはなりませんが、これによってDB負荷のボトルネックが改善され、点数が伸びることもあります。
オンメモリキャッシュのやり方
N+1の解消などにおいて、オンメモリキャッシュを使う場合は、どのように実装するのかチームで決めていました。 まず、書き込みや更新がないデータに関してはMap型で事前にキャッシュするようにしていました。
ただ、書き込みや更新があるデータに関しては、スレッドセーフなキャッシュを実現する必要があります。 そこで、私たちのチームでは、ISUCON用に開発されたcatatsuy/cacheを使うことにしました。
当初は、syncのMapを使うことを検討していましたが、以下の理由からcacheを使うことにしました。
- Genericsを使っているため、型キャストが不要
- キャッシュの有効期限を簡単に設定できる
- パフォーマンスもSync.Mapとほぼ変わらない
他にも理由はありましたが、主に上記の理由からcacheを使うことにしました。
JOINなどのSQL構文をスラスラ読める&書けるようにする
N+1の解消において、基本的にはJOINを使う方針となったので、チーム全員がJOINに対して慣れる必要がありました。 また、そもそもISUCONではサブクエリを使った複雑なクエリやORDER BY句を使ったクエリなども出題されるため、SQLをスラスラ読めるようにすることが重要だと考えました。
SQLは以下のネットの問題集を使って勉強しましたが、他にも問題集などはあるため正直なんでもいいと思います。
普段ORMを使っていたりすると、意識してSQLを書かなかったりすることもあると思うので、良い勉強になりました。
過去問を解く
過去問に関しては、時間的に全て解くことは難しかったため、直近の問題を何度も解くことにしました。
- ISUCON13
- ISUCON12の予選
- ISUCON11予選
- private-isu(過去問ではないが)
解説などを見ながら解いたりして、ボトルネックの特定のやり方や、どのようなアプローチで解いているのかを理解しました。 また練習中は、Copilotを切ったりコピペをしないようにしていましたが、これが意外と練習になりました。
まとめ
以上がISUCON14に向けて勉強したことです。 本番どうなるかは分かりませんが、練習してきたことを活かして、全力で取り組みたいと思います。
また、DBなどはISUCONのために勉強しましたが、普段の業務でも使える知識が多かったです。 そのため、ISUCONに活かすだけでなく、普段の業務でも活かせる部分は積極的に取り入れていきたいと思います。