├── .gitignore ├── README.md ├── go.mod ├── go.sum ├── grpc-protobuf ├── grpcprotobuf.go └── proto │ ├── api.pb.go │ └── api.proto ├── grpc_protobuf_test.go ├── http-json └── httpjson.go └── http_json_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | benchmark-grpc-protobuf-vs-http-json.test 3 | grpcprotobuf.cpu 4 | httpjson.cpu -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### gRPC+Protobuf or JSON+HTTP? 2 | 3 | This repository contains 2 equal APIs: gRPC using Protobuf and JSON over HTTP. The goal is to run benchmarks for 2 approaches and compare them. APIs have 1 endpoint to create user, containing validation of request. Request, validation and response are the same in 2 packages, so we're benchmarking only mechanism itself. Benchmarks also include response parsing. 4 | 5 | ### Requirements 6 | 7 | - Go 1.11 8 | 9 | ### Run tests 10 | 11 | Run benchmarks: 12 | ``` 13 | GO111MODULE=on go test -bench=. -benchmem 14 | ``` 15 | 16 | ### Results 17 | 18 | ``` 19 | goos: darwin 20 | goarch: amd64 21 | BenchmarkGRPCProtobuf-8 10000 117649 ns/op 7686 B/op 154 allocs/op 22 | BenchmarkHTTPJSON-8 10000 105837 ns/op 8932 B/op 116 allocs/op 23 | PASS 24 | ok github.com/plutov/benchmark-grpc-protobuf-vs-http-json 4.340s 25 | ``` 26 | 27 | They are almost the same, HTTP+JSON is a bit faster and has less allocs/op. 28 | 29 | ### CPU usage comparison 30 | 31 | This will create an executable `benchmark-grpc-protobuf-vs-http-json.test` and the profile information will be stored in `grpcprotobuf.cpu` and `httpjson.cpu`: 32 | 33 | ``` 34 | GO111MODULE=on go test -bench=BenchmarkGRPCProtobuf -cpuprofile=grpcprotobuf.cpu 35 | GO111MODULE=on go test -bench=BenchmarkHTTPJSON -cpuprofile=httpjson.cpu 36 | ``` 37 | 38 | Check CPU usage per approach using: 39 | 40 | ``` 41 | go tool pprof grpcprotobuf.cpu 42 | go tool pprof httpjson.cpu 43 | ``` 44 | 45 | My results show that Protobuf consumes less ressources, around **30% less**. 46 | 47 | ### gRPC definition 48 | 49 | - Install [Go](https://golang.org/dl/) 50 | - Install [Protocol Buffers](https://github.com/google/protobuf/releases) 51 | - Install protoc plugin: `go get github.com/golang/protobuf/proto github.com/golang/protobuf/protoc-gen-go` 52 | 53 | ``` 54 | protoc --go_out=plugins=grpc:. grpc-protobuf/proto/api.proto 55 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plutov/benchmark-grpc-protobuf-vs-http-json 2 | 3 | require ( 4 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 5 | github.com/golang/protobuf v0.0.0-20170920220647-130e6b02ab05 6 | github.com/plutov/benchmark-grpc-protobuf-vs-http-json v0.0.0-20171030061702-48899205a6cd 7 | golang.org/x/net v0.0.0-20171016235512-1087133bc4af 8 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect 9 | golang.org/x/text v0.0.0-20171013141220-c01e4764d870 // indirect 10 | google.golang.org/genproto v0.0.0-20171002232614-f676e0f3ac63 // indirect 11 | google.golang.org/grpc v1.7.0 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 2 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 3 | github.com/golang/protobuf v0.0.0-20170920220647-130e6b02ab05 h1:Kesru7U6Mhpf/x7rthxAKnr586VFmoE2NdEvkOKvfjg= 4 | github.com/golang/protobuf v0.0.0-20170920220647-130e6b02ab05/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 5 | github.com/plutov/benchmark-grpc-protobuf-vs-http-json v0.0.0-20171030061702-48899205a6cd h1:P+XP10rVeRqNkikXWBKD43ork/TIjTa4x/To+GDv4V8= 6 | github.com/plutov/benchmark-grpc-protobuf-vs-http-json v0.0.0-20171030061702-48899205a6cd/go.mod h1:yMXiz/eRs4JoXT+NDrjqCJ5f1/XvidVg0IzuIwkEdFI= 7 | golang.org/x/net v0.0.0-20171016235512-1087133bc4af h1:m+6nef2jgKhCUz/tHl2z4BIaaJ5g21A4j5eDkjrcrsc= 8 | golang.org/x/net v0.0.0-20171016235512-1087133bc4af/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 9 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 10 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 11 | golang.org/x/text v0.0.0-20171013141220-c01e4764d870 h1:xdgOfOjH4d435yr21ATHejC9Un1tBMu4Scm2B7DUbmI= 12 | golang.org/x/text v0.0.0-20171013141220-c01e4764d870/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 13 | google.golang.org/genproto v0.0.0-20171002232614-f676e0f3ac63 h1:yNBw5bwywOTguAu+h6SkCUaWdEZ7ZXgfiwb2YTN1eQw= 14 | google.golang.org/genproto v0.0.0-20171002232614-f676e0f3ac63/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 15 | google.golang.org/grpc v1.7.0 h1:VcvMlnRMpb70sBq6RF2LWnHakw+w6pO9ttP6kmXWhug= 16 | google.golang.org/grpc v1.7.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 17 | -------------------------------------------------------------------------------- /grpc-protobuf/grpcprotobuf.go: -------------------------------------------------------------------------------- 1 | package grpcprotobuf 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net" 7 | "net/mail" 8 | 9 | "github.com/plutov/benchmark-grpc-protobuf-vs-http-json/grpc-protobuf/proto" 10 | "golang.org/x/net/context" 11 | "google.golang.org/grpc" 12 | ) 13 | 14 | // Start entrypoint 15 | func Start() { 16 | lis, _ := net.Listen("tcp", ":60000") 17 | 18 | srv := grpc.NewServer() 19 | proto.RegisterAPIServer(srv, &Server{}) 20 | log.Println(srv.Serve(lis)) 21 | } 22 | 23 | // Server type 24 | type Server struct{} 25 | 26 | // CreateUser handler 27 | func (s *Server) CreateUser(ctx context.Context, in *proto.User) (*proto.Response, error) { 28 | validationErr := validate(in) 29 | if validationErr != nil { 30 | return &proto.Response{ 31 | Code: 500, 32 | Message: validationErr.Error(), 33 | }, validationErr 34 | } 35 | 36 | in.Id = "1000000" 37 | return &proto.Response{ 38 | Code: 200, 39 | Message: "OK", 40 | User: in, 41 | }, nil 42 | } 43 | 44 | func validate(in *proto.User) error { 45 | _, err := mail.ParseAddress(in.Email) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | if len(in.Name) < 4 { 51 | return errors.New("Name is too short") 52 | } 53 | 54 | if len(in.Password) < 4 { 55 | return errors.New("Password is too weak") 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /grpc-protobuf/proto/api.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: grpc-protobuf/proto/api.proto 3 | 4 | /* 5 | Package proto is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | grpc-protobuf/proto/api.proto 9 | 10 | It has these top-level messages: 11 | User 12 | Response 13 | */ 14 | package proto 15 | 16 | import proto1 "github.com/golang/protobuf/proto" 17 | import fmt "fmt" 18 | import math "math" 19 | 20 | import ( 21 | context "golang.org/x/net/context" 22 | grpc "google.golang.org/grpc" 23 | ) 24 | 25 | // Reference imports to suppress errors if they are not otherwise used. 26 | var _ = proto1.Marshal 27 | var _ = fmt.Errorf 28 | var _ = math.Inf 29 | 30 | // This is a compile-time assertion to ensure that this generated file 31 | // is compatible with the proto package it is being compiled against. 32 | // A compilation error at this line likely means your copy of the 33 | // proto package needs to be updated. 34 | const _ = proto1.ProtoPackageIsVersion2 // please upgrade the proto package 35 | 36 | type User struct { 37 | Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` 38 | Email string `protobuf:"bytes,2,opt,name=email" json:"email,omitempty"` 39 | Name string `protobuf:"bytes,3,opt,name=name" json:"name,omitempty"` 40 | Password string `protobuf:"bytes,4,opt,name=password" json:"password,omitempty"` 41 | } 42 | 43 | func (m *User) Reset() { *m = User{} } 44 | func (m *User) String() string { return proto1.CompactTextString(m) } 45 | func (*User) ProtoMessage() {} 46 | func (*User) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 47 | 48 | func (m *User) GetId() string { 49 | if m != nil { 50 | return m.Id 51 | } 52 | return "" 53 | } 54 | 55 | func (m *User) GetEmail() string { 56 | if m != nil { 57 | return m.Email 58 | } 59 | return "" 60 | } 61 | 62 | func (m *User) GetName() string { 63 | if m != nil { 64 | return m.Name 65 | } 66 | return "" 67 | } 68 | 69 | func (m *User) GetPassword() string { 70 | if m != nil { 71 | return m.Password 72 | } 73 | return "" 74 | } 75 | 76 | type Response struct { 77 | Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"` 78 | Code uint64 `protobuf:"varint,2,opt,name=code" json:"code,omitempty"` 79 | User *User `protobuf:"bytes,3,opt,name=user" json:"user,omitempty"` 80 | } 81 | 82 | func (m *Response) Reset() { *m = Response{} } 83 | func (m *Response) String() string { return proto1.CompactTextString(m) } 84 | func (*Response) ProtoMessage() {} 85 | func (*Response) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 86 | 87 | func (m *Response) GetMessage() string { 88 | if m != nil { 89 | return m.Message 90 | } 91 | return "" 92 | } 93 | 94 | func (m *Response) GetCode() uint64 { 95 | if m != nil { 96 | return m.Code 97 | } 98 | return 0 99 | } 100 | 101 | func (m *Response) GetUser() *User { 102 | if m != nil { 103 | return m.User 104 | } 105 | return nil 106 | } 107 | 108 | func init() { 109 | proto1.RegisterType((*User)(nil), "proto.User") 110 | proto1.RegisterType((*Response)(nil), "proto.Response") 111 | } 112 | 113 | // Reference imports to suppress errors if they are not otherwise used. 114 | var _ context.Context 115 | var _ grpc.ClientConn 116 | 117 | // This is a compile-time assertion to ensure that this generated file 118 | // is compatible with the grpc package it is being compiled against. 119 | const _ = grpc.SupportPackageIsVersion4 120 | 121 | // Client API for API service 122 | 123 | type APIClient interface { 124 | CreateUser(ctx context.Context, in *User, opts ...grpc.CallOption) (*Response, error) 125 | } 126 | 127 | type aPIClient struct { 128 | cc *grpc.ClientConn 129 | } 130 | 131 | func NewAPIClient(cc *grpc.ClientConn) APIClient { 132 | return &aPIClient{cc} 133 | } 134 | 135 | func (c *aPIClient) CreateUser(ctx context.Context, in *User, opts ...grpc.CallOption) (*Response, error) { 136 | out := new(Response) 137 | err := grpc.Invoke(ctx, "/proto.API/CreateUser", in, out, c.cc, opts...) 138 | if err != nil { 139 | return nil, err 140 | } 141 | return out, nil 142 | } 143 | 144 | // Server API for API service 145 | 146 | type APIServer interface { 147 | CreateUser(context.Context, *User) (*Response, error) 148 | } 149 | 150 | func RegisterAPIServer(s *grpc.Server, srv APIServer) { 151 | s.RegisterService(&_API_serviceDesc, srv) 152 | } 153 | 154 | func _API_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 155 | in := new(User) 156 | if err := dec(in); err != nil { 157 | return nil, err 158 | } 159 | if interceptor == nil { 160 | return srv.(APIServer).CreateUser(ctx, in) 161 | } 162 | info := &grpc.UnaryServerInfo{ 163 | Server: srv, 164 | FullMethod: "/proto.API/CreateUser", 165 | } 166 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 167 | return srv.(APIServer).CreateUser(ctx, req.(*User)) 168 | } 169 | return interceptor(ctx, in, info, handler) 170 | } 171 | 172 | var _API_serviceDesc = grpc.ServiceDesc{ 173 | ServiceName: "proto.API", 174 | HandlerType: (*APIServer)(nil), 175 | Methods: []grpc.MethodDesc{ 176 | { 177 | MethodName: "CreateUser", 178 | Handler: _API_CreateUser_Handler, 179 | }, 180 | }, 181 | Streams: []grpc.StreamDesc{}, 182 | Metadata: "grpc-protobuf/proto/api.proto", 183 | } 184 | 185 | func init() { proto1.RegisterFile("grpc-protobuf/proto/api.proto", fileDescriptor0) } 186 | 187 | var fileDescriptor0 = []byte{ 188 | // 213 bytes of a gzipped FileDescriptorProto 189 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0x41, 0x4b, 0xc4, 0x30, 190 | 0x10, 0x85, 0xdd, 0x6e, 0x56, 0xd7, 0x59, 0x50, 0x18, 0x3c, 0x84, 0x05, 0x51, 0x7a, 0xf2, 0xa0, 191 | 0x5b, 0x68, 0x7f, 0x81, 0x78, 0xf2, 0x26, 0x01, 0x0f, 0x82, 0x97, 0xb4, 0x19, 0x4b, 0xc0, 0x36, 192 | 0x21, 0xd3, 0xe2, 0xdf, 0x97, 0x4e, 0x5b, 0xf1, 0x94, 0xf7, 0xbe, 0x0c, 0xf3, 0xde, 0xc0, 0x6d, 193 | 0x9b, 0x62, 0xf3, 0x14, 0x53, 0x18, 0x42, 0x3d, 0x7e, 0x15, 0x22, 0x0a, 0x1b, 0xfd, 0x49, 0x14, 194 | 0xee, 0xe4, 0xc9, 0x3f, 0x41, 0xbd, 0x33, 0x25, 0xbc, 0x82, 0xcc, 0x3b, 0xbd, 0xb9, 0xdf, 0x3c, 195 | 0x5c, 0x9a, 0xcc, 0x3b, 0xbc, 0x81, 0x1d, 0x75, 0xd6, 0x7f, 0xeb, 0x4c, 0xd0, 0x6c, 0x10, 0x41, 196 | 0xf5, 0xb6, 0x23, 0xbd, 0x15, 0x28, 0x1a, 0x8f, 0xb0, 0x8f, 0x96, 0xf9, 0x27, 0x24, 0xa7, 0x95, 197 | 0xf0, 0x3f, 0x9f, 0x7f, 0xc0, 0xde, 0x10, 0xc7, 0xd0, 0x33, 0xa1, 0x86, 0x8b, 0x8e, 0x98, 0x6d, 198 | 0x4b, 0x4b, 0xcc, 0x6a, 0xa7, 0xad, 0x4d, 0x70, 0x24, 0x51, 0xca, 0x88, 0xc6, 0x3b, 0x50, 0x23, 199 | 0x53, 0x92, 0xa4, 0x43, 0x79, 0x98, 0x4b, 0x9f, 0xa6, 0xaa, 0x46, 0x3e, 0xca, 0x0a, 0xb6, 0xcf, 200 | 0x6f, 0xaf, 0xf8, 0x08, 0xf0, 0x92, 0xc8, 0x0e, 0x24, 0x57, 0xfc, 0x9f, 0x3b, 0x5e, 0x2f, 0x66, 201 | 0x6d, 0x90, 0x9f, 0xd5, 0xe7, 0x42, 0xaa, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x00, 0x44, 202 | 0x81, 0x1c, 0x01, 0x00, 0x00, 203 | } 204 | -------------------------------------------------------------------------------- /grpc-protobuf/proto/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | 5 | service API { 6 | rpc CreateUser(User) returns (Response) {} 7 | } 8 | 9 | message User { 10 | string id = 1; 11 | string email = 2; 12 | string name = 3; 13 | string password = 4; 14 | } 15 | 16 | message Response { 17 | string message = 1; 18 | uint64 code = 2; 19 | User user = 3; 20 | } 21 | -------------------------------------------------------------------------------- /grpc_protobuf_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/plutov/benchmark-grpc-protobuf-vs-http-json/grpc-protobuf" 8 | "github.com/plutov/benchmark-grpc-protobuf-vs-http-json/grpc-protobuf/proto" 9 | "golang.org/x/net/context" 10 | g "google.golang.org/grpc" 11 | ) 12 | 13 | func init() { 14 | go grpcprotobuf.Start() 15 | time.Sleep(time.Second) 16 | } 17 | 18 | func BenchmarkGRPCProtobuf(b *testing.B) { 19 | conn, err := g.Dial("127.0.0.1:60000", g.WithInsecure()) 20 | if err != nil { 21 | b.Fatalf("grpc connection failed: %v", err) 22 | } 23 | 24 | client := proto.NewAPIClient(conn) 25 | 26 | for n := 0; n < b.N; n++ { 27 | doGRPC(client, b) 28 | } 29 | } 30 | 31 | func doGRPC(client proto.APIClient, b *testing.B) { 32 | resp, err := client.CreateUser(context.Background(), &proto.User{ 33 | Email: "foo@bar.com", 34 | Name: "Bench", 35 | Password: "bench", 36 | }) 37 | 38 | if err != nil { 39 | b.Fatalf("grpc request failed: %v", err) 40 | } 41 | 42 | if resp == nil || resp.Code != 200 || resp.User == nil || resp.User.Id != "1000000" { 43 | b.Fatalf("grpc response is wrong: %v", resp) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /http-json/httpjson.go: -------------------------------------------------------------------------------- 1 | package httpjson 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log" 7 | "net/http" 8 | "net/mail" 9 | ) 10 | 11 | // Start entrypoint 12 | func Start() { 13 | http.HandleFunc("/", CreateUser) 14 | log.Println(http.ListenAndServe(":60001", nil)) 15 | } 16 | 17 | // User type 18 | type User struct { 19 | ID string `json:"id"` 20 | Email string `json:"email"` 21 | Name string `json:"name"` 22 | Password string `json:"password"` 23 | } 24 | 25 | // Response type 26 | type Response struct { 27 | Message string `json:"message"` 28 | Code int `json:"code"` 29 | User *User `json:"user"` 30 | } 31 | 32 | // CreateUser handler 33 | func CreateUser(w http.ResponseWriter, r *http.Request) { 34 | decoder := json.NewDecoder(r.Body) 35 | var user User 36 | decoder.Decode(&user) 37 | defer r.Body.Close() 38 | 39 | w.Header().Set("Content-Type", "application/json") 40 | validationErr := validate(user) 41 | if validationErr != nil { 42 | json.NewEncoder(w).Encode(Response{ 43 | Code: 500, 44 | Message: validationErr.Error(), 45 | }) 46 | return 47 | } 48 | 49 | user.ID = "1000000" 50 | json.NewEncoder(w).Encode(Response{ 51 | Code: 200, 52 | Message: "OK", 53 | User: &user, 54 | }) 55 | } 56 | 57 | func validate(in User) error { 58 | _, err := mail.ParseAddress(in.Email) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | if len(in.Name) < 4 { 64 | return errors.New("Name is too short") 65 | } 66 | 67 | if len(in.Password) < 4 { 68 | return errors.New("Password is too weak") 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /http_json_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/plutov/benchmark-grpc-protobuf-vs-http-json/http-json" 11 | ) 12 | 13 | func init() { 14 | go httpjson.Start() 15 | time.Sleep(time.Second) 16 | } 17 | 18 | func BenchmarkHTTPJSON(b *testing.B) { 19 | client := &http.Client{} 20 | 21 | for n := 0; n < b.N; n++ { 22 | doPost(client, b) 23 | } 24 | } 25 | 26 | func doPost(client *http.Client, b *testing.B) { 27 | u := &httpjson.User{ 28 | Email: "foo@bar.com", 29 | Name: "Bench", 30 | Password: "bench", 31 | } 32 | buf := new(bytes.Buffer) 33 | json.NewEncoder(buf).Encode(u) 34 | 35 | resp, err := client.Post("http://127.0.0.1:60001/", "application/json", buf) 36 | if err != nil { 37 | b.Fatalf("http request failed: %v", err) 38 | } 39 | 40 | defer resp.Body.Close() 41 | 42 | // We need to parse response to have a fair comparison as gRPC does it 43 | var target httpjson.Response 44 | decodeErr := json.NewDecoder(resp.Body).Decode(&target) 45 | if decodeErr != nil { 46 | b.Fatalf("unable to decode json: %v", decodeErr) 47 | } 48 | 49 | if target.Code != 200 || target.User == nil || target.User.ID != "1000000" { 50 | b.Fatalf("http response is wrong: %v", resp) 51 | } 52 | } 53 | --------------------------------------------------------------------------------