メインコンテンツまでスキップ

gRPC in .NET

gRPCは、Googleが開発した高性能なRPCフレームワークで、Protocol Buffersを使用してサービス間通信を実現します。.NETではgRPC.AspNetCoreを通じて、ファーストクラスのサポートが提供されています。

gRPCとは

概要

gRPC(gRPC Remote Procedure Call)は、HTTP/2プロトコルをベースとした高性能なオープンソースRPCフレームワークです。

主な特徴:

  • 高性能: Protocol Buffers(バイナリ形式)とHTTP/2による高速通信
  • 契約ファースト: .protoファイルでサービス定義を行い、コードを自動生成
  • 多言語対応: 多数のプログラミング言語をサポート
  • ストリーミング: 単方向、双方向ストリーミングをネイティブサポート
  • 型安全: 強い型付けによるコンパイル時の検証

Protocol Buffers(Protobuf)

Protocol Buffersは、構造化データをシリアライズするためのGoogleの言語中立的なメカニズムです。

メリット:

  • JSONに比べて3~10倍小さいサイズ
  • パースが20~100倍高速
  • 明確なスキーマ定義
  • 後方互換性を保ちやすい

gRPC vs REST

特徴gRPCREST API
プロトコルHTTP/2HTTP/1.1 / HTTP/2
データ形式Protobuf(バイナリ)JSON(テキスト)
パフォーマンス高速比較的遅い
ブラウザサポートgRPC-Web経由で可能ネイティブサポート
ストリーミング単方向・双方向SSE、WebSocket等
型安全強い弱い(スキーマ定義がない場合)
人間可読性バイナリ(読めない)テキスト(読みやすい)
キャッシング複雑シンプル(HTTP標準)
ユースケースマイクロサービス間通信公開API、CRUD操作

.NETでのgRPCサービス実装

1. プロジェクトのセットアップ

# gRPCサービスプロジェクトの作成
dotnet new grpc -n MyGrpcService
cd MyGrpcService

# 必要なパッケージ(通常、テンプレートに含まれています)
dotnet add package Grpc.AspNetCore
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

2. .protoファイルの定義

Protos/greet.proto:

syntax = "proto3";

option csharp_namespace = "MyGrpcService";

package greet;

// Greeterサービスの定義
service Greeter {
// 単純なRPC(Unary)
rpc SayHello (HelloRequest) returns (HelloReply);

// サーバーストリーミングRPC
rpc SayHellos (HelloRequest) returns (stream HelloReply);

// クライアントストリーミングRPC
rpc SendGreetings (stream HelloRequest) returns (HelloReply);

// 双方向ストリーミングRPC
rpc Chat (stream HelloRequest) returns (stream HelloReply);
}

// リクエストメッセージ
message HelloRequest {
string name = 1;
}

// レスポンスメッセージ
message HelloReply {
string message = 1;
}

3. サービスの実装

Services/GreeterService.cs:

using Grpc.Core;
using MyGrpcService;

namespace MyGrpcService.Services;

public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;

public GreeterService(ILogger<GreeterService> logger)
{
_logger = logger;
}

// 単純なRPC(Unary)
public override Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
_logger.LogInformation("Received request from {Name}", request.Name);

return Task.FromResult(new HelloReply
{
Message = $"Hello {request.Name}"
});
}

// サーバーストリーミングRPC
public override async Task SayHellos(
HelloRequest request,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
for (int i = 0; i < 5; i++)
{
// キャンセルチェック
if (context.CancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Request cancelled");
break;
}

await responseStream.WriteAsync(new HelloReply
{
Message = $"Hello {request.Name} #{i + 1}"
});

await Task.Delay(TimeSpan.FromSeconds(1));
}
}

// クライアントストリーミングRPC
public override async Task<HelloReply> SendGreetings(
IAsyncStreamReader<HelloRequest> requestStream,
ServerCallContext context)
{
var names = new List<string>();

// クライアントからのストリームを読み取る
await foreach (var request in requestStream.ReadAllAsync())
{
names.Add(request.Name);
}

return new HelloReply
{
Message = $"Received greetings from: {string.Join(", ", names)}"
};
}

// 双方向ストリーミングRPC
public override async Task Chat(
IAsyncStreamReader<HelloRequest> requestStream,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
await foreach (var request in requestStream.ReadAllAsync())
{
_logger.LogInformation("Chat message from {Name}", request.Name);

await responseStream.WriteAsync(new HelloReply
{
Message = $"Echo: {request.Name}"
});
}
}
}

4. Program.csでの設定

Program.cs:

using MyGrpcService.Services;

var builder = WebApplication.CreateBuilder(args);

// gRPCサービスの追加
builder.Services.AddGrpc(options =>
{
// グローバルオプションの設定
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
options.MaxSendMessageSize = 16 * 1024 * 1024;
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});

// インターセプターの追加(オプション)
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
});

var app = builder.Build();

// gRPCエンドポイントのマッピング
app.MapGrpcService<GreeterService>();

// gRPC リフレクションの有効化(開発時のみ推奨)
if (app.Environment.IsDevelopment())
{
app.MapGrpcReflectionService();
}

// HTTP/1.1へのフォールバック用エンドポイント
app.MapGet("/", () =>
"Communication with gRPC endpoints must be made through a gRPC client.");

app.Run();

.NETでのgRPCクライアント実装

1. クライアントプロジェクトのセットアップ

# コンソールアプリの作成
dotnet new console -n MyGrpcClient
cd MyGrpcClient

# 必要なパッケージ
dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

2. .protoファイルの追加

サーバーと同じ.protoファイルをクライアントプロジェクトにコピーまたは参照します。

.csprojファイル:

<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

3. クライアントの実装

Program.cs:

using Grpc.Net.Client;
using MyGrpcService;

// チャンネルの作成(再利用推奨)
using var channel = GrpcChannel.ForAddress("https://localhost:7001");
var client = new Greeter.GreeterClient(channel);

// 1. 単純なRPC(Unary)
await UnaryCallExample(client);

// 2. サーバーストリーミングRPC
await ServerStreamingExample(client);

// 3. クライアントストリーミングRPC
await ClientStreamingExample(client);

// 4. 双方向ストリーミングRPC
await BidirectionalStreamingExample(client);

static async Task UnaryCallExample(Greeter.GreeterClient client)
{
Console.WriteLine("=== Unary Call ===");

var reply = await client.SayHelloAsync(
new HelloRequest { Name = "World" });

Console.WriteLine($"Response: {reply.Message}");
}

static async Task ServerStreamingExample(Greeter.GreeterClient client)
{
Console.WriteLine("\n=== Server Streaming ===");

using var call = client.SayHellos(
new HelloRequest { Name = "Streaming" });

await foreach (var response in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Response: {response.Message}");
}
}

static async Task ClientStreamingExample(Greeter.GreeterClient client)
{
Console.WriteLine("\n=== Client Streaming ===");

using var call = client.SendGreetings();

// 複数のリクエストを送信
var names = new[] { "Alice", "Bob", "Charlie" };
foreach (var name in names)
{
await call.RequestStream.WriteAsync(
new HelloRequest { Name = name });
Console.WriteLine($"Sent: {name}");
}

await call.RequestStream.CompleteAsync();

var response = await call.ResponseAsync;
Console.WriteLine($"Response: {response.Message}");
}

static async Task BidirectionalStreamingExample(Greeter.GreeterClient client)
{
Console.WriteLine("\n=== Bidirectional Streaming ===");

using var call = client.Chat();

// 並行してリクエストの送信とレスポンスの受信を行う
var readTask = Task.Run(async () =>
{
await foreach (var response in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Response: {response.Message}");
}
});

var names = new[] { "Message1", "Message2", "Message3" };
foreach (var name in names)
{
await call.RequestStream.WriteAsync(
new HelloRequest { Name = name });
Console.WriteLine($"Sent: {name}");
await Task.Delay(1000);
}

await call.RequestStream.CompleteAsync();
await readTask;
}

gRPCインターセプター

インターセプターを使用して、横断的関心事(ロギング、認証、エラーハンドリング等)を実装できます。

サーバー側インターセプター

Interceptors/LoggingInterceptor.cs:

using Grpc.Core;
using Grpc.Core.Interceptors;

namespace MyGrpcService.Interceptors;

public class LoggingInterceptor : Interceptor
{
private readonly ILogger<LoggingInterceptor> _logger;

public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
{
_logger = logger;
}

public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
_logger.LogInformation(
"Starting call. Method: {Method}, Type: Unary",
context.Method);

try
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await continuation(request, context);
stopwatch.Stop();

_logger.LogInformation(
"Completed call. Method: {Method}, Duration: {Duration}ms",
context.Method,
stopwatch.ElapsedMilliseconds);

return response;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in call. Method: {Method}",
context.Method);
throw;
}
}

public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(
IAsyncStreamReader<TRequest> requestStream,
ServerCallContext context,
ClientStreamingServerMethod<TRequest, TResponse> continuation)
{
_logger.LogInformation(
"Starting call. Method: {Method}, Type: Client Streaming",
context.Method);

return await continuation(requestStream, context);
}

// ServerStreamingServerHandler、DuplexStreamingServerHandlerも同様に実装可能
}

クライアント側インターセプター

public class ClientLoggingInterceptor : Interceptor
{
private readonly ILogger<ClientLoggingInterceptor> _logger;

public ClientLoggingInterceptor(ILogger<ClientLoggingInterceptor> logger)
{
_logger = logger;
}

public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
_logger.LogInformation(
"Calling {Method}",
context.Method.FullName);

return continuation(request, context);
}
}

// 使用例
var channel = GrpcChannel.ForAddress("https://localhost:7001");
var invoker = channel.Intercept(new ClientLoggingInterceptor(logger));
var client = new Greeter.GreeterClient(invoker);

エラーハンドリング

サーバー側

public override async Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
if (string.IsNullOrEmpty(request.Name))
{
throw new RpcException(
new Status(StatusCode.InvalidArgument, "Name cannot be empty"));
}

try
{
// ビジネスロジック
return new HelloReply { Message = $"Hello {request.Name}" };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing request");

throw new RpcException(
new Status(StatusCode.Internal, "An error occurred processing your request"),
ex.Message);
}
}

クライアント側

try
{
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "" });
}
catch (RpcException ex)
{
Console.WriteLine($"gRPC Error: {ex.Status.StatusCode}");
Console.WriteLine($"Detail: {ex.Status.Detail}");

switch (ex.StatusCode)
{
case StatusCode.InvalidArgument:
Console.WriteLine("Invalid input provided");
break;
case StatusCode.Unavailable:
Console.WriteLine("Service is unavailable");
break;
case StatusCode.DeadlineExceeded:
Console.WriteLine("Request timed out");
break;
default:
Console.WriteLine("Unknown error occurred");
break;
}
}

メタデータ(ヘッダー)の使用

サーバー側:メタデータの読み取り

public override Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
// ヘッダーの読み取り
var headers = context.RequestHeaders;
var authToken = headers.GetValue("authorization");
var userId = headers.GetValue("user-id");

_logger.LogInformation(
"Request from user {UserId} with token {Token}",
userId,
authToken);

// レスポンスヘッダーの追加
context.ResponseTrailers.Add("server-time", DateTime.UtcNow.ToString());

return Task.FromResult(new HelloReply
{
Message = $"Hello {request.Name}"
});
}

クライアント側:メタデータの送信

var metadata = new Metadata
{
{ "authorization", "Bearer token123" },
{ "user-id", "user456" }
};

var reply = await client.SayHelloAsync(
new HelloRequest { Name = "World" },
headers: metadata);

// レスポンスヘッダーの取得
using var call = client.SayHelloAsync(
new HelloRequest { Name = "World" },
headers: metadata);

var response = await call.ResponseAsync;
var trailers = call.GetTrailers();
var serverTime = trailers.GetValue("server-time");

デッドラインとタイムアウト

クライアント側でのデッドライン設定

// 方法1: 個別の呼び出しでデッドラインを設定
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "World" },
deadline: DateTime.UtcNow.AddSeconds(5));

// 方法2: CallOptionsを使用
var options = new CallOptions(
deadline: DateTime.UtcNow.AddSeconds(5),
cancellationToken: cancellationToken);

var reply = await client.SayHelloAsync(
new HelloRequest { Name = "World" },
options);

サーバー側でのデッドラインチェック

public override async Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
// 長時間実行される処理
for (int i = 0; i < 10; i++)
{
// デッドラインをチェック
if (context.CancellationToken.IsCancellationRequested)
{
_logger.LogWarning("Request cancelled or deadline exceeded");
throw new RpcException(
new Status(StatusCode.Cancelled, "Operation was cancelled"));
}

await Task.Delay(1000);
}

return new HelloReply { Message = $"Hello {request.Name}" };
}

認証と認可

JWT Bearer認証

Program.cs:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGrpcService<GreeterService>();

サービスでの認可:

using Microsoft.AspNetCore.Authorization;

[Authorize]
public class GreeterService : Greeter.GreeterBase
{
[Authorize(Roles = "Admin")]
public override Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
// 認証されたユーザー情報の取得
var userName = context.GetHttpContext().User.Identity?.Name;

return Task.FromResult(new HelloReply
{
Message = $"Hello {request.Name} from {userName}"
});
}
}

クライアント側での認証トークン送信:

var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
if (!string.IsNullOrEmpty(token))
{
metadata.Add("Authorization", $"Bearer {token}");
}
return Task.CompletedTask;
});

var channel = GrpcChannel.ForAddress("https://localhost:7001", new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(
new SslCredentials(),
credentials)
});

var client = new Greeter.GreeterClient(channel);

パフォーマンスの最適化

1. チャンネルの再利用

// ❌ 悪い例:呼び出しごとにチャンネルを作成
foreach (var item in items)
{
using var channel = GrpcChannel.ForAddress("https://localhost:7001");
var client = new Greeter.GreeterClient(channel);
await client.SayHelloAsync(new HelloRequest { Name = item });
}

// ✅ 良い例:チャンネルを再利用
using var channel = GrpcChannel.ForAddress("https://localhost:7001");
var client = new Greeter.GreeterClient(channel);

foreach (var item in items)
{
await client.SayHelloAsync(new HelloRequest { Name = item });
}

2. HTTP/2接続プールの設定

var handler = new SocketsHttpHandler
{
PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
KeepAlivePingDelay = TimeSpan.FromSeconds(60),
KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
EnableMultipleHttp2Connections = true
};

var channel = GrpcChannel.ForAddress("https://localhost:7001", new GrpcChannelOptions
{
HttpHandler = handler
});

3. メッセージサイズの最適化

builder.Services.AddGrpc(options =>
{
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
options.MaxSendMessageSize = 16 * 1024 * 1024;
options.CompressionProviders = new[]
{
new GzipCompressionProvider(CompressionLevel.Optimal)
};
});

4. ストリーミングの活用

大量のデータを扱う場合は、すべてを1つのメッセージで送るのではなく、ストリーミングを使用します。

// ❌ 大きなメッセージ(非効率)
message LargeDataResponse {
repeated DataItem items = 1; // 10,000個のアイテム
}

// ✅ ストリーミング(効率的)
service DataService {
rpc GetData (DataRequest) returns (stream DataItem);
}

gRPC-Webでのブラウザサポート

ブラウザから直接gRPCサービスを呼び出すには、gRPC-Webを使用します。

サーバー側の設定

dotnet add package Grpc.AspNetCore.Web
builder.Services.AddGrpc();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
});
});

var app = builder.Build();

app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.UseCors("AllowAll");

app.MapGrpcService<GreeterService>().EnableGrpcWeb();

クライアント側(TypeScript/JavaScript)

npm install grpc-web
npm install google-protobuf
npm install @types/google-protobuf
import { GreeterClient } from './generated/greet_grpc_web_pb';
import { HelloRequest } from './generated/greet_pb';

const client = new GreeterClient('https://localhost:7001');

const request = new HelloRequest();
request.setName('World');

client.sayHello(request, {}, (err, response) => {
if (err) {
console.error('Error:', err);
} else {
console.log('Response:', response.getMessage());
}
});

ヘルスチェック

gRPCサービスのヘルスチェックを実装します。

dotnet add package Grpc.HealthCheck
dotnet add package Grpc.AspNetCore.HealthChecks
builder.Services.AddGrpc();
builder.Services.AddGrpcHealthChecks()
.AddCheck("Sample", () => HealthCheckResult.Healthy());

var app = builder.Build();

app.MapGrpcService<GreeterService>();
app.MapGrpcHealthChecksService();

クライアント側でヘルスチェック:

using Grpc.Health.V1;

var channel = GrpcChannel.ForAddress("https://localhost:7001");
var healthClient = new Health.HealthClient(channel);

var response = await healthClient.CheckAsync(new HealthCheckRequest());
Console.WriteLine($"Health status: {response.Status}");

ベストプラクティス

1. サービス設計

  • 細粒度の適切なバランス: サービスは適度な粒度にする(粗すぎず、細かすぎず)
  • 契約の安定性: .protoファイルの変更は慎重に行い、後方互換性を保つ
  • バージョニング: パッケージ名にバージョンを含める(例: package myapp.v1;

2. パフォーマンス

  • チャンネルの再利用: GrpcChannelはシングルトンまたは長期間保持
  • ストリーミングの活用: 大量データにはストリーミングを使用
  • 適切なデッドライン: すべての呼び出しにデッドラインを設定
  • 圧縮の使用: 大きなメッセージには圧縮を有効化

3. エラーハンドリング

  • 適切なStatusCode: 状況に応じた適切なステータスコードを返す
  • 詳細なエラー情報: 開発環境では詳細なエラーを有効化
  • リトライロジック: クライアント側で適切なリトライロジックを実装

4. セキュリティ

  • TLS/SSL: 本番環境では必ずTLS/SSLを使用
  • 認証・認可: JWT、mTLS、API Keyなどで適切に保護
  • レート制限: DoS攻撃を防ぐためにレート制限を実装

5. 監視とロギング

  • 構造化ログ: 十分なコンテキスト情報を含むログを記録
  • 分散トレーシング: OpenTelemetryなどで分散トレーシングを実装
  • メトリクス: レイテンシー、エラー率などのメトリクスを収集

トラブルシューティング

HTTP/2が無効になっている

問題: gRPCはHTTP/2が必要ですが、無効になっている場合があります。

解決策:

// Kestrelの設定
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(7001, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http2;
listenOptions.UseHttps();
});
});

デバッグ時の証明書エラー

問題: 開発環境でSSL証明書のエラーが発生する。

解決策:

// 開発環境のみ:証明書検証をスキップ
var httpHandler = new HttpClientHandler();
if (builder.Environment.IsDevelopment())
{
httpHandler.ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}

var channel = GrpcChannel.ForAddress("https://localhost:7001", new GrpcChannelOptions
{
HttpHandler = httpHandler
});

gRPC呼び出しのデバッグ

Grpc.Net.Client.Webを使用してgRPC呼び出しをデバッグ:

builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = true;
});

// ロギングの有効化
builder.Logging.AddFilter("Grpc", LogLevel.Debug);

gRPCを適用すべきマイクロサービスのシナリオ

gRPCは万能ではありません。適切なユースケースを理解し、各マイクロサービスの特性に応じて選択することが重要です。

gRPCが最適なシナリオ

1. 内部マイクロサービス間通信

推奨理由:

  • 同一組織内のサービス間通信では、gRPCの契約ファースト設計が大きなメリット
  • .protoファイルによる明確なAPI契約により、サービス間の依存関係が明示的
  • HTTP/2の多重化により、レイテンシーとスループットが大幅に改善

具体例:

┌─────────────┐ gRPC ┌──────────────┐
│ API │────────────────→│ 注文 │
│ Gateway │ │ サービス │
│ (REST) │←────────────────│ (内部通信) │
└─────────────┘ └──────────────┘
│ gRPC

┌──────────────┐
│ 在庫 │
│ サービス │
└──────────────┘

実装ポイント:

  • 外部公開APIはREST(API Gateway)、内部通信はgRPC
  • サービスメッシュ(Linkerd、Istio)との組み合わせで、セキュリティと監視を強化

2. 高頻度・低レイテンシーが求められるサービス

推奨理由:

  • Protocol Buffersのバイナリシリアライゼーションにより、JSONより3~10倍高速
  • リアルタイム処理が必要な金融取引、IoTデータ処理、ゲームサーバーなどに最適

具体例:

  • リアルタイム価格配信サービス: 株価、為替レートなどの高頻度更新
  • IoTデータ収集サービス: センサーデータの大量収集と処理
  • リアルタイム通知サービス: チャット、アラート、ライブフィードの配信
// リアルタイム価格配信の例
service PriceStream {
// サーバーストリーミング:継続的な価格更新
rpc StreamPrices (PriceRequest) returns (stream PriceUpdate);
}

message PriceUpdate {
string symbol = 1;
double price = 2;
google.protobuf.Timestamp timestamp = 3;
}

3. ストリーミングが必要なサービス

推奨理由:

  • gRPCは単方向・双方向ストリーミングをネイティブサポート
  • RESTのSSEやWebSocketよりもシンプルに実装可能

具体例:

  • ログ集約サービス: 複数のサービスからログをストリーミング収集
  • ファイルアップロード/ダウンロードサービス: 大容量ファイルのチャンク転送
  • チャットサービス: リアルタイムメッセージング
  • ライブデータ監視: メトリクス、ヘルスステータスのリアルタイム監視
// 双方向ストリーミングチャットの例
service ChatService {
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

message ChatMessage {
string user_id = 1;
string room_id = 2;
string content = 3;
google.protobuf.Timestamp timestamp = 4;
}

4. ポリグロット(多言語)環境のサービス

推奨理由:

  • .protoファイルから多言語のクライアント/サーバーコードを自動生成
  • Java、Python、Go、Node.js、C++など、主要言語をサポート
  • 言語間の型の不一致やシリアライゼーションの問題を回避

具体例:

┌──────────────┐
│ .NET │
│ APIサービス │───┐
└──────────────┘ │
│ 共通の.protoファイル
┌──────────────┐ │ で統一されたAPI契約
│ Python │───┤
│ ML サービス │ │
└──────────────┘ │

┌──────────────┐ │
│ Go │───┘
│ 認証サービス│
└──────────────┘

5. バッチ処理や集約処理を行うサービス

推奨理由:

  • クライアントストリーミングを使用して、複数のリクエストを効率的に送信
  • バッチリクエストをまとめて処理し、ネットワークオーバーヘッドを削減

具体例:

// バッチ処理の例
service DataProcessor {
// クライアントストリーミング:複数データを受信して一括処理
rpc ProcessBatch (stream DataItem) returns (ProcessResult);
}

message DataItem {
string id = 1;
bytes data = 2;
}

message ProcessResult {
int32 total_processed = 1;
int32 successful = 2;
int32 failed = 3;
repeated string error_ids = 4;
}

gRPCが適さないシナリオ

1. ブラウザから直接呼び出すパブリックAPIサービス

理由:

  • gRPCはHTTP/2が必要で、ブラウザからの直接呼び出しにはgRPC-Webが必要
  • gRPC-Webは機能制限があり、双方向ストリーミングが使えない
  • RESTの方がブラウザとの親和性が高い

推奨アプローチ:

  • 外部向けはREST API、内部サービス間通信はgRPC
  • API Gatewayでプロトコル変換(REST → gRPC)

2. キャッシングが重要な読み取り専用サービス

理由:

  • RESTはHTTPキャッシュ(CDN、ブラウザキャッシュ)を活用しやすい
  • gRPCではキャッシング戦略の実装が複雑

具体例:

  • 静的コンテンツ配信サービス
  • 公開APIドキュメント
  • ニュースフィード(更新頻度が低い)

3. サードパーティ統合が多いサービス

理由:

  • 多くのサードパーティAPIはRESTベース
  • gRPCのサポートは限定的

推奨アプローチ:

  • 外部統合はREST、内部処理はgRPC

4. デバッグや手動テストが頻繁に必要なサービス

理由:

  • バイナリフォーマットのため、Postman等での簡易テストが困難
  • curlやブラウザでの直接確認ができない

対策:

  • gRPC Reflectionを有効化
  • BloomRPCやgRPCurlなどの専用ツールを使用

ハイブリッドアプローチ

多くの実用的なシステムでは、gRPCとRESTを併用するハイブリッドアプローチが最適です。

// 同じASP.NET CoreアプリでgRPCとRESTを両方提供
var builder = WebApplication.CreateBuilder(args);

// gRPCサービスの追加
builder.Services.AddGrpc();

// RESTful APIの追加
builder.Services.AddControllers();

var app = builder.Build();

// gRPCエンドポイント(内部通信用)
app.MapGrpcService<OrderService>();
app.MapGrpcService<InventoryService>();

// RESTエンドポイント(外部公開用)
app.MapControllers();

app.Run();

推奨パターン:

  • 外部向けAPI: REST(HTTP/1.1、JSON)
  • 内部マイクロサービス間: gRPC(HTTP/2、Protobuf)
  • リアルタイムストリーミング: gRPC(双方向ストリーミング)
  • 静的コンテンツ: REST(CDNキャッシュ活用)

意思決定フローチャート

まとめ:適用判断のチェックリスト

gRPCを選択すべき場合:

  • ✅ 内部マイクロサービス間の通信
  • ✅ 低レイテンシー・高スループットが必要
  • ✅ リアルタイムストリーミングが必要
  • ✅ ポリグロット(多言語)環境
  • ✅ 型安全性と契約管理が重要
  • ✅ バイナリ効率が重要(モバイル、IoT)

RESTを選択すべき場合:

  • ✅ 外部公開API
  • ✅ ブラウザから直接呼び出し
  • ✅ キャッシングが重要
  • ✅ サードパーティ統合が多い
  • ✅ 人間が直接確認・デバッグする必要がある
  • ✅ 既存のRESTエコシステムとの互換性が必要

まとめ

gRPCは以下のような場面で非常に効果的です:

マイクロサービス間通信: 高速で型安全な内部通信 ✅ リアルタイムアプリケーション: 双方向ストリーミングによるリアルタイム通信 ✅ モバイルアプリ: 低帯域幅でも効率的な通信 ✅ ポリグロット環境: 多言語対応が必要なシステム

一方、以下の場合はRESTful APIの方が適しています:

⚠️ ブラウザからの直接呼び出しが必要(gRPC-Webが使えない場合) ⚠️ キャッシングが重要な要件 ⚠️ 人間が直接APIを操作する必要がある

.NETでのgRPC実装は非常に成熟しており、ASP.NET CoreとProtobufツールチェーンによって、効率的で保守性の高いサービスを構築できます。

参考リンク