はじめに
仕事で開発に携わっているアプリでは通信に WCF を使っている。 一方で去年から Microservices やゲームのバックエンドとして gRPC が盛り上がっている。 gRPC は通信に HTTP/2 を使っていて、HTTP/2 はクライアントとサーバー間の通信がバイナリベースで行われているし、 さらに gRPC ではシリアライズに ProtocolBuffers を使っている。
「こいつはもしかしたら、WCF の NetTcpBinding と同じくらい速いのでは?」 と思ったので、ベンチマークをとって比較してみた。
NuGet パッケージをインストール
gRPC を使うために必要なパッケージは、 公式のものが NuGet に公開されているのでインストールする。
service.proto ファイル作成
まずは gRPC 側から取り掛かる。
service.proto
ファイルを作成し、サービスのインタフェースを定義。
syntax = "proto3"; package grpcVsWcf.grpc; service BookService { rpc GetBooks(BooksRequest) returns (BooksReply); } message Book { string id = 1; string title = 2; string description = 3; string author = 4; int32 price = 5; string publishedAt = 6; } message BooksRequest { int32 count = 1; } message BooksReply { repeated Book books = 1; }
service.proto からコードを生成
NuGet でインストールした Grpc.Tools にコード生成のためのツールが含まれているので、 コマンドプロンプトから直接実行する。 Visual Studio にコマンドでも追加してくれれば楽なんだけどね。
packages\Grpc.Tools.1.13.0\tools\windows_x86\protoc.exe --csharp_out . --grpc_out service.proto --plugin=protoc-gen-grpc=packages\Grpc.Tools.1.13.0\tools\windows_x86\grpc_csharp_plugin.exe
次のようなサービスのコードが出力される。
// <auto-generated> // Generated by the protocol buffer compiler. DO NOT EDIT! // source: service.proto // </auto-generated> #pragma warning disable 0414, 1591 #region Designer generated code using grpc = global::Grpc.Core; namespace GrpcVsWcf.Grpc { public static partial class BookService { static readonly string __ServiceName = "grpcVsWcf.grpc.BookService"; static readonly grpc::Marshaller<global::GrpcVsWcf.Grpc.BooksRequest> __Marshaller_BooksRequest = grpc::Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::GrpcVsWcf.Grpc.BooksRequest.Parser.ParseFrom); static readonly grpc::Marshaller<global::GrpcVsWcf.Grpc.BooksReply> __Marshaller_BooksReply = grpc::Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::GrpcVsWcf.Grpc.BooksReply.Parser.ParseFrom); static readonly grpc::Method<global::GrpcVsWcf.Grpc.BooksRequest, global::GrpcVsWcf.Grpc.BooksReply> __Method_GetBooks = new grpc::Method<global::GrpcVsWcf.Grpc.BooksRequest, global::GrpcVsWcf.Grpc.BooksReply>( grpc::MethodType.Unary, __ServiceName, "GetBooks", __Marshaller_BooksRequest, __Marshaller_BooksReply); /// <summary>Service descriptor</summary> public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor { get { return global::GrpcVsWcf.Grpc.ServiceReflection.Descriptor.Services[0]; } } /// <summary>Base class for server-side implementations of BookService</summary> public abstract partial class BookServiceBase { public virtual global::System.Threading.Tasks.Task<global::GrpcVsWcf.Grpc.BooksReply> GetBooks(global::GrpcVsWcf.Grpc.BooksRequest request, grpc::ServerCallContext context) { throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); } } /// <summary>Client for BookService</summary> public partial class BookServiceClient : grpc::ClientBase<BookServiceClient> { /// <summary>Creates a new client for BookService</summary> /// <param name="channel">The channel to use to make remote calls.</param> public BookServiceClient(grpc::Channel channel) : base(channel) { } /// <summary>Creates a new client for BookService that uses a custom <c>CallInvoker</c>.</summary> /// <param name="callInvoker">The callInvoker to use to make remote calls.</param> public BookServiceClient(grpc::CallInvoker callInvoker) : base(callInvoker) { } /// <summary>Protected parameterless constructor to allow creation of test doubles.</summary> protected BookServiceClient() : base() { } /// <summary>Protected constructor to allow creation of configured clients.</summary> /// <param name="configuration">The client configuration.</param> protected BookServiceClient(ClientBaseConfiguration configuration) : base(configuration) { } public virtual global::GrpcVsWcf.Grpc.BooksReply GetBooks(global::GrpcVsWcf.Grpc.BooksRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) { return GetBooks(request, new grpc::CallOptions(headers, deadline, cancellationToken)); } public virtual global::GrpcVsWcf.Grpc.BooksReply GetBooks(global::GrpcVsWcf.Grpc.BooksRequest request, grpc::CallOptions options) { return CallInvoker.BlockingUnaryCall(__Method_GetBooks, null, options, request); } public virtual grpc::AsyncUnaryCall<global::GrpcVsWcf.Grpc.BooksReply> GetBooksAsync(global::GrpcVsWcf.Grpc.BooksRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) { return GetBooksAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken)); } public virtual grpc::AsyncUnaryCall<global::GrpcVsWcf.Grpc.BooksReply> GetBooksAsync(global::GrpcVsWcf.Grpc.BooksRequest request, grpc::CallOptions options) { return CallInvoker.AsyncUnaryCall(__Method_GetBooks, null, options, request); } /// <summary>Creates a new instance of client from given <c>ClientBaseConfiguration</c>.</summary> protected override BookServiceClient NewInstance(ClientBaseConfiguration configuration) { return new BookServiceClient(configuration); } } /// <summary>Creates service definition that can be registered with a server</summary> /// <param name="serviceImpl">An object implementing the server-side handling logic.</param> public static grpc::ServerServiceDefinition BindService(BookServiceBase serviceImpl) { return grpc::ServerServiceDefinition.CreateBuilder() .AddMethod(__Method_GetBooks, serviceImpl.GetBooks).Build(); } } } #endregion
Book, BookRequest, BookReply のコードも出力されたけど、570行もあるので省略。 出力されたコードは、プロジェクトにリンクとして追加しておく。
gRPC のサービスを実装
ツールを実行して出力されたコードは、リクエストとレスポンスのクラス、それにサービスの抽象クラス。 抽象クラスを継承して、サービス本体の処理を実装しないといけない。
using Grpc.Core; using System; using System.Linq; using System.Threading.Tasks; namespace GrpcVsWcf.Grpc { public class BookServiceImpl : BookService.BookServiceBase { static readonly Book[] _books; static BookServiceImpl() { _books = Enumerable.Range(0, 100) .Select(i => new Book() { Id = Guid.NewGuid().ToString(), Title = $"Book{i}", Author = $"Author{i}", Description = $"Description{i}", PublishedAt = DateTime.Today.ToString(), Price = 2000, }) .ToArray(); } public override Task<BooksReply> GetBooks(BooksRequest request, ServerCallContext context) { var result = new BooksReply(); result.Books.AddRange(_books.Take(request.Count)); return Task.FromResult(result); } } }
ベンチマークが目的なので、サービスの実装は単純に、 リクエストで指定された件数分のデータを返すだけにしておく。
WCF のサービスコントラクトとデータコントラクトを定義
gRPC と比較するので、サービスコントラクトとデータコントラクトは
service.proto
から生成したものに極力近づける。
using System.Collections.Generic; using System.Runtime.Serialization; using System.ServiceModel; using System.Threading.Tasks; namespace GrpcVsWcf.Wcf { [DataContract] public class Book { [DataMember] public string Id { get; set; } [DataMember] public string Title { get; set; } [DataMember] public string Description { get; set; } [DataMember] public string Author { get; set; } [DataMember] public int Price { get; set; } [DataMember] public string PublishedAt { get; set; } } [DataContract] public class BooksReply { [DataMember] public List<Book> Books { get; set; } = new List<Book>(); } [DataContract] public class BooksRequest { [DataMember] public int Count { get; set; } } [ServiceContract] public interface IBookService { [OperationContract] Task<BooksReply> GetBooks(BooksRequest request); } }
WCF サービス実装
WCF サービス本体を実装。 こちらも gRPC のサービスに実装を極力近づけておく。
using System; using System.Linq; using System.Threading.Tasks; namespace GrpcVsWcf.Wcf { public class BookService : IBookService { static readonly Book[] _books; static BookService() { _books = Enumerable.Range(0, 100) .Select(i => new Book() { Id = Guid.NewGuid().ToString(), Title = $"Book{i}", Author = $"Author{i}", Description = $"Description{i}", PublishedAt = DateTime.Today.ToString(), Price = 2000, }) .ToArray(); } public Task<BooksReply> GetBooks(BooksRequest request) { var result = new BooksReply(); result.Books.AddRange(_books.Take(request.Count)); return Task.FromResult(result); } } }
gRPC と WCF のホストを実装
クライアントとサービスは別プロセスで動かしてベンチマークを行いたいので、 サービスをホストするプログラムを作成。 gRPC と WCF のサービスは同じプロセスで動かす。
using Grpc.Core; using GrpcVsWcf.Grpc; using GrpcVsWcf.Wcf; using System; using System.ServiceModel; namespace GrpcVsWcf { class Program { static void Main(string[] args) { var tcpHost = StartTcpWcfService(); var namedPipeHost = StartNamedPipeWcfService(); var grpcServer = StartGrpcServer(); Console.WriteLine("Enter で終了します。"); Console.ReadLine(); grpcServer.ShutdownAsync().GetAwaiter().GetResult(); tcpHost.Close(); namedPipeHost.Close(); } static ServiceHost StartTcpWcfService() { var host = new ServiceHost(typeof(Wcf.BookService)); host.AddServiceEndpoint( typeof(IBookService), new NetTcpBinding(), "net.tcp://localhost:8080/BookService"); host.Open(); return host; } static ServiceHost StartNamedPipeWcfService() { var host = new ServiceHost(typeof(Wcf.BookService)); host.AddServiceEndpoint( typeof(IBookService), new NetNamedPipeBinding(), "net.pipe://localhost/BookService"); host.Open(); return host; } static Server StartGrpcServer() { var server = new Server() { Services = { Grpc.BookService.BindService(new BookServiceImpl()), }, Ports = { new ServerPort("localhost", 8090, ServerCredentials.Insecure), } }; server.Start(); return server; } } }
ベンチマークを作成
gRPC と WCF のサービスを呼び出すクライアントのプログラムを作成。 ベンチマークもこのプログラムで行う。 なお、ベンチマークには BenchmarkDotNet を使っている。
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using Grpc.Core; using GrpcVsWcf.Wcf; using System; using System.ServiceModel; using System.ServiceModel.Channels; namespace GrpcVsWcf { public class GrpcVsWcfBenchmark { Channel channel; Grpc.BookService.BookServiceClient client; IBookService wcfTcpChannel; IBookService wcfNamedPipeChannel; [GlobalSetup] public void GlobalSetup() { channel = new Channel("127.0.0.1:8090", ChannelCredentials.Insecure); client = new Grpc.BookService.BookServiceClient(channel); wcfTcpChannel = ChannelFactory<IBookService>.CreateChannel( new NetTcpBinding(), new EndpointAddress("net.tcp://localhost:8080/BookService")); wcfNamedPipeChannel = ChannelFactory<IBookService>.CreateChannel( new NetNamedPipeBinding(), new EndpointAddress("net.pipe://localhost/BookService")); } [GlobalCleanup] public void GlobalCleanup() { channel.ShutdownAsync().GetAwaiter().GetResult(); ((IChannel)wcfTcpChannel).Close(); ((IChannel)wcfNamedPipeChannel).Close(); } [Benchmark] [Arguments(1)] [Arguments(10)] [Arguments(50)] [Arguments(100)] public Grpc.BooksReply Grpc(int count) { return client.GetBooksAsync(new Grpc.BooksRequest() { Count = count }) .GetAwaiter().GetResult(); } [Benchmark] [Arguments(1)] [Arguments(10)] [Arguments(50)] [Arguments(100)] public BooksReply WcfTcp(int count) { return wcfTcpChannel.GetBooks(new BooksRequest() { Count = count }) .GetAwaiter().GetResult(); } [Benchmark] [Arguments(1)] [Arguments(10)] [Arguments(50)] [Arguments(100)] public BooksReply WcfNamedPipe(int count) { return wcfNamedPipeChannel.GetBooks(new BooksRequest() { Count = count }) .GetAwaiter().GetResult(); } } class Program { static void Main(string[] args) { BenchmarkRunner.Run<GrpcVsWcfBenchmark>(); Console.ReadLine(); } } }
ベンチマーク結果
おわりに
ベンチマークをとる前は「WCF の NetTcpBinding と gRPC は同等の速度が出たらいいな」 なんて思っていたけど、とんでもない。 gRPC の方が速かった。圧倒的に。 NetNamedPipeBinding よりも速かったのは完全に予想外。 WCF と比べてごめんなさいするレベルだ。
これはもう積極的に WCF を選ぶ理由は無いな。 CustomBinding 使えば WCF の方はもう少し速くなる可能性はある。 でも、そこに労力使うならその分 gRPC に労力使いたいし、 それでも gRPC を超えそうには思えない。 WCF は .NET Core でクライアントのみサポートされている状況で、 未来も無さそうだし。
gRPC がファーストチョイスでしょ。