負荷に強い（かもしれない）サーバーエンジニアが開発時に心掛けていること

アカツキゲームスのちょうです。
この記事は [Akatsuki Games Advent Calendar 2023](https://adventar.org/calendars/8724) の3日目の記事です。

昨日は shivashin さんの [Gravitonに移行した話](https://hackerslab.aktsk.jp/2023/12/02/000000) でした。Arm 製 CPU のインスタンスの移行で運用エンジニアは大変ですが、安くて性能いいから開発エンジニアとしてはとても嬉しいですね！（AWSさんにもっとインスタンス用意してもらいたいですねw）

## 自己紹介

ちょうは、最適化や高速化がとても好きで、プログラムが高速に動くと幸せな気持ちになります。

[Dyson sphere program](https://store.steampowered.com/app/1366540/Dyson_Sphere_Program/?l=japanese) のような工場を並べ、パイプライン生産を設計したりするゲームがとても好きで、最近は [Cities: Skylines II](https://store.steampowered.com/app/949230/Cities_Skylines_II/) で町作りしながら、交通の最適化に頭を悩まされています。この手のゲームをやっていると時間の流れが早いので、睡眠時間の最適化も必要になっちゃいますね。

[アカツキゲームス](https://games.aktsk.jp/)に入社してから、2つの大規模なプロジェクトで2年ずつソシャゲーのサーバー開発をしてきました。通常開発しながら、負荷改善を積み重ね、それぞれで全体の3割くらい高速化できた成果がありました。まあまあ負荷には強い開発エンジニアだと自負があるので、今日はちょうの「負荷に強い（かもしれない）サーバーエンジニアが開発時に心掛けていること」について書いてみようと思います。


# 早さは正義
ゲームの API サーバー開発において、早さは正義です。

APIが軽いと、ゲームがサクサク動くからプレイヤーの体感がいいですし、同じリクエスト数を処理することに必要なリソースが少なくなるのでインフラコストも低くなります。また、ゲームはキャンペーンなどが始まるタイミングにおいてはアクセスが集中しがちで、バックエンド処理が軽いと、システムの処理能力上限も高くなります。

プレイヤーも会社も喜ぶ、いいことずくめですねw


## 機能設計編
ゲーム開発はざっと企画、機能設計（ゲーム画面や遷移）、データ構造やAPI設計、プログラミング、テストのような流れになってたりします。エンジニアの仕事は、プランナーさんゲーム機能を設計してから始まるわけではなく、機能を設計する段階から入るべきだと考えています。実際に作り始めてから機能の仕様を直すと大変なので、そのまま最適じゃない状態で作ってしまうことが多かったりしますよね。やがてどんどん負債が溜まっていくことになります。

うちの会社のプランナーは優秀で、マスタデータの案まで綺麗に作れたりしますが、やはりプログラミングの視点を持ってないので、完璧は難しいですね。そのため、目的やWHYなど、何を求めてこの機能を作っているのかをプランナーさんに聞いて、機能設計段階でもっと作りやすくて同じく目的達成できるやり方がないかを考えることがとても重要になります。もちろん、提案が却下されることも多いですが、実践上開発工数を抑えられることもかなり多いと思います。

そして、こうすればよかったと思うのことがあるのであれば、間に合う場合なら必ず相談をするようにしています。世に出してからだと遅いですが、リリースしてないならまだ間に合う可能性あります。ダメ元ででもいいから、そのスタンスで仕事に取り込むのがいいと考えています。

## クエリ編
ゲームのみならず、ウェブアプリケーションの一番のボトルネックは通常データベース(RDB)にあります。高負荷下で、その貴重なデータベース資源をどのように有効に使うかがサーバーの限界突破の肝となります。

その中でも、高速化するには、データベースの index は一番肝心なところです。そのため、サーバーエンジニアは index の B+Tree 構造に深い理解を持つことがとても重要だと考えています。このようなクエリが来たらどのように検索をするのか、を常に頭で考えながら開発に取り組みのがプロの仕事ですね！

今風のサーバーアプリケーションでは、ORM を使ってクエリを自動的に発行することが多いので、直接クエリを書くことは少ないでしょう。とても便利ですが、プロであれば、裏で何が起きているかを把握したいですね。ほとんどのフレームワークではクエリ発行するとログを出力してくれるので、どのようなクエリになっているのか、自分の目で確かめるといいかもしれません。そこで変なクエリがないか、同じテーブルに複数のクエリ発行（いわゆる N+1 クエリってやつ）がないかは一目でわかってたりします。さらに検証時に開発環境でクエリのログを眺めることで N+1 が起きてないか簡単に確認できたりします。負荷テストして、APMで調査するよりも N+1 発見に役立つ可能性があったりします。

## マスターデータ編
ゲームには全員共通の設定データが多く、その名をマスターデータと呼んでいます。ウェブとは違い、都度APIから情報をもらうのではなく、データをフロントエンド（ゲームアプリ）にも持たせることで、高速に反応できるように通常しています。

そのマスターデータをサーバーでどう扱っているかというと、昔だとデータベースで管理することが多いですが、メモリに置くことで動作がより早かったりするので今風かも。メモリに置く場合、一般的には、id や foreign_key をキーとする MAP でデータを保持することで index がわりとなり、高速に取り出せるようにします。

ただし、ゲームは期間限定なレコードを処理することが多いです。レコードに `start_at`, `end_at` が存在し、その間にのみデータが有効である感じです。今の現場では元々は全検索に近いやり方でレコード取得をやっていました。リリース当初はデータが少なかったからとても早かったですが、データが多くなると、メモリ上ででも遅くなるものです。

そこで私が推奨するのは（データベースやメモリどちらででも）、`end_at` で index 化することです。 index を貼ること、当たり前だろうって思う方もいるかもですが、ベテランででも `start_at` を index に入れがちだったりします。まだ始まってないキャンペーンなどのレコードは、直前からしか本番に入らないのです。つまり、start_atを満たすレコードがほとんどで、絞り込みが効かないのです。そして、 index の特性上、邪魔になるので、効率がとても悪いのです。 index で `end_at` を優先度をあげることで、終了前のレコードを一気に絞り込むことができます。

## データ構造の設計
マスターデータやデータベースの設計ですが、「人間がわかりやすい」＆「検索しやすい」の2軸を重要視しています。研修・授業等では正規化の話が出てたりしますが、完全なる正規化するとアプリケーションを作るのが難しくなるので、重要なのはその形ではなく、そのマインドを把握することです。特に正規化レベルが高すぎて、`join` がいくつも必要な検索となると、データベースにとても負荷がかかってしまうし、 index も効きにくいです。また、検索されないカラムは `json` や `blob` などでも良く、1つのカラムに大量なデータを突っ込んでもよかったりします。（ただし、アプリ側で型の変化に要注意）

バランスの取れた設計が best practice となります。

## アプリケーション編
時間が経つとデータが増えていくので、性能の劣化により「数年後でも問題なく動くか」もとても重要な課題であります。計算量オーダーをちゃんと考えてプログラムを書きましょう。（しょうがない場合がほとんどですが、頑張る）

実際現場によくある例として、「リストに存在するか」・「リストから検索」があったりします。ループ処理になるので、さらにループの中でこれをやるととても重くなっちうことがあります。リストが長くなる恐れがあれば、リストをあらかじめMAPやSETに変換することで早くなってたりしますね。

また、クエリででも同じような問題が起きてたりします。`select * from x where y_id in (1,2,3,...100000)` みたいなクエリはとても多いと思います。
一気にデータベースに投げているのはいいですが、データベースではループで index 検索が必要ですね。
その場合は、rangeにしたり、複数レコードを1つにまとめたり、多くのレコードを1つのキーで取得できるようにすると改善できたりします。

## でも、キャッシュサーバーは慎重に使いましょう
`Redis` や `Memcached` をキャッシュサーバーとしてよく使います。レスポンスはデータベースよりずっと速いですよね。ただ、使えばいいというわけではないと考えていて、データベースで足りる場合は重くてもデータベースに任せることがいいと考えることもあります。

キャッシュサーバーは「揮発性がある」、「トランザクションに対応できない」といったデメリットがあり、同期が難しいので、データベースより扱いにくい問題があります。キャッシュサーバーを使うことで、アプリケーションの複雑度が高くなり、不具合の出現確率が高くなります。また、ループ処理などでアクセスをしすぎてしまうと、通信に時間がかかってしまうこともあります。

理解した上で、考えてから利用を決めることがとても大切です。

# まとめ
負荷改善をよくしてたちょうが考えた開発時の気をつけるポイントでした。偉そうに色々書きましたが、私もまだまだ成長途中なので、「最適な仕様 x 最適な設計 x 最適なプログラム = 開発エンジニアが幸せ」の考えで、これからもみなさんと一緒に最適化・高速化を頑張ります。

---

この記事は [Akatsuki Games Advent Calendar 2023](https://adventar.org/calendars/8724) の3日目の記事です。

個人感想が多いので、個人ブログで発表していますが、[Akatsuki Hackers Lab](https://hackerslab.aktsk.jp/about) にも面白い記事がいっぱいあるので、よかったら読んでみてください。

明日は、EichiItoさんの記事となります。お楽しみに！