gRPC for microservices communication

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 modeled around business capabilities.
  • Microservices are independently deployable.
  • A microservices encapsulates the data it owns. In case, if one microservice needs to get data from another microservice then 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 quite bulky, inefficient, and limiting for lots of 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 very widespread.

Another issue with using JSON-based API is that the support of schema 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 that are consumed by client applications typically browser, mobile application, or other application. 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 the 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 can be – to have API Gateway/BFF in front of the microservices and make all internal communication using gRPC.

A gRPC application architecture

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, for below payload JSON encoding takes 81 bytes 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 error. 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 choose to write microservices in the programming language you deemed 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 of 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 Buffers: 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 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 that is exchanged between client and 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 the backward and forward compatibility – clients and services can ignore field numbers that 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 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 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.

Social Share !
Default image
Pankaj
Software Architect @ Schlumberger ``` Cloud | Microservices | Programming | Kubernetes | Architecture | Machine Learning | Java | Python ```