Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Unityネットワーク通信の基盤である「RPC」について、意外と知られていないボトルネックと、その対策法

1,644 views

Published on

11月26日開催のプログラマのためのUnity勉強会にて、弊社ミドルウェア事業部部長 安田が講演をさせて頂いたスライドです。
ネットワークゲームを製作する際にご参考頂けますと幸いです。

講演者:安田京人

Published in: Technology
  • Be the first to comment

Unityネットワーク通信の基盤である「RPC」について、意外と知られていないボトルネックと、その対策法

  1. 1. Unityネットワーク通信の基盤である 「RPC」について、 意外と知られていないボトルネック と、その対策法 @プログラマのためのUnity勉強会 2017/11/26 ミドルウェア事業部部長 安田 京人
  2. 2. 1 ■自己紹介 株式会社モノビットは、ゲームとネットワークのテクノロジーをベースに、 あらゆるエンターテインメントコンテンツ制作を行っている、 エンターテインメントテクノロジーベンチャーです。 講演者紹介 <安田 京人(Kyoto Yasuda)> 株式会社モノビットミドルウェア事業部部長 ・IT系企業でSE兼PGとして2年勤務。 ・6年間大手デベロッパーでコンシューマーゲームプログラマーと してアクションゲームを中心に様々なジャンルのゲーム開発に携わ る。 ・オンラインゲームの知識を身に付けるため株式会社モノビットに 入社。現在はモノビットエンジンの開発指揮とエヴァンジェリスト としても活動。 モノビット社紹介
  3. 3. 2 アジェンダ 2. RPC(リモートプロシージャコール) 3. ネットワーク通信における 隠されたボトルネック要因(1) 4. ネットワーク通信における 隠されたボトルネック要因(2) 1. ネットワークのプランニング(設計)について
  4. 4. 3 アジェンダ 2. RPC(リモートプロシージャコール) 3. ネットワーク通信における 隠されたボトルネック要因(1) 4. ネットワーク通信における 隠されたボトルネック要因(2) 1. ネットワークのプランニング(設計)について
  5. 5. 4 最初にどんなことを決めておくべきか? 1. ネットワークシステムの運用形態 C/S型を使う? P2P型を使う? 2. プロトコルの選択 TCPを使う? UDPを使う? 3. 通信ライブラリの選択 UNETを使う? サードパーティ製ミドルウェアを使う? 独自開発する?
  6. 6. 5 ネットワークシステムの運用形態 主な選択要素 C/S P2P 開発難度 クライアントとサーバのプログラムを 別々のプロジェクトで作成することが 多いので、比較的難度が高い ホストとピアのプログラムを 同一のプロジェクトで作成することが 多いので、比較的難度が低い ライフタイム サーバがダウンした時点、 もしくはサーバを提供しない時点で サービスが終了するので ライフタイムは短い クライアント間で接続を確立・維持する 方法が提供可能な限り サービスは維持できるので ライフタイムは長い 負荷分散 サーバ側ですべての負荷を 捌ききることが出来れば、 クライアント間でほぼ均等な 負荷分散が可能 ほぼすべてのゲームロジックが ホスト側で処理されるため、 負荷がホストに偏重しやすい (場合によってはゲームが成立しない) チート対策 サーバサイドに重要なゲームロジックを 実装することができれば チート要因を防ぐことは可能 チートを監視する役割がないため、 少なくともホストがチート行為を 行使した場合に防ぐ手立てがない 基本的にはC/Sが望ましいが、維持費やサービス継続が難しい場合、 P2P/offlineへの移行が比較的容易に可能であるシステムが好ましい。 (最近のソーシャルゲームはその傾向にある。)
  7. 7. 6 プロトコルの選択 主な選択要素 TCP UDP 特徴 エラー再送・順受信・輻輳制御が可能 ストールに伴うラグが発生することも ストールに伴うラグが皆無だが TCPに伴う付加制御がない (RUDPで一部補完できるが、純粋なUDPよりは重い) 回線速度 UDPより遅いとよく言われるが 100ms程度の遅延が許容できれば 十分選択肢足りえる TCPより比較的速い ワールドワイドであれば顕著 速度が問われるならばほぼ一択 使用用途 多量の通信が必要なシステム。 MO/MOBA/MMOなど。 Web(WebSocket)による通信。 低遅延が求められるシステム。 FPS/対戦格闘/レースゲームなど。 アドホックモードの通信。 開発難度 UDPと比べれば比較的楽 TCPと実装方法自体は変わらないが トラブル時のデバッグが超大変! 詳しくは弊社CTOの中嶋編集による、こちらもご覧ください。 ネットワーク ゲームにおけるTCPとUDPの使い分け https://www.slideshare.net/yhonjo/tcpudp-81497235 (「TCP UDP Slideshare」で検索!)
  8. 8. 7 通信ライブラリの選択 主な選択要素 UNET 3rdParty全般 独自開発 運用形態 基本的にP2P カスタムすればC/Sでも 殆どC/S 独自仕様による プロトコル RUDP/WebSocket TCP/UDP/RUDP /WebSocket 独自仕様による 通信品質 ほとんど利用者の 環境に依存 UNETの条件+ オンプレ/クラウドサーバの 設置環境が加わる 独自仕様による 開発サポート Unityのドキュメント 公式フォーラムなど 各社ドキュメント 会社によってはフォーラム 手厚いサポート 自力で何とかしよう C/S→P2P移行であればUNETが最適なはずだが、旧UnityNetwork同様 放置されがち。3rdParty製であればある程度楽に開発できるしサポート も充実しているものが多いが、本当に「最適な通信品質」を目指すなら RPCを独自開発すべき!という「ぶっちゃけ話」をします。
  9. 9. 8 1. ネットワークのプランニング(設計)について アジェンダ 2. RPC(リモートプロシージャコール) 3. ネットワーク通信における 隠されたボトルネック要因(1) 4. ネットワーク通信における 隠されたボトルネック要因(2) 5.まとめ
  10. 10. 9 RPCとは? RPC = Remote Procedure Call の略。遠隔手続き呼び出し。 通信を介して接続された相手の端末上で動作する、特定のプログラムを呼 び出して実行する方法です。原則的には相手の実行プロセスに含まれる特 定のコードを呼び出して、実行します。 ※ Unity/C# では特定のメソッドを呼び出すことになるので、どちらかというと Java にある 「RMI (=Remote Method Invocation)」の呼び名が近いと思いますが、何故か皆さんこぞって 「RPC」と呼んでますので、それに倣います。 Unityでは、公式のUNET を始めとして、 各種サードパーティ製のネットワークライブラリ(弊社製品「MUN」も含 む)では「RPCを主軸に、任意の情報の送受信としてお使いください」と いう前提で提供されています。 よって、 Unityでリアルタイムネットワークプログラム、といったら、 このRPCを利用することと同義といっても過言ではありません。
  11. 11. 10 Unityでリアルタイムネットワークプログラム、 といったらRPCを利用することと同義といっても過言ではあ りません。 …ですが、RPCにはネットワークの通信負荷とは別に、意図 しないボトルネックとなりうる要素が極めて多いという事実 をご存知でしょうか? ということで、ここからちょっとしたDeepDiveです。
  12. 12. 11 RPCのボトルネック要因を説明する前に Unity(ごめんなさいC#前提で…。)において、 RPCがどのようなメカニズムで実行されるかについて簡単に説明すると、 送信側: 1. 以下の要素を用いて API を呼び出して RPC の送信要求。 ・受信側のメソッド名(またはメソッドを識別するためのID) ・送信するデータ本体 2. 上記 1. で設定した値を、バイナリデータへシリアライズする。 3. TCP/UDP を利用して、ネットワーク上に送信。 受信側: 1. TCP/UDP を利用して受信したデータをキューより取得。 2. 上記 1. で取得したデータから、以下のデータへデシリアライズする。 ・受信側のメソッド名(またはメソッドを識別するためのID) ・送信されてきたデータ本体 3. 指定されたメソッドを、送信されてきたデータ本体をパラメータにして コールバックで呼び出す。
  13. 13. 12 RPCのボトルネック要因・その1 以下の赤文字の部分が、通信品質とは関係なしに 「送受信処理においてボトルネックに陥りやすい部分・その1」です。 送信側: 1. 以下の要素を用いて API を呼び出して RPC の送信要求。 ・受信側のメソッド名(またはメソッドを識別するためのID) ・送信するデータ本体 2. 上記 1. で設定した値を、バイナリデータへシリアライズする。 3. TCP/UDP を利用して、ネットワーク上に送信。 受信側: 1. TCP/UDP を利用して受信したデータをキューより取得。 2. 上記 1. で取得したデータから、以下のデータへデシリアライズする。 ・受信側のメソッド名(またはメソッドを識別するためのID) ・送信されてきたデータ本体 3. 指定されたメソッドを、送信されてきたデータ本体をパラメータにして コールバックで呼び出す。
  14. 14. 13 1. ネットワークのプランニング(設計)について アジェンダ 2. RPC(リモートプロシージャコール) 3. ネットワーク通信におけるボトルネック要因 (1) : シリアライズ/デシリアライズ 4. ネットワーク通信における 隠されたボトルネック要因(2) 5.まとめ
  15. 15. 14 シリアライズ/デシリアライズのボトルネック 調査する限りですが、Unity向けとして提供されている通信ライブラリ上の RPC では、シリアライズ/デシリアライズされる送受信データの『型』には 殆ど制約がありません。 制約がない分、シリアライズ/デシリアライズの方式は「単にバイナリデータ に落とし込む」だけでなく、送信データの『型』と『実体』の両方を含んで シリアライズするので、結構な時間を食います。 特に C# であれば object, Hashtable, Dictionary など、ボックス化された 不定形型が、シリアライズにおいて最も厄介です。 ・シリアライズされるデータ(パラメータ)そのものの数が多い ・シリアライズされるデータ型が object, Hashtable, Dictionary など この条件を満たす場合、思わぬ形で負荷が掛かります。 次のページで非常にシンプルな実例コードを示します。
  16. 16. 15 問題:以下のような結果を出力する端末で… using System; using System.Collections.Generic; public class Test { void Start() { Connect(); } void Update() { // RPC x 3 for(int i=0; i<3; ++i) RPC ("OnRPC", ALL, null); } [RPC] void OnRPC() { // Nothing. } } }
  17. 17. 16 問題:左の箇所を右のように書き換えた場合、 DeepProfile結果はどれだけ違うでしょうか? void Start() { Connect(); } void Update() { for(int i=0; i<3; ++i) RPC ("OnRPC", ALL, null); } [RPC] void OnRPC() { // Nothing. } List<Int32> list = new List<Int32>(); void Start() { for (int i = 0; i < 255; ++i) list.Add(new Rand.Next()); Connect(); } void Update() { for(int i=0; i<3; ++i) /* send int[255] */ RPC ("OnRPC", ALL, list.ToArray()); } /* receive object[255] */ [RPC] void OnRPC(params object[] obj) { // Nothing. }
  18. 18. 17 答え:左が変更前、右が変更後
  19. 19. 18 シリアライズ/デシリアライズのボトルネックを 解消するためには? この問題は送信側でRPCでの送信要求を出す前に、任意のデータをバイナリ 配列にするだけで、簡単にボトルネックを解消できます。 どのような通信ライブラリを開発したとしても、最終的に通信データとして TCP/UDP送受信可能なデータストリームは byte[] に限られます。これを可 逆変換するために、通信ライブラリ内部で、かなり負荷の掛かりやすいシリ アライザを実装しているようです。あくまで「弊社調べ」ですが。 ※ お使いの通信ミドルウェアが .NET 4.5 で作成されており、BinaryFormatter などを使って低負荷な可逆 変換をこなしていれば、このあたりの問題が発生しないかもしれません。ですが、気になるようであればシリ アライズ/デシリアライズは自前で用意された方が、最適化されるのは間違いないと思います。 ※ ちなみに弊社的には、旧来の Unity(4.3.4f1以降、またはIL2CPP)にも対応しなければならない目的があ るので、現状は .NET 3.5 ベースでライブラリを提供せざるを得ない、という苦しい事情があります。 任意のデータをバイナリ配列にして送受信する簡単な例を次に触れます。
  20. 20. 19 解消のための組み込み例 void Update() { for (int i=0; i<3; ++i) { List<byte> byteArray = new List<byte>(); foreach (Int32 value in list) { byteArray.AddRange( BitConverter.GetBytes(value)); } RPC("OnRPC", ALL, byteArray.ToArray()); } } [RPC] void OnRPC(byte[] obj) { List<Int32> intList = new List<Int32>(); for (int index = 0; index < obj.Length; index += 4) { intList.Add(BitConverter.ToInt32(obj, index)); } // Anything to do. } .NET 3.5 での事例です。 C#6.0 /.NET 4.5以降であれば、これよりも効率的な組み込みが可能です。
  21. 21. 20 RPCにはもう1つ、ボトルネックになる隠された要因があります。
  22. 22. 21 RPCのボトルネック要因・その2 以下の赤文字の部分が、通信品質とは関係なしに「送受信処理においてボト ルネックに陥りやすい部分・その2」です。 送信側: 1. 以下の要素を用いて API を呼び出して RPC の送信要求。 ・受信側のメソッド名(またはメソッドを識別するためのID) ・送信するデータ本体 2. 上記 1. で設定した値を、バイナリデータへシリアライズする。 3. TCP/UDP を利用して、ネットワーク上に送信。 受信側: 1. TCP/UDP を利用して受信したデータをキューより取得。 2. 上記 1. で取得したデータから、以下のデータへデシリアライズする。 ・受信側のメソッド名(またはメソッドを識別するためのID) ・送信されてきたデータ本体 3. 指定されたメソッドを、送信されてきたデータ本体をパラメータにして コールバックで呼び出す。
  23. 23. 22 1. ネットワークのプランニング(設計)について アジェンダ 2. RPC(リモートプロシージャコール) 3. ネットワーク通信におけるボトルネック要因 (1) : シリアライズ/デシリアライズ 4. ネットワーク通信におけるボトルネック要因 (2) : RPCの受信コールバック 5.まとめ
  24. 24. 23 RPCの受信コールバックのボトルネック Unity(C#)における、RPC受信メソッドのコールバックに関する一般的な実 装法は下記の通りです。 1. Instantiateが実行されたときに、同期対象のオブジェクトコンポーネ ントに登録されているスクリプト内に、RPC受信メソッドのコールバッ クメソッドがあった場合、それをリストに登録する。 2. RPCの受信メソッドが実行されたときに、1.で登録されているリストの うち、コールされたスクリプトを持つオブジェクトで、かつメソッド名 が同一であった場合、そのメソッドを引数付きでコール。 1.はオブジェクト生成時に実行されるので「オブジェクトを読み込むとき のスパイク」程度の負荷にはなりませんが、2.については負荷の掛かりや すい部分です。特に「instantiate済みのオブジェクトの実数が多い」「 RPC受信メソッドの候補数が多い」場合、この負荷が無視できないレベル になります。
  25. 25. 24 単純にこれだけで重くなります public class Test { /* 10000個のRPCメソッドを実装 */ [RPC] void OnRPC_0000() { // Nothing. } [RPC] void OnRPC_0001() { // Nothing. } … [RPC] void OnRPC_9999() { // Nothing. } void Update() { RPC ("OnRPC_0001", ALL, null); } }
  26. 26. 25 RPCの受信コールバックのボトルネックを 解消するためには? とにかく「instantiate済みのオブジェクトの実数を少なくする」「RPC受信 メソッドの候補数を少なくする」以外に明確な対策はありません。 ただし、「解消努力」は開発者レベルでも可能です。 ・目的別にRPCを量産せず、1つのオブジェクトに関する全ての受信は 1つのRPCメソッド内に収めておくこと ・どうしても「1つのオブジェクトにあらゆる目的のRPCを送信したい」 場合には無理せず、 1. 送信側で、処理目的を示す定数を指定 2. 受信側で、処理目的に応じて分岐 という組み込みを実践する
  27. 27. 26 目的別にRPCを量産したい場合の代替策 public class Test { /* OnRPC_0000() ~ OnRPC_9999() には [RPC] を付けない! */ void OnRPC_0000() { … } void OnRPC_0001() { … } void OnRPC_0002() { … } … void OnRPC_9999() { … } [RPC] void OnRPC(UInt32 type) { switch(type) { case 0: { OnRPC_0000(); } break; case 1: { OnRPC_0001(); } break; case 2: { OnRPC_0002(); } break; .... case 9999: { OnRPC_9999(); } break; } } void Update() { RPC (“OnRPC”, ALL, 1); /* OnRPC_0001() をコール */ } }
  28. 28. 27 1. ネットワークのプランニング(設計)について アジェンダ 2. RPC(リモートプロシージャコール) 3. ネットワーク通信におけるボトルネック要因 (1) : シリアライズ/デシリアライズ 4. ネットワーク通信におけるボトルネック要因 (2) : RPCの受信コールバック 5. まとめ
  29. 29. 28 RPCは便利なようで実は不便、というお話でした。 通信負荷とは別のところでボトルネックが発生しやすいので、通信ミドルウ ェアを提供する会社としては「RPCを推奨しつつも、実のところRPCによっ て快適な通信速度を出すことは、利用者側の都合によっては不可能に近い」 という事実を露呈せざるを得ません。 開発者の方々には、「通信ミドルウェアの一般的な仕様」を理解していただ き、かつ「ボトルネックを回避するためにどうすればいいか」というのをで きるだけ提供できればいいと常々思ってはいるのですが、ケースバイケース な部分もあって、完璧な実装法をお伝え出来ずに苦悶する毎日です。 ということで、ネットワークを利用した最速レスポンスに拘る場合、RPCは おろか、通信ミドルウェアに頼らず、独自実装してみることが一番良いのか もしれません。満足できなくなったらレッツチャレンジです。 ■まとめ

×