In the olden days, a software application was built as a large monolith, and It’s still being done. However, in recent times microservices architecture has become a popular choice for developing software applications. In a microservice architecture, the microservices often need to communicate with each other. Compared to the traditional RESTful web API, a gRPC based RPC framework can be a better alternative for microservices communication.
Do you want to know more about gRPC, the remote procedure call (RPC) framework used by companies like Netflix and Dropbox?
Let’s get started:
What is microservices architecture?
A microservice architecture consists of many (often hundreds) of small, autonomous, self-contained services. A microservice is built around business capability. Some of the important characteristics of microservice architecture are:
- Microservices are modelled around business capabilities.
- Microservices are independently deployable.
- A microservices encapsulates the data it owns. If one microservice needs to get data from another, it should call API.
- A microservice should be small in size.
Microservices inter-process communication
A microservices-based software system requires applications to talk to each other using an inter-process communication mechanism. Microservices can communicate with each other as
- Synchronous communication (request-reply): In this pattern, a service calls an API exposed by another service and waits for the response. The API must have well-defined semantics and versioning.
- Asynchronous communication: In this pattern, a service sends a message, without waiting for a response from another service. And, one or more services can process the message.
A most common way of implementing a request-reply style of communication is by using RESTful services, typically implemented as JSON payload over HTTP. However, RESTful services can be bulky, inefficient, and limiting for many use cases. The bulkiness of RESTful services is because of the fact that most implementation relies on JSON, a text-based encoding format. The JSON format uses lots of space compared to the binary format.
There are binary encoding formats of JSON (MessagePack, BSON, SMILE, etc.); however, their adoption is not widespread.
Another issue with using JSON-based API is that schema support is optional. The most common way for an application to advertise API is by using OpenAPI spec, but this is not tightly integrated with the RESTful architecture style. It’s very common to see OpenAPI spec and implementation drifting in due course.
Why gRPC for microservice communication?
To solve issues associated with RESTful services, we need a modern inter-process communication system that is scalable and more efficient than RESTful services. Enter gRPC, a modern, open-source RPC framework.
The gRPC framework is based on binary encoding format protocol buffer and implemented on top of HTTP/2. It’s strongly typed, polyglot, and provides both client and server-side streaming.
In gRPC, a client application can directly call a method on a distributed server application on a different machine as if it were a local method.
So, the question is, should you move all our APIs to use gRPC?
Probably No !!
Before we answer, let’s understand some facts about APIs. There are two types of APIs:
- Public API: these APIs are consumed by client applications, typically browsers, mobile applications, or other applications. For example – GitHub public APIs.
- Private API: these APIs are not exposed to the outside world, and it’s mainly used for inter-service communication within your application.
The de facto standard for using Public API is REST over HTTP. For private APIs, you can think of using gRPC; however, you must be aware of trade-offs associated with introducing a new technology stack vs gRPC benefits. Moreover, supporting gRPC on a browser-based application is not straightforward.
One of the possible gRPC-based architectures is to have API Gateway/BFF in front of the microservices and make all internal communication using gRPC.
Advantages of gRPC
There are many advantages of using gRPC. That’s why it’s widely adopted by companies like Netflix and Dropbox. Let’s understand some of the advantages.
Faster compared to JSON based RESTful APIs
The gRPC is much faster compared to traditional JSON-based REST APIs. The gRPC is based on binary protocol-buffer format, which is very lightweight compared to text-based formats such as JSON.
For example, JSON encoding takes 81 bytes for the below payload, and the same payload in protocol buffers takes 33 bytes (source – Chapter 4 – Designing Data-intensive Applications by Martin Kelpmann. If you haven’t read this book, you have missed something !!)
{
"serName": "Martin",
"favoriteNumber": 1337,
"interests": [" daydreaming", "hacking"]
}
It’s possible to implement RESTful APIs on top of the protocol buffer, but why would you want to do that?
Another reason which makes gRPC faster is that it’s implemented on top of HTTP/2. How fast? Check this comparison of HTTP/1.1 vs HTTP/2.
Strong type and well-defined interface
gRPC introduces a well-defined service interface and strong type that helps us avoid run time errors. For the client application, it’s as good as calling the local method. For example, you can define a gRPC service as:
service ProductService {
rpc getProduct(GetProductRequest) returns (GetProductResponse);
}
Also, you can call this from a gRPC client as:
var productResponse = productServiceBlockingStub.getProduct(productRequest);
As you can see, calling the gRPC server is as good as calling a local method.
Polyglot
gRPC supports multiple programming languages. You can write microservices in the programming language you deem fit and not worry about interoperability issues.
Stream support
The gRPC supports both client and server-side streaming. Enabling these features in your application is as simple as changing the service definition. For example, server-side streaming is enabled by just declaring a response with the stream
keyword.
service ProductService {
rpc getProduct(GetProductRequest) returns (stream GetProductResponse);
}
Other features
gRPC has in-built support for resiliency, authentication, encryption, compression, load-balancing, and error handling. The gRPC is a proud member of the Cloud Native Computing Foundation (CNCF), and many modern frameworks provide native support for gRPC.
Code sample
You can find the working code example of this article at GitHub . To run code, clone repository, and import grpc-example
as Gradle project. To build the project and generate client and server stubs, run the command gradlew clean build
. You can start the gRPC server in IDE by running the main method of ProductServer. The gRPC server runs on localhost:8081. To execute RPC, run the main method of the ProductClient
from order-service.
Implementing gRPC client and server
Before we start, let’s try to understand some important concepts of gRPC. A gRPC application consists of three important components:
- Protocol Buffer: it defines messages and services exposed by the server application. gRPC services send and receive data as Protocol Buffer (Protobuf) messages.
- Server: the server application implements and exposes RPC services.
- Client: the client application consumes RPC services exposed by the server.
What are protocol buffers?
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
Google
Protocol buffers are nothing but interface definition language (IDL) used to define API contracts in gRPC. In gRPC, you define API contracts in .proto
files.
You define gRPC APIs by declaring messages and services. For example, if the order-service (gRPC client) calls the product-service (gRPC server) to fetch information about the product by passing productId
, then you can define service definition in the protocol buffer as :
syntax = "proto3";
package dev.techdozo.product.api;
message GetProductRequest {
string productId = 1;
}
message GetProductResponse {
string name = 1;
string description = 2;
double price = 3;
}
service ProductService {
rpc getProduct(GetProductRequest) returns (GetProductResponse);
}
Let’s understand the different elements of the proto file.
Protobuf message
The message is a binary data structure exchanged between the client and the server. The message and service are declared in .proto
files. If you want, you can create separate files for messages and services. For example, resources.proto
for messages, and service.proto
for service definition.
Field numbers, such as name = 1
, are used to identify fields in the binary encoded data. This means you can’t change the field number from one version to another. Furthermore, this helps in backward and forward compatibility – clients and services can ignore field numbers they don’t know about.
Service
A service is a remote method that is exposed by the server. The client can use the generated stub to call the remote method on the server. In the above example, product-service exposes an RPC method rpc getProduct(GetProductRequest) returns (GetProductResponse);
. This RPC call returns information about the Product as GetProductResponse
.
Code generation
You can use protoc
compiler to generate client and server code. The protoc
compiler supports code generation in many different languages.
For our example, we will use Protobuf Gradle Plugin to generate source code in java. The protocol buffer plugin assembles the Protobuf Compiler (protoc
) command line and use it to generate Java source files from the proto files. The generated java source files should be added to the sourceSet so that they can be compiled along with Java sources.
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/java'
}
}
}
Running command gradlew build
generates source code in the directory build/generated/source/proto/main/grpc
and build/generated/source/proto/main/java
.
gRPC Server
The gRPC server implements services defined in the proto files and expose those as RPC API. In this example, we will treat the product microservice as a gRPC server and order microservice as the gRPC client.
In our case, the gRPC server exposes one RPC method rpc getProduct(GetProductRequest) returns (stream GetProductResponse)
.
Implementing service definition
After you have the server stub generated, you can implement the RPC service ProductService
, as defined in the service.proto
file.
To implement the business logic, you should override getProduct(..)
method in the autogenerated abstract
class ProductServiceGrpc.ProductServiceImplBase
.
public class ProductService extends ProductServiceGrpc.ProductServiceImplBase {
private final ProductRepository productRepository;
public ProductService() {
this.productRepository = new ProductRepository();
}
@Override
public void getProduct(
GetProductRequest request, StreamObserver<GetProductResponse> responseObserver) {
var productId = request.getProductId();
Optional<ProductInfo> productInfo = productRepository.get(productId);
if (productInfo.isPresent()) {
var product = productInfo.get();
var getProductResponse =
GetProductResponse.newBuilder()
.setName(product.getName())
.setDescription(product.getDescription())
.setPrice(product.getPrice())
.build();
responseObserver.onNext(getProductResponse);
responseObserver.onCompleted();
} else {
responseObserver.onError(new StatusException(Status.NOT_FOUND));
}
}
}
The above code fetches Product information from ProductRepository
and calls onNext()
on StreamObserver
by passing the getProductResponse
. The onCompleted()
method notifies the stream about successful completion.
In case of an error, you can call onError()
method by passing the appropriate error code.
Registering service
To expose the RPC service ProductService
, you can create a gRPC server instance and register the service by calling the addService
method. The server listens to the specified port and dispatches all requests to the relevant service.
public ProductServer(int port) {
this.port = port;
var productService = new ProductService();
this.server = ServerBuilder.forPort(port).addService(productService).build();
}
Implementing gRPC client stub
The first thing we need to do to implement a gRPC client is to generate client stubs using the proto file defined in the server (product-service
) application.
public class ProductClient {
private final ProductServiceGrpc.ProductServiceBlockingStub productServiceBlockingStub;
public ProductClient(String host, int port) {
var managedChannel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
this.productServiceBlockingStub = ProductServiceGrpc.newBlockingStub(managedChannel);
}
public void call() {
var productRequest = GetProductRequest.newBuilder().setProductId("apple-123").build();
var productResponse = productServiceBlockingStub.getProduct(productRequest);
log.info("Received Product from server, info {}", productResponse);
}
public static void main(String[] args) {
var client = new ProductClient("0.0.0.0", 8081);
client.call();
}
}
You can create a gRPC channel specifying the server address and port as ManagedChannelBuilder.forAddress(host, port).usePlaintext().build()
. The channel represents a virtual connection to an endpoint to perform RPC.
You can create the client stub using the newly created channel as:
var managedChannel = ManagedChannelBuilder.forAddress(host,port).usePlaintext().build();
var productServiceBlockingStub = ProductServiceGrpc.newBlockingStub(managedChannel);
There are two types of client stubs:
- Blocking: The
BlockingStub
, which waits until it receives a server response. - Non Blocking: The
NonBlockingStub
, which doesn’t wait for server response, but instead registers an observer to receive the response.
Here plaintext means we are setting up an unsecured connection between the client and server.
Summary
A gRPC-based RPC framework is a great choice for inter-process communication in microservices applications. Not only the gRPC services are faster compared to RESTful services, but also they are strongly typed. The Protocol Buffer, a binary format for exchanging data, is used for defining gRPC APIs. The gRPC is supported in many programming languages. Using code generation tools makes it easier to generate client and server stubs in the gRPC.
Discussion about this post