Building Microservices using gRPC on Ruby
Today, REST with JSON is the most popular framework amongst web developers for network communication. But, it is not very suitable for a microservice architecture mainly because of latency added by JSON data transmission / serializing / deserializing.
My quest for finding an optimal network communication framework for microservices brought me to gRPC.
*gRPC is a modern, open source remote procedure call (RPC) framework that can run anywhere. It enables client and server applications to communicate transparently, and makes it easier to build connected systems.*
To read more about benefits of gRPC, visit the official site here.
Serialization in gRPC is based on Protocol Buffers, a language and platform independent serialization mechanism for structured data.
*Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data – think XML, but smaller, faster, and simpler.*
In the remaining part of this post, I will be walking you through setting up a simple gRPC server from scratch on Ruby. Let's build Snip - a dummy URL shortener!
We will divide our code structure into 3 separate repositories:
snip
: contains the proto definitions and converted ruby files for client communication. Basically, this is like an interface between client and server, specifying the RPC methods, and the request and response formats.snip-service
: Service implementation for the RPC methods (This is where the gRPC server sits).X-app
: This is any application who wishes to call snip-service for shortening URLs.
snip
will be packaged as a gem, and included in both snip-service
and X-app
.
Step 0: Install dependencies
Make sure you have ruby and bundler setup working. Then, install the required gems for grpc:
gem install grpc
gem install grpc-tools
PART A: snip gem
Step 1: Setup snip gem
snip
is supposed to be a ruby gem, so you could use the bundler scaffold for creating it.
bundle gem snip
Add this to snip.gemspec
file:
spec.add_dependency "grpc"
Step 2: Define proto files
Let's create a new file proto/snip.proto
syntax = "proto3";
package snip;
service UrlSnipService {
rpc snip_it(SnipRequest) returns (SnipResponse) {}
}
message SnipRequest {
string url = 1;
}
message SnipResponse {
string url = 1;
}
Step 3: Generate ruby bindings for the proto definition
Next, we are going to convert the defined proto files to ruby bindings, which are ultimately going to be used by both the client and the server.
grpc_tools_ruby_protoc -Iproto --ruby_out=lib --grpc_out=lib proto/snip.proto
My snip
directory tree after this command:
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── lib
│ ├── proto
│ │ ├── snip_pb.rb
│ │ └── snip_services_pb.rb
│ └── snip
│ └── version.rb
├── proto
│ └── snip.proto
└── snip.gemspec
Note that snip_pb.rb
and snip-services_pb.rb
are the important files which are required for client-server communication. This is how they should look:
snip_pb.rb
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: snip.proto
require 'google/protobuf'
Google::Protobuf::DescriptorPool.generated_pool.build do
add_message "snip.SnipRequest" do
optional :url, :string, 1
end
add_message "snip.SnipResponse" do
optional :url, :string, 1
end
end
module Snip
SnipRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("snip.SnipRequest").msgclass
SnipResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("snip.SnipResponse").msgclass
end
snip_services_pb.rb
# Generated by the protocol buffer compiler. DO NOT EDIT!
# Source: snip.proto for package 'snip'
require 'grpc'
require 'snip_pb'
module Snip
module UrlSnipService
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'snip.UrlSnipService'
rpc :snip_it, SnipRequest, SnipResponse
end
Stub = Service.rpc_stub_class
end
end
Voila! You are done with your snip
gem. We will move onto the service implementation next.
PART B: snip-service & gRPC server
Step 1: Setup
Add the following to your Gemfile
in snip-service
gem 'snip',:git => "https://github.com/shiladitya-bits/snip",:branch => 'master'
gem 'grpc', '~> 1.0'
Replace snip
gem path with wherever you have setup the gem to be in.
Step 2: Service implementation
lib/services/snip_service.rb
require 'grpc'
require 'snip_services_pb'
class SnipService < Snip::UrlSnipService::Service
def snip_it(snip_req, _unused_call)
puts "Received URL snip request for #{snip_req.url}"
Snip::SnipResponse.new(url: snip_req.url)
end
end
snip_it
is the RPC method we defined in our proto. Let us look at the 2 parameters here:
snip_req
- the request proto object sent by client in the format as defined in protoSnip::SnipRequest
_unused_call
- this contains other metadata sent by client. We will talk about how to send metadata in another post later.
You need to return an object of Snip::SnipResponse
from this method as your response. For keeping the implementation simple, we are sending back the same URL as sent by the client.
Step 3: Setup your gRPC server
Now that your service implementation is ready, let us setup the gRPC server that will be serving calls to your service.
lib/start_server.rb
#!/usr/bin/env ruby
require 'rubygems'
require 'snip_services_pb'
require_relative 'services/snip_service'
class SnipServer
class << self
def start
start_grpc_server
end
private
def start_grpc_server
@server = GRPC::RpcServer.new
@server.add_http2_port("0.0.0.0:50052", :this_port_is_insecure)
@server.handle(SnipService)
@server.run_till_terminated
end
end
end
SnipServer.start
A very simple ruby class which starts the server on port 50052 on running this:
bundle exec lib/start_server.rb
You might need to do a chmod +x lib/start_server.rb
for giving executable permissions.
PART C: X-app client
Step 1: Setup
Same as the snip-service
Gemfile
, you need to include snip
gem in your client as well.
gem 'snip',:git => "https://github.com/shiladitya-bits/snip",:branch => 'master'
gem 'grpc', '~> 1.0'
Step 2: Last step: RPC call!
Just a final piece of code which helps you test out your gRPC server:
test/test_snip_service
#!/usr/bin/env ruby
require 'grpc'
require 'snip_services_pb'
def test_single_call
stub = Snip::UrlSnipService::Stub.new('0.0.0.0:50052', :this_channel_is_insecure)
req = Snip::SnipRequest.new(url: 'http://shiladitya-bits.github.io')
resp_obj = stub.snip_it(req)
puts "Snipped URL: #{resp_obj.url}"
end
test_single_call
There you go! Your first working gRPC communication is complete!
The snip
and snip-service
repositories are available on Github. You can find a sample client call inside snip-service
itself in test/test_snip_service
.
This is the end of this post. Stay tuned for more posts on how to integrate a gRPC server in your existing Rails apps, and how to deploy gRPC servers using Docker.
Check out my latest post on Dockerizing your gRPC service