MagicOnion v4 - .NET 5 と ASP .NET Core gRPC対応への進化

CysharpからMagicOnion v4を先週リリースしました。今回のリリースの実装はほぼ全て@mayukiさんにやってもらったので、詳細はそちらに丸投げドンとして(ReadMeもかなり書き換えてあるので、詳しいところはそちらも読んでください)、改めて .NET 5とgRPC、そしてMagicOnionの位置付けとロードマップなどを説明したいかな、と思います。

MagicOnion v4ではサーバーサイド側は完全に ASP.NET Core KestrelベースのPure C#実装になりました。今まではGoogleの提供していたgRPC C Coreを利用していたのですが、今回よりMicrosoft実装に切り替えています。これによりASP.NET Core MVCなどと基盤が共通化されたので、gRPCを提供しつつHTTP/1 REST APIの口やHTML出力を行うような同居がとてもやりやすくなりました。

そして何より、パフォーマンスが向上しています。

.NET 5における性能向上に関してはgRPC performance improvements in .NET 5にて紹介されていますが、まず、gRPCと一口で言っても性能は言語によって千差万別です。HTTP/2だから速いとか、gRPCだから速いとか、そういうことはありません。大事なのは実装です。

gRPCの場合は、各言語での独自実装組(Java, Go, Rust)と、Cで作られているCoreのバインディング組(Ruby, Python, Node.js, etc...)に分かれます。バインディングだから低速だということはないのですが(そもそも高速なC++実装はC Coreの上に作られている)、どうしてもマーシャリング部分の実装の甘さや、各言語の部分に乗っかった箇所の実装の弱さに引っ張られて、性能が落ちやすい傾向にあります。

実際のところ、どれだけパフォーマンスに本気になって実装しているか、というところが性能に現れるので、わざわざ各言語で独自実装しているものは本気度が高く、バインディングで済ませているのは本気度が低い(動けば御の字)といったような見方でも良いでしょう。

C#も今まではC Coreのバインディングでしたが、.NET Core 3.1からPure C#実装が提供され、そして今回の.NET 5よりHTTP/2の性能向上に注力したことで、C++, Rust, Goと並ぶTier1の位置までパフォーマンス向上を果たしました。

もともとHTTP/1においても執念深く延々と性能改善施策を続けていて、ついにTechEmpower Web Framework Benchmarksでは1位(Plaintextのみですが)を奪取しています。

それ以外にも .NET 5ではPerformance Improvements in .NET 5として細かい対応を延々としてきたのがついに実ったという感じですね。Announcing .NET 5.0で表明されていますが、.NET 5 は Unified Platformsを標榜してランタイムコンポーネント・コンパイラ・言語を統一するという話があります。そうした大きなバージョンアップに相応しい一歩なのではないでしょうか。

ちなみに、.NET 5、実質的に機能しだすのは.NET 6からで、5に関しては基礎固めと.NET Core 3のリブランディング的な感じなので、実際のインパクトは今の所あんまありません。

gRPCとトランスポート中立、或いはQUIC

色々な構成要素の塊がgRPCなのですが、それぞれの要素はプラガブルで分解可能だったりします。シリアライザはProtocol Buffersでなくてもいいし(C++実装はFlatBuffersに置き換えられるものも用意されていたりするし、MagicOnionはMessagePackを使っています)、トランスポート層もHTTP/2的な決まりごとにさえ従えるのなら、ある程度は自由に変更できます、実際TCPではなくUNIX domain socketへの置き換えはプロセス間通信としてgRPCを使う場合にはままあります。

C Coreにべったりの場合は、換装の自由度が低かったりしたのですが、Pure C#に置き換わり、その辺の仕組みが全て ASP.NET Core の上に乗っかったことにより、比較的自由に弄れるようになっています。

既にASP.NET CoreにはQuic実装がMicrosoft.AspNetCore.Server.Kestrel.Transport.Experimental.Quicとして(Experimentalですが)提供されています。MsQuicを基盤として利用するため、Quicの実装準拠度としても比較的信用が置けるでしょう。MsQuicはWindows Serverで使うため、当然ながら相当固い実装である必要があるからです。なお、MsQuic自体はクロスプラットフォームのためLinuxで動きます。

サーバーはそうして自由に対応できるとして、現状クライアント側がイマイチなのですが、そこは追々という感じでしょうか。

特にゲームでのリアルタイム通信での利用時に、TCPであることがボトルネックとなることは避けたいので、RUDPや、中国のネットワークゲームでよく使われるKCPによる通信の口は用意しておきたいと思っています。QUICが大安定して全てそれで解決、みたいな時代が来ればいいんですけれどね。そう遠くはなさそうな感じがあるので、期待しています。

gRPCとMagicOnion、StreamingHubとBidirectional Streaming

gRPCの色々な構成要素の中でも最重要なのがprotoによる言語中立のスキーマとコードジェネレートにあるでしょう。MagicOnionは、そのprotoを投げ捨ててC# to C#に限定されるため、デメリットを超えるだけのメリットが必要です。

一つはprotoが言語中立であることによる表現力の乏しさを、C#そのものスキーマとすることで解決しています。protoの場合は少しのプリミティブとEnum、コレクションとマップのみですが(また、nullもない)、MagicOnionの場合は、C#そのものスキーマとして見立て、メッセージ形式にMessagePack for C#を利用することで、(ほぼ)全てのC#型が転送可能になっています。

例えば.NET 5ではWCFの置き換えにgRPCが推奨されていますが Migrate a WCF request-reply service to a gRPC unary RPC 、protoへの書き換えが必要なことと、表現力のギャップに苦しむことがあります。MagicOnionならDataContractで表現された型は全てMessagePack for C#でシリアライズ可能ですし、OperationContractのメソッドの複数引数のような表現も可能なため、移行におけるギャップはほとんどありません。

また、gRPCは双方向のリアルタイム通信用にBidirectional streaming RPCが利用できますが、これは双方が投げっぱなしのAPIしか存在しないため、戻り値の取得や処理の完了の待機などが実装できません。更にエンドポイントとなる型も一つしか使えないため、大量のoneofで呼び出しの切り分け処理をするしかありません。

MagicOnion StreamingHubはBidirectional streaming RPCの上に、双方向にC#としての型やメソッドのルーティング処理をつけ、client -> server -> client の呼び出しでは戻り値の取得やエラー送信、完了待機のシステムを入れました。この基盤処理の実装によって、初めてgRPCで実用的なリアルタイム通信が可能になっています。なお、APIは ASP.NET Core SignalRに寄せたため、そちらの経験があれば比較的スムーズに移行できるはずです。

UnityとgRPC

MagicOnion v4ではサーバーサイド側は完全に ASP.NET Core KestrelベースのPure C#実装になりました。クライアント側も.NET Coreの場合はHttpClientベースのPure C#実装になりました。Unityは……?というと、引き続きC Coreベースの提供になります。Unity側の改善はMagicOnionにおいてはv5でなんとかする予定ですので少々お待ち下さい。

Unity側の実装がC Coreで提供されている状態は、初期セットアップが相当面倒くさくなっています。というのもGoogleがAndroid, iOS向けのビルドを雑にとりあえずといった感じで提供しているだけなので、そのまんまだと動かないという……。MagicOnionのReadMeのSupport for Unity Clientセクションで、その辺は手厚めに解説してはいます。例えばそのまんまだとiOS用のgRPC libが100MBを超えていてGitHubで扱えないという問題が発生するのですが、ReadMeに説明してあるストリッピングの手順に従ってもらえればlibのサイズを縮めることができます。

ほか、C Coreの持つネイティブコネクションのライフサイクルと、Unity上での頻繁なPlay/StopによるC#側のライフサイクルが自動では一致しないため、ネイティブコネクションがリークするとエディタごと巻き込んでフリーズする(この場合、コネクション管理を徹底してライフサイクルを一致させれば大丈夫)、といった面倒くさい問題が発生したりします。

また、Taskベースで作られているためアロケーションが多めという問題もあったり。

これらネイティブライブラリであることの問題は、Pure C#実装を提供することで解決すると考えています。Task部分に関してはUniTaskを活用するように書き換えれば、アロケーションも減らせるでしょう。実際、Unity用のメジャーなOSSネットワークフレームワークであるMirror、の作者陣が内部分裂してForkされたMirrorNGはUniTaskベースで構築されています。

Pure C#実装の場合は、UnityのC#ランタイムであるmonoがあまり性能が良いとは言えないため、問題になる可能性があるのですが、サーバーとして使わなければ大丈夫なのではないかと踏んでいます。フルUnity実装でサーバーを提供する場合は気になるところなのでネイティブ実装を混ぜるなどの方向性もあるとは思いますが、現状のMagicOnionの構成はUnityはクライアントにしかならないので。

gRPCであること

gRPCを推しているのは、HTTP/2に乗っかっていることでインフラ側のミドルウェアが豊富なことがあります。Nginx, Envoyなど、今ではほとんどのソフトウェアがHTTP/2対応していますし、AWS ALBに至ってはgRPC専用のサポートを追加してきました。これらを活用することで、独自の通信形式などに比べると、サーバー構築の柔軟性が飛躍的に向上しています。独自っぽい雰囲気の漂っているMagicOnionも、変更しているのはメッセージの中身だけなので、gRPCのミドルウェアのエコシステムにはフルに乗っかれています(というか、ちゃんと乗れるように作っているのです)

また、自社で何もかもをすべて作らない、というのもあります。ASP.NET Coreに乗っかることで、Microsoftによる通信ライブラリの性能改善にタダ乗りしています。

gRPCそのもののメリットとしては、API通信とリアルタイム通信の二系統を一つのフレームワークに一本化できること、これは私が4年前にgRPCを採用した(当時は1.0が出たばかりでUnityの利用事例はゼロだし、一般の事例はマイクロサービスのサーバー間通信用としてが多くクライアント-サーバー通信を置き換えようとする例もあまりなかった)理由でもあります。

ロードマップ

v5におけるUnityクライアントの作成、ユーティリティとして負荷テストツールの提供、などがとりあえず並んでいます。特に負荷テストツールはあと少しなので、近日中にお届けできるかと。

MagicOnion自体のプロダクト採用の実績は増えてきていて、直近ではD4DJ Groovy MixがAPI(Unary)もリアルタイムマルチ(StreamingHub)も活用しています。また、バーチャルキャスト 2.0では、ルームというVR SNSの実装に活用されているそうです。

ゲームーサーバーとしてC#が活用できるのか?といったことは、クラスメソッドさん向けにお話したゲームサーバー用の発表でも話しましたので、レポ記事 - Cysharpの河合様をゲスト講師にお招きしてゲームサーバーに関する社内勉強会を開催しました!と、発表資料もどうぞ。

Building the Game Server both API and Realtime via c# from Yoshifumi Kawai

世の中、適材適所なのは間違いありません。だからこそ、「C#がその適材である」と言えるだけの環境を提供していく、というのがCysharpのミッションでもあります。Microsoftも.NET Coreにおいて、当初はWindowsべったりなC#が今更Linuxとか言ったって、みたいな白い目で見られていました。しかし、最初のバージョンから4年が経ち、文句を言わせないだけのパフォーマンスでもって証明してきました。

.NET 5はスタート地点だと考えています。C#も大変面白い環境になってきたと思うので、是非みんなと追求していけたら嬉しいですね。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive