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 を投げ捨ててみると道が開けるかも インフィニットループでは、札幌・仙台で働くエンジニアを募集中! ご清聴ありがとうございました!