/note/tech

秒間 10,000 リクエストを "簡単に"いなすゲームサーバーを Laravel で作る設計

要約:

■ 1. リクエストとRPSの定義

  • 1リクエストの定義:
    • クライアントからサーバーにデータを要求する
    • サーバーが適切な処理を行って結果をクライアントに返す
    • これを1リクエストと定義する
  • RPSの概念:
    • リクエスト・パー・セカンドの略称
    • 秒間1万リクエストは約1万台のクライアントが一斉にリクエストし始める状態
    • その状態が1秒間だけでなく1分間や1時間や1日や1ヶ月ずっと同じような負荷がかかっている想定
    • クライアント1台のリクエストの時と比べると約1万倍の負荷がサーバー側にかかる

■ 2. ゲームサーバーの特徴

  • 対象となるゲーム:
    • スマートフォン向けのソーシャルゲームを対象とする
    • 4人で同時に協力するゲームや3対3で対戦するゲームは除外する
  • HTTP通信サーバー:
    • 普通のWebサービスとあまり変わらない
    • HTTPは最も安定してスケールもするプロトコル
    • 構造化にはJSONやProtocol Buffersを使用する
    • 受託企業ではクライアントとサーバーは別の会社が担当することが多い
    • OpenAPI 3.0を採用してRESTful APIの定義を共有する
    • AsyncAPIによりWebhookやWebSocketなど双方向の通信のプロトコルを定義できる
  • 非常に大規模かつ素早くスケールする必要性:
    • 事前登録で予想はつくが実際のリリース時にSNSでバズると予想の何倍や10何倍とかのユーザーが来る可能性がある
    • サーバー落ちや緊急メンテナンスが発生することが多い
    • 大量のアクセスが来るのは前提として準備をしっかりする必要がある
  • 負荷の波への対応:
    • 1日のプレイサイクルがあり朝や昼や夕方の空き時間にユーザー数が増える
    • その波に応じてスケールしておかないと無駄なコストが発生する
    • イベント開始の直後やイベント終了の直前に急激にスパイクする
    • ユーザーがどれくらい来るのかは正直わからないため職人芸で事前にサーバー台数を調整する
    • 運用が数年続くとユーザー数が減って負荷も落ちていくため台数は減らせるようにする必要がある
  • データベースへの高頻度な書き込み:
    • ライトヘビーと呼ばれる特徴がある
    • ECサイトなどはほとんどのユーザーが見るだけでリードヘビー
    • ソーシャルゲームはユーザーがなんか操作したらすぐデータを更新する必要がある
    • コインを使ってアイテムを購入した時はコインを減らしてアイテムを追加する更新処理を行う
    • 書き込みはスケールしづらいためボトルネックになることが非常に多い

■ 3. 負荷をいなすための実践

  • 負荷試験の必要性:
    • いなせる負荷をかけることができないといなせているかどうかの判断がつけられない
    • 1万RPSをいなすには1万RPS分の負荷をかける状態にある必要がある
    • 実際にかけてみて大丈夫かどうかをチェックする必要がある
  • 負荷試験の結果:
    • RDBが非常に重い
    • PHPと比べ物にならないくらい負荷がかかる
    • Aurora DB for MySQLのCPU使用率が70%ちょっと出る
    • Auroraは80%以上のCPU使用率が出ると急に不安定になるため70%は結構ギリギリなライン
    • コネクション数はMySQLで16000コネクションが限界
    • 同時に16000の接続しか処理できない
    • 1万RPSでだいたい8000から9000弱のため2万RPSいかない計算になる
  • PHP側とLaravel側の評価:
    • 別に何も言うことがなかった
    • コスパ考えたら安定してどんどんスケールして1万RPSまではほとんどの障害ほぼなく終えることができた
    • Laravelだからとか何とかっていうフレームワークだからとかフレームワーク使ってないからとかっていうのは今の時代だとそんなに関係ない
  • 結論:
    • クエリの最適化とコネクション数の削減が重要
    • AWSベースのAuroraベースの構成では特に重要

■ 4. アプリ設計の要

  • クエリの最適化:
    • 発行されるクエリがアプリ側からすぐわかる設計にすべき
    • コードを見てどんなクエリが発行されるのかがすぐわかることが非常に大事
    • ここのクエリ重そうとかここインデックス使われてなさそうとかが確認しやすくなる
  • ドメイン実装とSQLの分離:
    • 本質的な実装とSQLがどうだとかDBがどうだとかっていうのは全く別物
    • そこを分離して実装をまずやる人を分けれるレベルで分離できることが重要
  • 大規模でもスケールする設計:
    • 疎結合でどこで何が実装されてるかわかることが重要
  • バージョンアップへの対応:
    • 数年運用または10年以上運用する可能性がある
    • LaravelやPHPのバージョンアップを前提に踏まえた上で設計する必要がある

■ 5. DDD-y Light Dream Designの詳細

  • 設計の概要:
    • スライドを作りながら名前を考えた設計
    • 一般的には軽量DDDと呼ばれる
    • ドメイン駆動設計のいいところだけピックアップしてやる設計
    • アーキテクトする人がDDDのことをわかってないためわかる範囲で簡単な理解ができるところで取り入れる
  • 3つの原則:
    • Laravelフレームワークは利用するがフレームワークとドメインの実装は完全に分離すること
    • 責務がネームスペースごとに分離されていること
    • ネームスペースごとの依存関係が明らかで片方向になっていること
  • エントリーポイントレイヤー:
    • LaravelのデフォルトであるControllerとConsoleコマンドの2種類
    • Responseクラスで配列をバリデートする
    • FormRequestと同じようなバリデーションの機能をミドルウェアで持たせる
    • 実装間違ってる時にクライアントに教える前にサーバー側で気付ける
  • CQRSサービスレイヤー:
    • コマンドクエリ責務分離のこと
    • データの更新とデータの取得は要件が大きく異なるから別物として考える概念
    • 更新は比較的低頻度でトランザクションの管理を使って安全にデータを更新する責務
    • クエリは高頻度でSQLを最適化して高速なクエリを発行する責務
  • ドメインレイヤー:
    • 本質的なユーザーやレベルやコインといった部分の実装
    • ドメインの中でuse Illuminateなんとかと書くことは禁止でサポートだけは除く
    • CollectionやStrはさすがに使わないで書くと面倒くさくなるためサポートだけを除く
    • ドメインからドメイン外部を使うのはダメで逆方向にする
    • データベースにアクセスしたい時はドメインの中にインターフェースを用意して定義だけし実装は別のところでする
  • ドメインレイヤーの構成要素:
    • エンティティ: 識別子を持ったオブジェクトでだいたいテーブルと1対1でここにロジックを書くのが一番望ましい
    • バリューオブジェクト: 複雑なロジックを1個クラスにする
    • ドメインサービス: ドメインをまたいで行うロジックの集合体で更新処理など
    • リポジトリインターフェース: クエリの発行でfindするとかinsertするとかでインターフェースなので実装は行わない
    • S3など全部インターフェースとして定義してドメインというネームスペースの中では実装はしない
    • use Illuminate Databaseとかはドメインの中にはない
    • クリーンアーキテクチャっぽいところも取り入れている
  • インフラストラクチャーレイヤー:
    • ドメイン以外のクエリの実行部分の実装を全部ここに入れる
    • ここの実装は荒くなりがちだがテストで担保する

■ 6. Eloquentを使わない理由と自作ORM

  • Eloquentを使わない決定:
    • Eloquentを窓からポイッと投げ捨てた
    • Eloquentは悪くないしめちゃくちゃすごい機能
    • Eloquent使ってるのがいけないことだというわけでは全くない
    • 今回のチームの設計と相性が悪かったというだけ
  • 使わなかった理由:
    • アクティブレコード型ORMマッパーであること
    • DDD-y Light Dream Designにアクティブレコードをうまくはめ込むのが難しかった
    • データマッパー型ORMが対照的に存在する
    • 初期実装にIlluminate Databaseのクエリビルダーを使っていたため他のORMに移動すると初期実装全部捨てることになる
    • データベースの負荷分散をするにあたってシャーディングと呼ばれる仕組みを用いる
    • 垂直シャーディングはJOINしないテーブルをデータベースごと分ける仕組み
    • 水平シャーディングはユーザーIDに基づいてこのユーザーはこのDBみたいに分ける
    • シャーディングの機能が垂直はあるが水平がEloquent自身にはないため負荷軽減に厳しい
  • 自作ORMの特徴:
    • 軽量なデータマッパー型ORM
    • Illuminate Databaseベース
    • シャーディングも対応している
    • マイグレーションも全DB一気にマイグレートやロールバックや自動生成ができる
  • 自作ORMの制限:
    • MySQL以外は検証してないため動かない可能性もある
    • hasOneやhasManyは未実装
    • リレーションがなくても意外といけた
    • リレーション簡単に取得できるとN+1クエリが出ちゃう危険性があるためあえて実装しなかった

■ 7. まとめ

  • 結論:
    • 秒間1万リクエストや1万RPSはLaravelでもいける
    • RDBの負荷をいかに裁くかがこの負荷のところでは一番重要
    • Laravelは結構いい
    • Eloquentを投げ捨ててみるともしかしたら道が開けるかもしれない