3月24日、サイバーエージェントのエンジニア・クリエイターによる技術カンファレンス「CyberAgent Developer Conference2022」を開催しました。本記事では、「スマホゲーム業界におけるPHPの歴史とLaravel Octaneで広がるこれからのPHP」の様子をお届けします。
目次
■サイバーエージェントグループのゲーム事業の歴史とPHP
■PHPで培ったもの
■多様化するゲームの要件とサイバーエージェントグループでの事例
■PHPの変革「Swoole」「Laravel Octane」の登場
■Laravel Octane Deep Dive
■まとめ
■サイバーエージェントグループのゲーム事業の歴史とPHP
まずはサイバーエージェントグループのゲーム事業の歴史とPHPについて振り返ります。サイバーエージェントグループでは2009年からゲーム事業に参入しており、2010年はソーシャルアプリ黎明期と位置づけられる時期でした。
当時のソーシャルアプリはガラケーでした。ガラケーは情報量が少なく、基本的にボタンを押すたびにリクエストが走るので、サーバーの負荷が非常に大きい環境でした。当初はどれだけの負荷対策が必要か分からないまま、相当な負荷をさばいていました。
当時サイバーエージェントグループが提供していたアプリは『星空バータウン』『ドリームプロデューサー』『コーデマニア』などです。
ここで紹介したタイトルは全てPHPで動いていました。ほかの言語も選択肢にはありましたが、PHPのガラケー対応ライブラリが充実していたので、開発スピードを加味してPHPが採用されました。今ではクラウドに移行していますが、当時のサーバーはオンプレでした。PHPのフレームワークはCakePHP、puppetでサーバー構築、デプロイはCapistrano、データベースのマイグレーションはRakeなど、ほかの言語でも使われるようなものを使用していました。全てに導入できていたわけではなく、プロジェクトにより差異がありました。
2012年から13年頃は、ブラウザソーシャルアプリ後期になります。スマホが登場し、スマホで提供するブラウザアプリとネイティブアプリ開発が並行しました。サーバーはオンプレからクラウドになります。速いフレームワークが良いので、CodeIgniter、デプロイはshellに戻るなど試行錯誤がありました。
2013年から14年は転換期でした。子会社の1つが開発言語をPHPからNode.jsに変え、時代に合わせた言語に切り替えるチャレンジをしました。言語切り替えと同時に、テストドリブン開発、テストの自動化、デプロイの自動化、マイグレーション管理、マージリクエストによるコードレビューなどを同時に導入しました。
2015年から2018年は安定期です。アプリはネイティブアプリになりました。基本的に開発はあまり変わらず、適材適所で言語を使い分けており、APIのロジック部分はPHP、リアルタイム通信(チャットやマルチバトル)ではNode.jsもしくはSaaSを利用していました。
2018年から現在は成熟期です。AndroidとiOSはもちろん、マルチプラットフォーム化が進みPCにも配信する動きが出てきました。そのため、PHP一辺倒ではなく、Go言語を採択するプロジェクトも増えました。これは通信プロトコルをgRPCにするため、モダンな言語とセットで切り替えました。友達と遊ぶ体験を提供するためにリアルタイム通信の要件が多く、SaaSを使うことが多いです。
ゲーム開発ではデータ量に悩まされることが多くあります。MySQLをよく使っていましたが、シャーディングの煩雑さから解放されるために、Cloud Spannerの選択肢も増えてきました。またユーザーのリクエストが本当に多いです。正規ユーザーのリクエストなら大歓迎ですが、リセマラやbotに悩まされることも多々あります。イタチごっこですが対策をしながら運用しています。
■PHPで培ったもの
ざっと歴史を振り返りましたが、12年間開発運用した経験はかけがえのないものです。開発時、設計時、開発フロー、全てインフラ構築も含めてノウハウが培われ、現在の開発につながっています。
ゲーム開発は、1回作ったら終わりではありません。実際に運用9年目のプロジェクトでは、9年前のソースをいまだにメンテナンスしています。ゲーム運用はソースに手を入ることが多く、下手すると毎日APIのリリースがあります。運用が続く限り、開発が続くのです。
また開発前には想像できなかったことも起こります。同一ユーザーから同じリクエストが違うサーバーにほぼ同時に届き、通信経路などの関係で処理の追い越しが起きてバグが発生したことがあります。シーケンス図にあるように、最初にAの処理が流れましたが、A’の処理が次に実行さて、先にA’が終了してしまいました。Aが後に終わり、A’の処理がなかったことになり、数字がバグりました。こんなことが起きてしまうのがゲームの負荷やゲームの運用です。
そうした教訓もあり、追い越しがないように必ずユーザーのロック(例えばユーザーテーブルをロック)を更新ロジックに入れるようにしています。
ゲームの開発の基本的な考え方はWebサービスと同じです。トランザクション設計は大事ですし、負荷対策も必要です。セキュリティ対策も当然しなければなりません。違いを強いて挙げるなら、近年は大きいタイトル開発が多く、データ量に悩まされることです。1個のテーブルに1億レコードほどが普通に入るので、1テーブルあたりにレコード数が多くならないように設計したり、シャーディングしたり、当たり前のようにやる必要があります。
■多様化するゲームの要件とサイバーエージェントグループでの事例
ここからはPHPのこれからについてお話しします。近年は多様なジャンルのゲームが出てきています。多様化するゲームの要件とその事例についてご紹介します。
1.遅延が許されないケース
例えばバトルロイヤルゲームでは、フィールドにいるデータをユーザー間で同期するなど、基本的には遅延が許されないような環境です。敵に攻撃を与えたのに死んでいなくて、死んだ判定が遅いがゆえに反撃を食らったとなると、ユーザーはがっかりしてしまいます。
2.大規模な同時接続数に耐えうるアーキテクチャが必要なケース
『プロジェクトセカイ カラフルステージ! feat. 初音ミク』は、同時視聴数が10万人に及ぶようなバーチャルライブを提供しています。、同時接続数が増えてもスケールするようなアーキテクチャを組む必要があります。
3.素早いリアルタイムマッチングが必要なケース
過去に提供していたスマートフォン向け360°空中対戦アクションゲーム『Kick-Flight』では、4対4の2チームに分かれてゲームが行われます。マッチング画面で次々にユーザーがマッチングされ、8人そろったらゲームがスタートする使用なのですが、チーム分け以外にも、プレーヤースキルを揃えたり、キャラクターを被らないようにするなど、多くの条件がある中、ユーザーのストレスにならないよう、短時間でマッチングを成立させないといけません。
『Kick-Flight』ではその要件を満たすために、Open Matchというオープンソースを利用しました。Open Matchのマッチング部分はMatch Making Functionで、その部分はPHPで書いてあります。ほかはGoやSaaSですが、コアはPHPです。
■PHPの変革「Swoole」「Laravel Octane」の登場
ここからは近年PHPで起きている変化「Swoole」と「Laravel Octane」の登場についてご紹介します。
ゲーム要件が多様化する中、PHPだと実現できないと思っていたことがいくつかあります。その代表的な3つが、非同期処理、WebSocketサーバー、gRPCサーバーです。それぞれ一応の解決策はあるものの、gRPCサーバーはまだ完全解決には至らず、有名なGoogleグループのやりとりでは「PHPだと無理ではないか」言及され、実現は難しいものだと思われていました。
そこで登場したのが「Swoole」です。これはイベント駆動型の非同期・コルーチン型並列処理ネットワーク通信エンジンです。簡単に言うと、非同期処理や並行処理がお手軽に書けるライブラリです。これによりPHP単体でHTTP/2サーバーやWebSocketサーバーが提供可能になるという恩恵があります。そのため、出来ないと思われていたWebSocketと非同期処理は「Swoole」で実現できるようになりました。
「Swoole」は普及するまでに2〜3年の月日を要しましたが、このタイミングで出てきたのが「Laravel Octane」です。公式ドキュメントから引用すると「Laravel OctaneはSwooleやRoadRunnerなどの高性能なアプリケーションサーバーを使用し、アプリケーションを提供することで、アプリケーションのパフォーマンスを向上させます。Octaneはアプリケーションを一度起動したら、メモリ内に保持し、そして超高速でリクエストを送り返します」となります。簡単に言うと「Swoole」をLaravelで使えるところが最大のポイントですが、最も人気が高いフレームワークLaravelで「Swoole」が使えるようになったのが「Laravel Octane」登場の意義だと思います。
これでPHPで出来ないことはかなり減りました。PHPでgRPCサーバーを実現するのは難しいと前述しましたが、最近日本人の方が「サーバーサイドストリーミングのRPCを実現した」と投稿されているのを見かけました。実際にカンファレンス発表時の話を聞いたところ、Server Streaming RPCをAMPというPHPのフレームワークを用いて実現しているそうです。PHPでgRPCのストリーミングを提供する仕組みが編み出されました。
PHPで出来ないと思われていた、非同期処理、WebSocketサーバー、gRPCサーバーは全て実現出来るようになり、PHPの可能性がだんだん見えてきました。
■Laravel Octane Deep Dive
ここからは「Laravel Octane」をもう少し深掘りします。まずは「Laravel Octane」の仕組みから。これまでPHPと言えばNGINXとPHP-FPMが多かったと思います。リクエストが届いたときに、実行されるPHPファイルを探して、メモリー上にロードして、処理を実行する。これをリクエストごとに行うのが辛いところです。それを「Laravel Octane」が解消するのですが、結構無駄が多いとも言えます。
「Laravel Octane」はWebサーバーになれるので、NGINXなどが必要なくなり、直接リクエストを受け付けることができます。起動処理ではファイルを探して読み込む必要はありますが、起動処理以降は基本的にはメモリー上に全てのアプリケーション処理が載っているので、CPUはメモリー上の内容に従い処理を実行すればいい状態です。そのためアプリケーションが高速に動作できます。
実際の仕組みです。OctaneにはManagerとWorkerがあり、Managerがリクエストを受け付けて、Workerに処理を委譲しています。Workerを複数並べることでスケールしたり、CPUを無駄なく使えます。プロセスツリーを見ると、ここではManagerからWorkerが4つ起動されています。こんな形で大量のリクエストを素早くさばけるようなアーキテクチャにしています。
Workerに着目しましょう。基本的には起動したままですが、それでは弊害もあるので「Laravel Octane」はリクエストが来たらサンドボックスコンテナをクローンしています。ほかのリクエストからの汚染を防ぐためです。処理終了後には次のリクエストに影響がないように、リクエストが終わるとサンドボックスコンテナを空にして、使用しなくなったオブジェクトを空にしています。それ以外にもクリーンアップ処理が必要なので、Octaneはlistenerにクリーンアップ処理を登録しています。ここではprepareApplicationForNextOperation関数を呼び、クッキー、セッション、認証情報など他人と共有できないものをクリーンアップしています。
「Laravel Octane」を実運用環境に導入した事例を深掘りします。今回はキャンペーンサイトで「Laravel Octane」を使用しました。通常のLaravelとは違うことをご紹介します。
大事なのがクリーンアップ処理です。ただし全てクリーンな状態でロジックが走るとは限りません。アプリケーションコンテナに格納されたRouting情報はクリアされないためです。Closureとして登録しないと、Worker内でインスタンス化されたControllerが再利用されてしまいます。Controllerの中でコンストラクタを呼び出したいので、Closure呼出しをして、Controllerをnewするようなロジックを書いておく必要があります。例えばランダムな数字を表示するところで、同じインスタンスが使い回されてしまうと、毎回同じ数字が表示されてしまうというバグが引き起こされてしまいます。
実際のソースを見ていきます。まずは起動処理。「Laravel Octane」を起動するときはartisanコマンドでOctane:startで実行します。今回はSwooleを使うので、サーバーのタイプにswooleを使います。そうするとstartSwooleServerが呼ばれます。Octane側のコマンドはoctane:swooleが呼ばれ、この後にswoole-serverというPHPスクリプトがProcess経由で呼ばれます。これでswoole-serverの中でWorkerが動き出します。OnWorkerStartの処理でWorkerがnewされて、実際のリクエストが処理されます。これでoctane:startからWorkerが待ち受け状態になるとこまで確認できました。
Controllerのコンストラクタが呼ばれる時を見ていきます。swoole-serverが呼ばれるとWorkerのhandleにリクエストが渡されます。Workerの処理を順に見ると、WorkerからApplicationGatawayが呼ばれ、ApplicationGatawayからKernelが呼ばれ、Kernelの中でリクエストが引き回され、sendRequestThroughRouterからdispatchToRouterが呼ばれていきます。
その後にRouterのdispatchが呼ばれ、runRouteやRoute関連の処理が呼ばれます。Routeの中のgatherMiddlewareで、$this->computedMiddlewareがあればリターンしてて、なければ処理が走ります。この部分が先ほどのコンストラクタが呼ばれるか呼ばれないかの境目になります。
実際にWorkerがリクエストを待ち受ける仕組みで大事になるのは、このRouteのcomputedMiddlewareに値が保持されてるかどうかです。ソースから確認したとおり、1回しか呼ばれませんのでControllerが使い回されます。Controllerの部分しか掘り下げられませんでしたが「Laravel Octane」開発でここがつまずくポイントだと思います。
「Laravel Octane」はPHP-FPMに比べて高速に処理できるアーキテクチャです。Workerがキーポイントで、Controllerのコンストラクタは初回リクエストしか実行されないことにご注意ください。それを回避するために、Controllerをnewするようなロジックに組んでおく必要があります。
■まとめ
ゲーム制作におけるサーバーの要件は非常に多様化してきています。「PHPではできない」と思われていたものも「Laravel Octane」のような新しい仕組みを採り入れることで解決しつつあります。まだまだPHPはゲーム開発においても楽しめるということを、皆さんにお伝えできていれば幸いです。
「CyberAgent Developer Conference 2022」のアーカイブ動画・登壇資料は公式サイトにて公開しています。ぜひご覧ください。
https://cadc.cyberagent.co.jp/2022/
■採用情報
新卒採用:https://www.cyberagent.co.jp/careers/special/students/tech/?ver=2023-1.0.0
キャリア採用:https://www.cyberagent.co.jp/careers/professional/