Start Debugging

.NET 11 のサービス間呼び出しにおける gRPC vs REST vs SignalR

.NET 11 の内部サービス間呼び出しでは、契約の両端を自分たちで所有していて呼び出しがポイントツーポイントであれば、既定で gRPC を選んでください。自分たちが管理していないものがそのサービスを呼び出す必要が出た時点で、JSON を使う REST に切り替えます。SignalR はサービス間の RPC トランスポートではありません。1 つの生産者が多数の長寿命な消費者へメッセージを配信する必要がある場合にのみ使ってください。

サービス A がサービス B を呼び出し、他に B を呼び出すものが存在しないなら、gRPC を使ってください。両端を自分たちで所有しているので、生成されたクライアントとバイナリ契約のコストは実質ゼロであり、JSON 相当のおよそ半分のサイズのペイロードと、本物のデッドライン伝播が手に入ります。自分たちが管理していないものがそのサービスを呼び出す必要が出た時点で、JSON を使う REST に切り替えてください。ブラウザー、取引先、運用手順書の中の curl コマンドなどです。SignalR はこの中で異質な存在であり、この比較で最も多い誤りは、SignalR を 3 つ目の RPC の選択肢として扱ってしまうことです。そうではありません。SignalR は接続管理と配信のレイヤーであり、1 つの生産者が多数の長寿命な消費者へプッシュする必要がある場合にのみ、その居場所を得ます。以下の内容はすべて .NET 11 (Preview 6、SDK 11.0.100-preview.6.26359.118、GA は 2026 年 11 月予定) と C# 14、Grpc.AspNetCore 2.83.0 を対象としています。

判断を 1 つの表で

項目gRPCJSON を使う RESTSignalR
呼び出しの形ポイントツーポイントの RPCポイントツーポイントのリクエスト/レスポンス1 つの生産者、多数の消費者
契約必須、.proto任意、OpenAPIなし、メソッド名は文字列
プロトコルHTTP/2 (必須)HTTP/1.1、HTTP/2、HTTP/3WebSockets、SSE、ロングポーリング
ペイロードProtobuf、バイナリJSON、テキストJSON または MessagePack
クライアント.proto から生成手書きまたは OpenAPI から生成手書き、メソッド名は文字列
ストリーミングクライアント、サーバー、双方向サーバー (chunked / SSE)サーバー、クライアント、双方向
呼び出し側のキャンセルが呼び出され側に届くかはい、加えてネイティブなデッドラインあり接続の中断としてのみ.NET 11 以降ははい、ストリーミングでない呼び出しについて
ブラウザーから呼び出せるかいいえ、gRPC-Web またはトランスコーディングが必要はいはい、それが本来の用途
L4 ロードバランサーの背後で動くかうまく動かないはいスティッキーセッションまたはバックプレーンが必要
通信内容が人間に読めるかいいえはいJSON なら読める、MessagePack なら読めない
ASP.NET Core に同梱されるかいいえ、別配布の NuGet パッケージはいはい

現実のケースのほとんどは 2 つの行で決まります。「呼び出しの形」が SignalR を他の 2 つから分け、「契約」が gRPC を REST から分けます。表の下のほうの行を天秤にかけているなら、おそらくすでに判断は済んでいて、後押しを探しているだけです。

SignalR がこの比較に登場し続ける理由と、たいてい負ける理由

SignalR がサービス間通信の検索に現れるのは、ハブメソッドが RPC そのものに見えるからです。

// .NET 11, C# 14 -- looks like RPC, is not built for it
public sealed class PricingHub : Hub
{
    public Task<decimal> GetPrice(string sku) => _pricing.LookupAsync(sku);
}

呼び出し側は別のサービスから InvokeAsync<decimal>("GetPrice", sku) を実行して答えを得ることが確かにできます。動きはします。しかしそこで作られたものは、来ては去るクライアントの接続ライフタイム管理を設計の中心にすえた技術の上に載せた RPC チャネルです。その設計の利点をまったく必要としないまま、コストだけを引き継ぐことになります。

具体的なコストは次のとおりです。メソッド名はディスパッチ時にリフレクションで解決される文字列なので、名前の変更はビルドエラーではなく実行時の失敗になります。スキーマがないため、クライアントを生成するものも、ペイロードの形を検証するものもありません。スケールアウトするにはプール内のすべてのサーバーがすべての接続に到達できる必要があり、Redis バックプレーンか Azure SignalR Service が必要になります。WebSockets を使っていない場合はさらにスティッキーセッションも必要です。そしてハブ接続はステートフルです。以前はステートレスなリクエストだったものについて、呼び出し側は再接続の状態機械を考慮しなければならなくなります。

トラフィックが本当に多数への配信であるとき、SignalR は正しい答えです。40 個のワーカープロセスに価格更新をプッシュしなければならない価格サービスは SignalR の問題です。SignalR にはグループ、ブロードキャスト、バックプレーンがあり、gRPC にはそのどれもないからです。Microsoft 自身の gRPC と HTTP API の比較がこれを直接述べています。gRPC はストリーミングをサポートしますが、登録された接続へブロードキャストするという概念を持たないため、各 gRPC 呼び出しがそれぞれのクライアントへ個別にストリーミングする必要があります。

区別されるのは多数への配信であって、「リアルタイム」かどうかではありません。gRPC の双方向ストリーミングはリアルタイムです。ただポイントツーポイントであるだけです。

それぞれが実際に通信路へ載せるもの

gRPC を推すパフォーマンスの主張は、たいてい数字を伴わずに「Protobuf は JSON より小さい」と語られます。典型的な内部レスポンスの形をしたメッセージについて、その数字を示します。

// proto3
message OrderStatus {
  string order_id   = 1;  // "8f14e45f-ceea-467a-9c1d-2b7f2f0c3a11"
  int32  status     = 2;  // 3
  int64  updated_at = 3;  // 1786060800
  double total      = 4;  // 129.95
  string currency   = 5;  // "EUR"
}
エンコーディングメッセージのバイト数フレーミング込みのバイト数JSON 比
JSON (System.Text.Json、既定のオプション)116116100%
MessagePack (SignalR のバイナリハブプロトコル)66該当なし56.9%
Protobuf (Google.Protobuf 3.35.1)606551.7%
SignalR の JSON ハブプロトコルの呼び出し該当なし165142%

測定方法: 同じ 5 つのフィールドを各エンコーディングでシリアル化してバイト数を数えました。測定環境は Windows 11、.NET 10.0.5 ランタイム (SDK 10.0.201)、Google.Protobuf 3.35.1、MessagePack 3.1.8 です。通信フォーマットはランタイムのバージョンとは独立に仕様化されているため、.NET 11 でもバイト数は同一で、異なるのはエンコードを行うランタイムだけです。「フレーミング込みのバイト数」には、gRPC の 5 バイトの長さプレフィックス (圧縮フラグ 1 バイトとビッグエンディアンの長さ 4 バイト) を加え、SignalR については JSON の呼び出しエンベロープとレコード区切り文字 0x1E を加えています。

この表を何かの根拠に使う前に、よく読んでください。Protobuf は 116 バイトのメッセージで 56 バイトを節約します。毎秒 1 万件の呼び出しを処理するサービスなら 560 KB/s の送信量であり、ゾーン間トラフィックに課金されているなら重要で、そうでなければ誤差です。興味深いのは SignalR の行です。JSON ハブプロトコルのエンベロープにより、1 回の呼び出しは素の REST 相当よりも大きくなります。ペイロードに加えて typetargetarguments の分を支払うからです。ハブを MessagePack に切り替えればその大半は取り戻せますが、そもそもテキストプロトコルを検討した理由であった人間可読性を失います。

シリアル化サイズは gRPC の利点の中で最も弱いものでもあります。より強いのは、生成されるクライアントとデッドラインです。

gRPC を選ぶとき

// .NET 11, C# 14 -- Grpc.AspNetCore 2.83.0
// Server
builder.Services.AddGrpc();
app.MapGrpcService<OrderService>();

// Client: register through the factory so channels are reused.
builder.Services
    .AddGrpcClient<Orders.OrdersClient>(o => o.Address = new Uri("https://orders"))
    .AddStandardResilienceHandler();

// Call site: the deadline is the point.
var reply = await client.GetStatusAsync(
    new OrderRequest { OrderId = id },
    deadline: DateTime.UtcNow.AddSeconds(2),
    cancellationToken: ct);

アプリケーションコードでは GrpcChannel.ForAddress ではなく AddGrpcClient を使ってください。呼び出しごとにチャネルを作ると、そのたびに新しいソケット、TCP ハンドシェイク、TLS ネゴシエーション、HTTP/2 接続プリアンブルが強制されますが、ファクトリーはチャネルを再利用してくれます。リトライを重ねる場合も、HttpClient をラップするのと同じレジリエンスハンドラーがここでも使えます。gRPC チャネルの内部は SocketsHttpHandler だからです。

JSON を使う REST を選ぶとき

// .NET 11, C# 14 -- minimal API + typed client
app.MapGet("/orders/{id}", async (string id, IOrderStore store, CancellationToken ct)
    => await store.FindAsync(id, ct) is { } o
        ? Results.Ok(o)
        : Results.NotFound());

// Caller
builder.Services
    .AddHttpClient<OrdersClient>(c => c.BaseAddress = new Uri("https://orders"))
    .AddStandardResilienceHandler();

これより構造化されたものが必要なら、型付きの Results 共用体を返すことで、レスポンスの形のコンパイル時チェックと、手書きの属性なしでの正しい OpenAPI ドキュメントが得られます。これは gRPC を魅力的にしていた契約の厳密さの一部を取り戻すものです。

SignalR が本当に正解であるとき

.NET 11 は長寿命な接続について SignalR を 2 つの点で明確に改善しています。/refresh エンドポイントと EnableAuthenticationRefresh により、ベアラートークンの期限切れでハブ接続が落ちなくなりました。これはトークン認証を使うデプロイにおける不要な再接続の最大の原因でした。さらに、SignalR クライアントがついに実行中のハブメソッドをキャンセルできるようになり、InvokeAsync に渡した CancellationToken をキャンセルすると実際にサーバーまで届きます。Preview 6 の時点でどちらも .NET クライアント限定であり、JavaScript クライアントと Azure SignalR Service のサポートは作業中です。

あなたの代わりに判断を決めてしまう落とし穴

L4 ロードバランサーは gRPC を壊します。 gRPC チャネルは 1 本の HTTP/2 接続であり、すべての呼び出しがその上で多重化されます。L4 バランサーは TCP 接続を分配するため、そのチャネルからのすべての呼び出しはずっと同じバックエンドに着地します。結果として、1 台が過負荷になり残りは遊びます。これを直すにはクライアントサイドのロードバランシングか、Envoy、Linkerd、YARP のような L7 プロキシが必要で、その判断はたいてい自分ではなくプラットフォームチームの領分です。その変更ができないなら比較は終わりで、REST の勝ちです。同種のインフラ上の摩擦は gRPC をコンテナーで動かすときにも現れ、HTTP/1.1 しか話さないプロキシがプロトコル不一致にはまったく見えない障害を引き起こします。

gRPC は .NET とは別サイクルで出荷され、TFM の一覧がそれを証明しています。 2026-08-03 に公開された Grpc.AspNetCore 2.83.0 は net8.0net9.0net10.0 を対象としています。net11.0 のターゲットフレームワークは存在せず、リリースノート What’s new in ASP.NET Core in .NET 11 には gRPC のセクションがそもそもありません。これはサポートの欠落ではありません。net10.0 のアセンブリは .NET 11 上で読み込まれ動作します。これはリリース周期の違いです。.NET 上の gRPC は grpc/grpc-dotnet で独自のリリーススケジュールに沿って保守されているため、gRPC にとって有益な .NET 11 の機能は 11 月ではなく grpc-dotnet が出荷したときに届きます。アップグレードの計画はそれを前提に立ててください。

HTTP/2 は gRPC には必須で、それ以外にとっては任意です。 これは、中間経路を自分で管理できない区間では実際の制約になります。同時に、gRPC は今日 HTTP/3 の恩恵を受けられない一方、REST エンドポイントは受けられるということでもあります。Kestrel で HTTP/3 を提供する設定はエンドポイントの 1 行の変更で済み、.NET 11 の Kestrel は制御ストリームと SETTINGS フレームを待たずに HTTP/3 リクエストの処理を開始するようになり、新規接続の初回リクエストのレイテンシが短くなりました。

SignalR のスケールアウトは設定ではなく依存関係です。 サーバーインスタンスが 2 つ以上なら Redis バックプレーンか Azure SignalR Service が必要で、WebSocket 以外のトランスポートではさらにスティッキーセッションが必要です。多数への配信に見合うかを判断する前に、ラウンドロビンのバランサーの背後に置いたステートレスな REST エンドポイントと比べてみてください。

可観測性は同等ではありません。 3 つとも OpenTelemetry を通る ActivitySource のトレースを出力するので、トレースを無料のバックエンドに接続することですべてを扱えます。違うのはネットワークキャプチャで何が見えるかです。JSON は読めますが、Protobuf と MessagePack にはスキーマとツールが要ります。

推奨、あらためて

まず多数への配信かどうかで線を引いてください。1 つのサービスが多数の長寿命な消費者へ通知しなければならないなら、それは SignalR であり、他の 2 つにはグループとバックプレーンの代替はありません。それ以外はすべてポイントツーポイントであり、そこでの問いは契約を誰が所有しているかです。両端を自分たちで所有していて、スキーマを変更する pull request の中でクライアントを再生成できるなら、生成されたクライアントと伝播されるデッドラインによって gRPC は元が取れます。ペイロードが小さいことは理由ではなくおまけです。自分たちのビルドの外の誰かがそのサービスを呼ぶなら、JSON を使う REST を提供し、支払ってもいないバイト数の最適化はやめましょう。

避けるべき失敗の形は、ベンチマークがペイロードサイズ 51.7% を示したという理由で毎分 3 件しか呼ばれないサービスに gRPC を選び、その後で L4 ロードバランサーがすべての呼び出しを 1 つの Pod に固定していると気づくことです。メッセージあたり 56 バイトは、プラットフォーム移行に見合いません。

関連記事

参考資料

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る