gRPC for Beginners
Introduction
I started designing a new microservice that I wanted to write in Go. The service was to be a JSON RPC service over TCP, and the expected consumer servicer I would build using Ruby.
I had some initial concerns regarding the nature of TCP sockets with a highly scalable and distrubuted set of services (this was to be utilised within the BBC so these are genuine concerns to be had for my purposes) and so I decided to do some research.
There are a few issues that I discovered:
- TCP connections aren’t free (so utilise a connection pool)
- Too many simultaneous requests can exhaust open port/file descriptors
- If not careful you can end up with orphaned TCP connections (e.g. if no timeouts configured)
Now these are all things that can be worked around (or I could just build the Go service as a HTTP REST service and some of these problems dissapear). But it seems lots of people have been talking about using Google’s new open-source RPC framework called gRPC and I thought what better time to investigate what it can do.
Google define gRPC as:
A high performance, open source, general RPC framework that puts mobile and HTTP/2 first
How does it work?
One of the initial benefits is the ability to be able to define and codify your service requirements via a .proto
file. The proto file is based around another concept Google have been working on known as Protocol Buffers.
In essence, protocol buffers are an open-source mechanism for serializing structured data.
Once you have your service defined, you can utilise a command line compiler to generate stubs and code in multiple programming languages. So you can generate a client and server with the Go programming language, and then using the same .proto
file generate a client/server with Ruby.
Google have built gRPC on top of the HTTP/2 standard, meaning you get features such as bidirectional streaming, flow control, header compression and multiplexing requests over a single TCP connection.
See here for Google’s “motivation and design principles” around gRPC
Now the reason for this post is that I didn’t find the documentation to be that intuitive. I thought I might be able to help people get started more quickly by detailing the steps in a more succinct fashion than found in Google’s documentation, thus opening up gRPC to more users.
So with this in mind, let’s crack on…
Install gRPC
First thing we need to do is install gRPC’s C based libraries. Once we have this installed we will later install plugin extensions for other programming languages (such as Go and Ruby, but there are other languages available).
One of the things I discovered further along in my research of gRPC (and I wish I had known earlier) was that some commands that are utilised by the language extensions are only available when installing gRPC from source. So that’s what we’ll be doing now:
git clone https://github.com/grpc/grpc.git
cd grpc
git submodule update --init
make
make install
Install Proto Buffer Compiler
Now that we have gRPC installed we also need the compiler for the Protocol Buffer definition file. This is the file that defines our service and which we’ll get round to writing shortly. In order to install the compiler you’ll first need to make sure you have the requisite dependencies installed.
For myself, running this on Mac OS X I just need XCode installed:
sudo xcode-select --install
Once you do that, you can execute the following steps:
git clone git@github.com:google/protobuf.git
./autogen.sh
./configure
make
make check
sudo make install
Note: each Make target took ~10mins each to run
Hello World Proto Definition
Protocol Buffers are designed by Google to be language and platform neutral, and so in theory you can use it with your own RPC implementation. But in reality most people will use gRPC with Protocol Buffers.
So with that said, here is our service definition written using the latest syntax (proto3
) and I’ve named it requester.proto
:
syntax = "proto3";
package requester;
service Requester {
rpc Process (Config) returns (Response) {}
}
message Config {
string data = 1;
}
message Response {
string message = 1;
}
Note: see here for full proto3 syntax documentation
In summary it defines an RPC service that exposes a Process
method which can be called remotely. In reality, it’s the same ‘Hello World’ app provided by the gRPC docs but with some changes in identifiers.
Syntax Explanation
You can see the package
statement near the top, which according to Google’s docs are used to avoid name clashes between protocol messages; but more specifically it effects the way code is compiled/generated.
For example, in Ruby it’ll generate a top level module that utilises that namespace. As you’ll see shortly, I have two nested modules with the same name Requester::Requester
. This is because the package
setting is the top level and the nested module name is because that’s what I named the service
. So be careful what you name it as the compiled code might not be what you want.
Note: because the design has come from Google you’re going to notice lots of design considerations that correlate to their opinions and choices with the Go programming language
In Go, the other language we’re using, the package
value is used (conveniently) as the name of the Go package. Which makes sense as there is a closer correlation in the design of Protocol Buffers and Go vs a dynamic language such as Ruby.
Inside of the service
statement we state that we want an RPC service that has a Process
method and that method accepts something of type Config
and returns something of type Response
. We can then define what Config
and Response
look like, which we do using the message
statement.
So to keep things simple I’ve only used a single property setting for each message, but there is a rich selection of data types you can utilise. In my simple example both properties have a string
type.
You can then access these properties from your code as a nested object field/property. So in Ruby, for example, if you accepted the message Config
as an argument c
to your Process
method then your code would call c.data
.
The numbers assigned to the property (e.g. both data
and message
are assigned the value 1
) are known as ‘tags’. Effectively, tags with a number between 1 and 15 take one byte to encode whereas tags between 16 and 2047 take two bytes to encode.
The idea is that you should reserve the tags 1 through 15 for very frequently occurring message elements. But if you really want all the gory details then I’ll refer you to the encoding documentation.
Auto Generating Service Code
So at this point we have the option of auto-generating ‘service code’ for any of the languages gRPC supports, which is:
- C
- C#
- C++
- Go
- Java
- Node.js
- Objective-C
- PHP
- Python
- Ruby
We’re interested in Go and Ruby as I want to have the RPC service server side running in Go but have the consumer in Ruby. So I’ll first generate the Ruby client stub using the protoc
compiler we installed earlier:
protoc --ruby_out=lib \
--grpc_out=lib \
--plugin=protoc-gen-grpc=`which grpc_ruby_plugin` ./requester.proto
Note: execute
mkdir lib
if that directory doesn’t already exist
This will generate two files requester.rb
and requester_services.rb
inside of the lib
directory we’ve specified. The content of those files looks like the following. The first file being requester.rb
:
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: requester.proto
require 'google/protobuf'
Google::Protobuf::DescriptorPool.generated_pool.build do
add_message "requester.Config" do
optional :name, :string, 1
end
add_message "requester.Response" do
optional :message, :string, 1
end
end
module Requester
Config = Google::Protobuf::DescriptorPool.generated_pool.lookup("requester.Config").msgclass
Response = Google::Protobuf::DescriptorPool.generated_pool.lookup("requester.Response").msgclass
end
Here is the second file requester_services.rb
:
# Generated by the protocol buffer compiler. DO NOT EDIT!
# Source: requester.proto for package 'requester'
require 'grpc'
require 'requester'
module Requester
module Requester
# TODO: add proto service documentation here
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'requester.Requester'
rpc :Process, Config, Response
end
Stub = Service.rpc_stub_class
end
end
We’ll see how to consume these stubs from Ruby in the next section. But now let’s move onto how to use protoc
to generate some Golang stubs:
protoc --go_out=plugins=grpc:pb ./requester.proto
So in the above example the pb
reference is to a folder that has to exist before you run that command. You can name the folder whatever you like obviously, but pb
(protocol buffer) made sense to me.
The file that is generated will be named requester.pb.go
and (as with the Ruby code) we’ll look at how to consume this file in a following section that demonstrates the Go code examples. But for now let’s see the contents of this file (shield your eyes, Go isn’t the most concise programming language):
// Code generated by protoc-gen-go.
// source: requester.proto
// DO NOT EDIT!
/*
Package requester is a generated protocol buffer package.
It is generated from these files:
requester.proto
It has these top-level messages:
Config
Response
*/
package requester
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
const _ = proto.ProtoPackageIsVersion1
type Config struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}
func (m *Config) Reset() { *m = Config{} }
func (m *Config) String() string { return proto.CompactTextString(m) }
func (*Config) ProtoMessage() {}
func (*Config) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
type Response struct {
Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
}
func (m *Response) Reset() { *m = Response{} }
func (m *Response) String() string { return proto.CompactTextString(m) }
func (*Response) ProtoMessage() {}
func (*Response) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func init() {
proto.RegisterType((*Config)(nil), "requester.Config")
proto.RegisterType((*Response)(nil), "requester.Response")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion1
// Client API for Requester service
type RequesterClient interface {
Process(ctx context.Context, in *Config, opts ...grpc.CallOption) (*Response, error)
}
type requesterClient struct {
cc *grpc.ClientConn
}
func NewRequesterClient(cc *grpc.ClientConn) RequesterClient {
return &requesterClient{cc}
}
func (c *requesterClient) Process(
ctx context.Context, in *Config, opts ...grpc.CallOption
) (*Response, error) {
out := new(Response)
err := grpc.Invoke(ctx, "/requester.Requester/Process", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Requester service
type RequesterServer interface {
Process(context.Context, *Config) (*Response, error)
}
func RegisterRequesterServer(s *grpc.Server, srv RequesterServer) {
s.RegisterService(&_Requester_serviceDesc, srv)
}
func _Requester_Process_Handler(
srv interface{}, ctx context.Context, dec func(interface{}) error
) (interface{}, error) {
in := new(Config)
if err := dec(in); err != nil {
return nil, err
}
out, err := srv.(RequesterServer).Process(ctx, in)
if err != nil {
return nil, err
}
return out, nil
}
var _Requester_serviceDesc = grpc.ServiceDesc{
ServiceName: "requester.Requester",
HandlerType: (*RequesterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Process",
Handler: _Requester_Process_Handler,
},
},
Streams: []grpc.StreamDesc{},
}
var fileDescriptor0 = []byte{
// 172 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x2f, 0x4a, 0x2d, 0x2c,
0x4d, 0x2d, 0x2e, 0x49, 0x2d, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x84, 0x0b, 0x28,
0xc9, 0x70, 0xb1, 0x39, 0xe7, 0xe7, 0xa5, 0x65, 0xa6, 0x0b, 0x09, 0x71, 0xb1, 0xe4, 0x25, 0xe6,
0xa6, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, 0x81, 0xd9, 0x4a, 0x2a, 0x5c, 0x1c, 0x41, 0xa9,
0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x42, 0x12, 0x5c, 0xec, 0xb9, 0xa9, 0xc5, 0xc5, 0x89, 0xe9,
0x30, 0x25, 0x30, 0xae, 0x91, 0x03, 0x17, 0x67, 0x10, 0xcc, 0x40, 0x21, 0x63, 0x2e, 0xf6, 0x80,
0xa2, 0xfc, 0x64, 0xa0, 0x94, 0x90, 0xa0, 0x1e, 0xc2, 0x62, 0x88, 0x25, 0x52, 0xc2, 0x48, 0x42,
0x30, 0x93, 0x95, 0x18, 0x9c, 0x8c, 0xb9, 0xa4, 0x32, 0xf3, 0xf5, 0xd2, 0x8b, 0x0a, 0x92, 0xf5,
0x52, 0x2b, 0x12, 0x73, 0x0b, 0x72, 0x52, 0x8b, 0x11, 0x0a, 0x9d, 0xf8, 0xe0, 0xa6, 0x07, 0x80,
0x9c, 0x1f, 0xc0, 0xb8, 0x88, 0x89, 0x29, 0x28, 0x30, 0x89, 0x0d, 0xec, 0x19, 0x63, 0x40, 0x00,
0x00, 0x00, 0xff, 0xff, 0xac, 0xd0, 0xf7, 0x73, 0xdf, 0x00, 0x00, 0x00,
}
Ruby Example
OK, so we’ve defined what our service does: it’s an RPC service that exposes a Process
method that takes an argument. But what that method returns we’ve yet to build (that’s not the responsibility of the definition file).
We’ve used the protoc
compiler to auto-generate some code stubs for us, which handle the setting up of the service. So let’s see how we consume that from Ruby, we’re going to need the following files:
Gemfile
server.rb
client.rb
This is what the contents of those files look like…
Gemfile
source "https://rubygems.org/"
gem "grpc", "~> 0.11"
We only have one dependency, which is the grpc
extension.
server.rb
$: << File.join(File.dirname(__FILE__), "lib")
require "grpc"
require "requester_services"
class RequesterServer < Requester::Requester::Service
def process(config, _unused_call)
Requester::Response.new(message: "Hello #{config.name}")
end
end
s = GRPC::RpcServer.new
s.add_http2_port("0.0.0.0:50051", :this_port_is_insecure)
s.handle(RequesterServer)
s.run_till_terminated
So same set of dependencies pulled in, like with the client. But this time we’re creating a new instance of a class that inherits from our Requester::Requester::Service
auto-generated class.
This is similar in essence to the Template Method Pattern, where we’re now able to define the implementation of the method type process
. But one thing to remember is that you need the 2nd argument _unused_call
that’s passed into the process
method. Remove it and things will break.
Why? I’ve actually no idea. I’ve found nothing in the documentation that explains this, and I’ve sifted through the source code and nothing I could grok to understand why this second (seemingly pointless) argument is there.
From here we create a new gRPC server instance (GRPC::RpcServer
). We then specify the address and port we want the server to listen to. Don’t be fooled in the specific nature of ‘http2’ in the method add_http2_port
, there is no add_http_port
or alternative method.
Also, as before, the :this_port_is_insecure
is required. I don’t really like the design of the code here, but I guess what can you expect from low-level programmers designing code for dynamic languages they typically don’t use.
Next we specify our RequesterServer
to be the instance that handles any incoming requests. Finally we tell the server to run until it’s terminated via a signal such as INT
or TERM
(documented here).
To run this program:
bundle install
bundle exec ruby server.rb
You wont see anything in the output, so let’s move onto the client code…
client.rb
$: << File.join(File.dirname(__FILE__), "lib")
require "grpc"
require "requester_services"
stub = Requester::Requester::Stub.new("localhost:50051", :this_channel_is_insecure)
msg = stub.process(Requester::Config.new(name: "Mark")).message
p "Greeting: #{msg}"
Here we’re loading our grpc dependency and the service stub that was auto-generated for us. Notice that because my protocol buffer definition file specified the package as requester
and the file itself was called requester
I’ve now got this ugly namespace Requester::Requester
.
Again, just be aware of what you’re naming things because to be honest that double named module is annoying for me to look at. I left it like that to demonstrate why it’s important to name things well.
You’ll notice that we pass in :this_channel_is_insecure
to the Stub.new
method. This isn’t an arbitrary value, it needs to be exactly that value otherwise you’ll see errors. I’ve yet to look into using HTTPS/TLS but if you’re interested, then you can find the relevant details on the authentication documentation.
Once we create a new instance of our service, we can now access the process
method that is exposed by our RPC service. Convention in Ruby is lowercase method names, so although we defined it as Process
it’s accessed as process
.
We pass into process
the expected Config
‘type’ (Ruby doesn’t have types as part of the language so they’ve provided us a module/namespace instead to mimic this feature), and finally we call the message
property on the returned object (remember we defined a Response in our protocol buffer definition file that had a message
field).
To run this program:
bundle install
bundle exec ruby client.rb
Which should result in the output:
"Greeting: Hello Mark"
Go Example
We can set up our services to use Go completely or we can mix and match. But let’s see how to use both the client and server from Go. As with Ruby, we’ve defined what our service does: it’s an RPC service that exposes a Process
method that takes an argument. But what that method returns we’ve yet to build.
We’ve used the protoc
compiler to auto-generate some code stubs for us, which handle the setting up of the service. So let’s see how we consume that from Go, we’re going to need the following files:
server.go
client.go
server.go
package main
import (
"log"
"net"
pb "github.com/integralist/test-grpc-custom/pb"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
const (
port = ":50051"
)
type server struct{}
func (s *server) Process(ctx context.Context, in *pb.Config) (*pb.Response, error) {
return &pb.Response{Message: "Hello " + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterRequesterServer(s, &server{})
s.Serve(lis)
}
So the Go variation is fairly straight forward, our main
function listens on the specified port and we start up a new grpc server instance.
From there we take the protocol buffer pb/requester.pb.go
that was generated by the protoc
compiler and call a pre-supplied pb.RegisterRequesterServer
method and pass in a data structure for it to utilise along with the grpc server.
For the server
struct type we associate the required Process
method and define its behaviour. In this case, similar to the Ruby version, we create an instance of the Response
type.
To run this program, execute:
go run server.go
client.go
package main
import (
"log"
"os"
pb "github.com/integralist/test-grpc-custom/pb"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
defaultName = "world"
)
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewRequesterClient(conn)
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
r, err := c.Process(context.Background(), &pb.Config{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
}
With the server program running we can now execute our client to call the server’s Process
method. Again, in summary, we use gRPC’s own Dial
method to call the specified address. The second argument disables the transport security for this particular connection. If you want HTTPS/TLS encryption then you’ll need to read the documentation for those details.
We create a new instance of the auto-generated client, and call the Process
method. Passing along the auto-generated Config
type with the data we want it to receive.
The pb
identifier references the auto-generated protocol buffer package (pb "github.com/integralist/test-grpc-custom/pb"
), and as you’ll probably already know this path is unique to your local setup and where you created that package.
To run this program, execute:
go run client.go Mark
Which should result in the output:
"Greeting: Hello Mark"
Note: if you leave off the argument “Mark”
then the output will default to “Hello world” instead
Conclusion
So there you go. Hopefully you’ve found this break down useful. The principles of gRPC seem promising, and although I’m not keen on the design of the auto-generated code being not as ‘idiomatic’ as you’d expect for a language such as Ruby (I’m not sure what the other language implementations are like) I still think this could be an interesting evolution of the microservices movement.
Update
There are alternatives that work in a similar fashion, one of which is Apache Thrift and is defined as being a “software framework, for scalable cross-language services development”. But unfortunately it doesn’t support the Go programming language, which is a requirement for me. But interesting nonetheless.