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
| 特徴 | gRPC | REST API |
|---|---|---|
| プロトコル | HTTP/2 | HTTP/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ツールチェーンによって、効率的で保守性の高いサービスを構築できます。