gRPC for Beginners

Datetime:2016-08-23 03:08:19          Topic: Ruby           Share

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:

  1. TCP connections aren't free (so utilise a connection pool)
  2. Too many simultaneous requests can exhaust open port/file descriptors
  3. 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 where as 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++
  • 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.

Links





About List