gRPC vs. WCF

はじめに

仕事で開発に携わっているアプリでは通信に 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();
        }
    }
}

ベンチマーク結果

f:id:griefworker:20180706100859p:plain

おわりに

ベンチマークをとる前は「WCF の NetTcpBinding と gRPC は同等の速度が出たらいいな」 なんて思っていたけど、とんでもない。 gRPC の方が速かった。圧倒的に。 NetNamedPipeBinding よりも速かったのは完全に予想外。 WCF と比べてごめんなさいするレベルだ。

これはもう積極的に WCF を選ぶ理由は無いな。 CustomBinding 使えば WCF の方はもう少し速くなる可能性はある。 でも、そこに労力使うならその分 gRPC に労力使いたいし、 それでも gRPC を超えそうには思えない。 WCF .NET Core でクライアントのみサポートされている状況で、 未来も無さそうだし。

gRPC がファーストチョイスでしょ。