秒間 10,000 リクエストを "簡単に"いなすゲームサーバーを Laravel で作る設計
by
Masaru Yamagishi
Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
Slide 1
Slide 1 text
秒間 10,000 リクエストを "簡単に" いなすゲームサーバーを Laravel で作る設計 やまゆ - PHPカンファレンス福岡2023
Slide 2
Slide 2 text
こんにちはー - 写真撮影/SNS投稿大歓迎! - 後でエゴサして全部見させていただきます 👀 - #phpconfuk #hall_fu - 初九州・初福岡です! - Ask the speaker お待ちしています!
Slide 3
Slide 3 text
概要 1秒間に PHP が受信する HTTP リクエストが最大 10,000 回以上——— そんな世界が存在します。その一つが 「ソーシャルゲーム」 です。メンテナンスが明けた瞬間、イベントが始まった・終わ る瞬間、様々なタイミングでゲームサーバーは瞬間的に高負荷になります。もちろん、サービスをリリースし PR をたくさん 出し始めたその瞬間が、プロジェクトで最も高負荷となるでしょう。それらに耐えうるサーバー構成が求められています が、「リリース直後にサーバーがダウンした」「限定イベントが始まったらすぐ緊急メンテナンスが始まった」という話はちょ くちょく聞こえてきます。その 瞬間的な高負荷(いわゆる "スパイク") に耐えるには、事前準備を怠らないことが重要です。 ソーシャルゲームにおいては、他の Web アプリケーションに比べ 書き込みヘビーなワークロード であることが多いで す。読み込みは比較的簡単に分散できますが、書き込みを分散することは容易ではありません。 そういった要件を達成するため、私のチームで行っている、 Laravel による高負荷に耐えるサーバー設計をご紹介したい と思います。 - 高負荷案件でも Laravel は使えるの?と疑問に思う方 - どのようにスパイクをさばいているのかを知りたい方
Slide 4
Slide 4 text
1 リクエスト クライアント サーバ
Slide 5
Slide 5 text
1 リクエスト HTTP Req コンニチ ハ クライアント サーバ
Slide 6
Slide 6 text
1 リクエスト コンニチ ハ クライアント サーバ コンニチ ハ HTTP Req HTTP Res
Slide 7
Slide 7 text
1 リクエスト ワーイ クライアント サーバ ヤッター HTTP Req HTTP Res Done!
Slide 8
Slide 8 text
秒間 1 リクエスト クライアント サーバ
Slide 9
Slide 9 text
秒間 1 リクエスト クライアント サーバ HTTP Req コンニチ ハ
Slide 10
Slide 10 text
秒間 1 リクエスト クライアント サーバ HTTP Req コンニチ ハ HTTP Res コンニチ ハ
Slide 11
Slide 11 text
秒間 1 リクエスト クライアント サーバ HTTP Req アリガト イエイエ HTTP Res Done!
Slide 12
Slide 12 text
秒間 1 リクエスト クライアント サーバ HTTP Req HTTP Res Done! — 約 1 秒後 —
Slide 13
Slide 13 text
秒間 1 リクエスト クライアント サーバ HTTP Req HTTP Res Done! — 約 1 秒後 — HTTP Req マタキタ ヨ
Slide 14
Slide 14 text
秒間 1 リクエスト クライアント サーバ HTTP Req HTTP Res Done! — 約 1 秒後 — HTTP Req マタキタ ヨ HTTP Res ソウナノ
Slide 15
Slide 15 text
秒間 1 リクエスト クライアント サーバ HTTP Req HTTP Res Done! — 約 1 秒後 — HTTP Req マタクル ネ HTTP Res アイヨ Done!
Slide 16
Slide 16 text
秒間 1 リクエスト クライアント サーバ HTTP Req HTTP Res Done! — 約 1 秒後 — HTTP Req HTTP Res Done! 以下続く...
Slide 17
Slide 17 text
クライアント サーバ 秒間 10 リクエスト
Slide 18
Slide 18 text
クライアント サーバ 秒間 10 リクエスト HTTP Req x 10 コンニチ ハ コンニチ ハ コンニチ ハ コンニチ ハ コンニチ ハ コンニチ ハ コンニチ ハ コンニチ ハ コンニチ ハ コンニチ ハ
Slide 19
Slide 19 text
サーバ 秒間 10 リクエスト HTTP Req x 10 HTTP Res x 10 ワア クライアント アリガト オオキニ タスカル ヤッタ セヤナ オケ ワーイ ドウモ アザス ウィッス
Slide 20
Slide 20 text
クライアント サーバ 秒間 10 リクエスト HTTP Req x 10 クレ データ ホシイ ヨコセ マダー ハヨ ナニシテ ン ツギノ コレ ヨロシク HTTP Res x 10 マタキタ Done! HTTP Req x 10
Slide 21
Slide 21 text
クライアント サーバ 秒間 10 リクエスト HTTP Req x 10 ヨイショ ヤッタ ドモ ザッス ワーイ マタクル ネ ヨロシク コレダ フム サラバ HTTP Res x 10 イソガシ Done! HTTP Req x 10 HTTP Res x 10
Slide 22
Slide 22 text
クライアント サーバ 秒間 10 リクエスト HTTP Req x 10 ヨイショ ヤッタ ドモ ザッス ワーイ マタクル ネ ヨロシク コレダ フム サラバ HTTP Res x 10 ヒエー Done! HTTP Req x 10 HTTP Res x 10 以下続く... Done!
Slide 23
Slide 23 text
クライアント サーバ 秒間 100 リクエスト オオイナ HTTP Req x 100
Slide 24
Slide 24 text
クライアント サーバ 秒間 1,000 リクエスト ヒャー HTTP Req x 1,000 x10
Slide 25
Slide 25 text
クライアント サーバ 秒間 10,000 リクエスト (x x) HTTP Req x 10,000 x100
Slide 26
Slide 26 text
秒間 10,000 リクエスト(rps) の負荷って? - 約 10,000 台のクライアントが一斉にリクエストし始めること - その状態が「ずっと続く」こと - クライアント 1 台と比べて 10,000 倍の負荷がかかること ※以下リクエスト・パー・セカンド「rps」という
Slide 27
Slide 27 text
こんにちは。 やまゆです。
Slide 28
Slide 28 text
自称赤魔道士系エンジニア ㈱インフィニットループ やまゆ この画像は自撮りでも いつも使っているアイコンでも構いません ☕
Slide 29
Slide 29 text
最近の主な業務内容 - AWS インフラストラクチャアーキテクト - PHP アプリケーションアーキテクト・実装 - インフラ管理ツール実装 - 初級者・中級者向け教育 - 社内・社外向け広報
Slide 30
Slide 30 text
本日のお題 - ゲームサーバーとは? - 負荷をいなすとは? - 負荷をいなすアプリ設計は? - Laravel いけんの?
Slide 31
Slide 31 text
本日の NOT お題 - インフラ構築の仕方は? - 負荷試験のやり方は?
Slide 32
Slide 32 text
ゲームサーバとは
Slide 33
Slide 33 text
ゲームサーバーとは ※ここでいうゲームとは、スマートフォン向けのソーシャルゲームとする ※リアルタイムサーバーは対象外とする - HTTP(S) 通信サーバー - 非常に大規模かつ素早くスケール - データベースへの高頻度な書きこみ - それ以外は案外と普通の Web サービスとあまり変わらない
Slide 34
Slide 34 text
HTTP(S) 通信サーバー (例) HTTP(S) json/protobuf/… OpenAPI 3.0/gRPC/…
Slide 35
Slide 35 text
HTTP(S) 通信サーバー 現状最も安定してスケールする通信プロトコル json や protobuf のような構造化されたペイロードを送受信する サーバーとクライアントは別会社が担当することもある ペイロード構造の共有が重要 弊社では様々な形式で自動生成しているが、うちは OpenAPI 3.0 を利用 OpenAPI 3.1 では Webhook にも対応 AsyncAPI https://www.asyncapi.com/ というのも登場した
Slide 36
Slide 36 text
非常に大規模かつ素早くスケール 例) リリース直後 事前にスケールアウト 大量のアクセス
Slide 37
Slide 37 text
非常に大規模かつ素早くスケール 例) リリース直後 事前登録によりある程度の負荷を予測し、それを超えても大丈夫な程度に大きくス ケールアウト ここでコケると離脱がかなり増えるので、費用はかさむがびっくりするくらい台数増や すことがある(Web数百台とか) 負荷試験の段階で Web サーバーや DB サーバー以外の部分で詰まらないかしっか り確認する必要がある
Slide 38
Slide 38 text
非常に大規模かつ素早くスケール 例) 平時 柔軟にスケール 波のあるアクセス
Slide 39
Slide 39 text
非常に大規模かつ素早くスケール 例) 平時 定常的に同じ負荷ではなく、大体一日のプレイサイクルがあるので波が出る ソシャゲだと朝・昼・夕方が負荷高め リソースに遊びがあるとただ費用を垂れ流すだけなので柔軟にスケールする必要が ある 最近だとサーバーレス化してこのスケールを自動化している場合も増えているのでは
Slide 40
Slide 40 text
非常に大規模かつ素早くスケール 例) 人気イベント開始・終了前後 アクセススパイク 注意してスケールアウト
Slide 41
Slide 41 text
非常に大規模かつ素早くスケール 例) 人気イベント開始・終了前後 人気イベントの開始時は急激にスパイクする イベントの人気度にもよるが、事前にスケジュールでスケールアウトしておくことで サーバー高負荷を避ける どれくらいの DAU が出るかを予測するのは半分職人芸 また、イベント終了直前は最後に走る人も多いので高負荷が続く
Slide 42
Slide 42 text
非常に大規模かつ素早くスケール 例) 長く運用していき縮退 段々減るアクセス スケールイン
Slide 43
Slide 43 text
非常に大規模かつ素早くスケール 例) 長く運用していき縮退 運用が数年続くとどうしてもユーザー数は減る そうすると負荷の波も小さくなっていくのでそれに合わせて台数を減らしく必要がある 計画メンテナンスに入れて DB サーバーなど一旦止めないと動かせないリソースをス ケールダウンしたりもする
Slide 44
Slide 44 text
データベースへの高頻度な書き込み コインを使ってアイテムを購入 ゲームをプレイして報酬を獲得 フレンドの申請 プロフィールの編集 ガチャの実行 …
Slide 45
Slide 45 text
データベースへの高頻度な書き込み ソーシャルゲームではとにかくユーザーの操作によって、その人が持っているリソー スの更新が多い DB は読み込みはスケールしやすいが書き込みはスケールしづらい トランザクションを張ったりするので単体の負荷も読み込みより高い
Slide 46
Slide 46 text
負荷をいなす?
Slide 47
Slide 47 text
負荷をいなす means いなせる負荷を かけることができる
Slide 48
Slide 48 text
負荷試験 多くのユーザーが想定されるサービスでは負荷試験が欠かせない 10,000 rps をいなすには、 10,000 rps の負荷をかけられる必要がある 実際の負荷試験の準備・実施・レポートのやり方については、後日フォローアップ記 事をブログに投稿予定。お楽しみに!
Slide 49
Slide 49 text
結果: RDB が重い!
Slide 50
Slide 50 text
書き込み DB の CPU がカツカツ
Slide 51
Slide 51 text
書き込み DB の CPU がカツカツ 今回は Amazon Aurora を使ったが、これは CPU 80 % 以上いくと急に不安定になる 傾向がある 70 % ならまだ安定稼働するが、これ以上の負荷は厳しい まだまだスケールアップは可能だが、アップには限度があるし何より費用がかさむ
Slide 52
Slide 52 text
コネクション数には限界がある
Slide 53
Slide 53 text
コネクション数には限界がある MySQL の仕様上 max_connections は 1 台あたり 16,000 が限界 今回 10,000 rps で 9,000 ということは、 20,000 rps は耐えられないということ RDS Proxy で改善されるかもしれない? NewSQL も選択肢?
Slide 54
Slide 54 text
Laravel は正直問題ない コストパフォーマンスを考えると他言語/FWに劣る可能性はあるが、別に安定してス ケールするのでフレームワークレベルで詰まることはなかった
Slide 55
Slide 55 text
結論:クエリ最適化と コネクション数削減が 実装において 最重要である
Slide 56
Slide 56 text
アプリ設計の要 - クエリ最適化が容易であること - 発行されるクエリが明快であること - ドメイン実装とクエリ最適化を分離して作業できること - ドメインが大規模になってもスケールすること - 疎結合であること - どこで何が実装されているかわかること - 運用を考えて、依存関係のアップデートが容易であること - Framework 等のバージョンアップがしやすいこと
Slide 57
Slide 57 text
実際のアプリ設計
Slide 58
Slide 58 text
DDD-like Driven Design
Slide 59
Slide 59 text
なにそれ
Slide 60
Slide 60 text
DDD-like Driven Design このスライドを作りながら提唱(?) 軽量 DDD と呼ばれる奴に近い 要約:「DDD 全部は難しいので一部良さ気なとこだけ取り入れる」 さらに要約:「アーキテクトの私がわからん奴はメンバーもわからんくなるやろ」 ※私の DDD の見識は超絶浅いので全然違うこと書いている可能性も
Slide 61
Slide 61 text
DDD-like Driven Design の三つの原則 1. Laravel Frameworkは利用するが、 Framework とドメイン実装は分離すること 2. 責務がネームスペースごとに分離されていること 3. ネームスペースごとの依存関係が明らかで、 片方向になっていること
Slide 62
Slide 62 text
弊プロジェクトの基本設計図
Slide 63
Slide 63 text
エントリポイントレイヤー
Slide 64
Slide 64 text
CQRS サービスレイヤー
Slide 65
Slide 65 text
CQRS? コマンド・クエリ責務分離のこと 「データの更新(Insert/Update/Delete)」と「データの取得(Select)」は要件が大きく 異なるから分けようぜ 更新コマンド -> 低頻度、トランザクションの管理、安全にデータを更新する 取得クエリ -> 高頻度、最適化された SQL を発行して取得する
Slide 66
Slide 66 text
ドメインレイヤー
Slide 67
Slide 67 text
ドメイン アプリケーションの本質的な実装部分 use Illuminate\...; と書いたら処される ※ Illuminate\Support ネームスペースは除く Domain から Domain 外部方向への依存は基本的に NG 必要であればインターフェースを定義して別の場所で実装
Slide 68
Slide 68 text
エンティティ 一意に識別できる識別子を持ったオブジェクト 大体テーブルと 1対1(UserEntity, UserEquipmentEntity, …) getter/setter が用意され、プロパティの更新が可能 ここにロジックを書くのが望ましいがあんまり書かれていない
Slide 69
Slide 69 text
値オブジェクト Life や Stamina など、複雑なロジック(例えば時間で回復とか)を持つ値を一つのオ ブジェクトとして表現する手法 immutable なのが特徴的 あまり使われていない。 Identity(AUTO_INCREMENT の値オブジェクト)とか作った方 が良かったと後悔
Slide 70
Slide 70 text
ドメインサービス 複数のエンティティや値オブジェクトをまたいで何かしらの更新を行うロジックの集合 ドメインサービスと呼ばれることは少ない そしてあまり使われていない... 本当はエンティティに書いた方が良いものではある
Slide 71
Slide 71 text
リポジトリインターフェース リレーショナルデータベースのクエリ発行を担当する部分 クエリ発行はもちろん具体的な実装依存が必要なので、実装自体は別の場所で Domain 内ではインターフェースのみを定義している
Slide 72
Slide 72 text
その他インターフェース Domain の実装の中で S3 にアクセスしたい、 Laravel のこの機能を使いたい、などと いった場合は、 Domain で Laravel や S3 を参照するのではなく、単純に欲しいイン ターフェースを定義するだけとする 具体的に接続したりリクエストを送ったりするのは Domain の本質ではないので「イン フラストラクチャ」に分離する ※クリーンアーキテクチャっぽい考え方でもある
Slide 73
Slide 73 text
インフラストラクチャレイヤー
Slide 74
Slide 74 text
インフラストラクチャレイヤー 外部(=ライブラリ、DB、S3、...)と Domain をつなぐ架け橋を担当 クエリの発行、 HTTP リクエストの送受信などは全てここの中で行う Domain で定義したインターフェースを実装する ※名前が長いのでやめたいけど他に良いのが思いつかなかった
Slide 75
Slide 75 text
インフラストラクチャレイヤー 外部(=ライブラリ、DB、S3、...)と Domain をつなぐ架け橋を担当 クエリの発行、 HTTP リクエストの送受信などは全てここの中で行う Domain で定義したインターフェースを実装する ※名前が長いのでやめたいけど他に良いのが思いつかなかった
Slide 76
Slide 76 text
インフラストラクチャレイヤー 外部(=ライブラリ、DB、S3、...)と Domain をつなぐ架け橋を担当 クエリの発行、 HTTP リクエストの送受信などは全てここの中で行う Domain で定義したインターフェースを実装する ※名前が長いのでやめたいけど他に良いのが思いつかなかった そういえば、 Eloquent さんは?
Slide 77
Slide 77 text
No content
Slide 78
Slide 78 text
例えば
Slide 79
Slide 79 text
Eloquent を 例えば
Slide 80
Slide 80 text
Eloquent を 投げ捨てる 例えば
Slide 81
Slide 81 text
※諸注意 大前提として、 Eloquent は Laravel が誇る素晴らしい機能なのは確実です。 Eloquent を使うことで生産性が大きく向上することは間違いありません。 今回の話は、いくつかの課題点からどうしても「使うのが難しい」と判断した結果にな ります。 Eloquent が悪いのではなく、今回は偶然相性が悪かった場合の話をします。
Slide 82
Slide 82 text
Why? - Active Record 型 - 初期実装 - シャーディング
Slide 83
Slide 83 text
Why?: Active Record 型であること Eloquent は Active Record 型 ORM 大規模アプリケーションにおいて Active Record を適切に運用するのは難しい(もちろ ん不可能ではない) 今回の DDD-like DD において大きな課題となった 対照となる Data Mapper 型 ORM の必要性が高まった
Slide 84
Slide 84 text
Why?: 初期実装 初期のプロトタイプ実装に illuminate/database のクエリビルダ(とそれに紐づく illuminate/collection)を採用した それらを活かすため、 Doctrine や Cycle ORM など他の Data Mapper 型 ORM の 採用は難しかった
Slide 85
Slide 85 text
Why?: シャーディング Eloquent 自体には水平シャーディングの機能がない global log user2 user1 user3 user n … 〇 ×
Slide 86
Slide 86 text
じゃあどうやってクエ リ作るの?
Slide 87
Slide 87 text
自作 ORM で。
Slide 88
Slide 88 text
自作 ORM
Slide 89
Slide 89 text
自作 ORM
Slide 90
Slide 90 text
自作 ORM
Slide 91
Slide 91 text
Why? リレーションを取得出来る ≒ N+1 クエリの危険が出る 幸いこのプロジェクトでは大量のリレーションを管理することが少なかった (最初うまい設計が思い浮かばなかった)
Slide 92
Slide 92 text
まとめ - 秒間 10,000 リクエストをさばくのは Laravel でも安定していける - RDB 負荷をいかに捌くかがキモ - DDD-like DD は結構良いぞ - 例えば Eloquent を投げ捨ててみると道が開けるかも インフィニットループでは、札幌・仙台で働くエンジニアを募集中! ご清聴ありがとうございました!