Building high-performance microservices with gRPC, Ballerina and Go

Hi, Habr. Future students of the course “Software Architect” we invite you to take part in an open webinar on the topic “Idempotency and API commutability in queues and HTTP”

We also share the translation of useful material.


Within the framework of a modern microservice architecture, we can divide microservices into two main groups depending on their relationship and interaction. The first group represents external microservices that are directly accessible to users. These are mostly HTTP based APIs that use plain text messages (JSON, XML, etc.), optimized for use by third parties, using Representative State Transfer (REST) ​​as the communication technology.

The prevalence and good support of REST is critical to the success of external microservices. OpenAPI provides well-defined specifications for describing, creating, using, and rendering REST APIs. For such APIs, there are API management systems that provide security, rate limiting, caching, and monetization along with business requirements. As an alternative to HTTP based REST API you can use GraphQL, but this is already a topic for a separate article.

The second group is internal microservices, which are not designed to interact with external systems or third-party developers. These microservices interact with each other to perform a specific set of tasks. Internal microservices use either synchronous or asynchronous communication mode. We have seen the use of REST APIs over HTTP in many implementations of synchronous mode, but this is not the most suitable technology for such purposes. In this article, we will take a closer look at how we can use a binary protocol like gRPC as an optimized interservice communication protocol.

What is gRPC?

gRPC is a relatively new API paradigm remote procedure call (Remote procedure call – RPC) for communication between services. Like all other RPCs, it allows you to directly call methods in a server application on another computer, as if it were a local object. Like other binary protocols such as Thrift and Avro, gRPC uses the interface description language (IDL) to define the service contract. gRPC uses HTTP / 2 by default, the newest network transport protocol, making gRPC much faster and more reliable than REST over HTTP / 1.1.

You can define a gRPC contract with Protocol bufferswhere each service definition specifies the number of methods with expected input and output messages, parameter data structure, and return types. Using the tools provided by the major programming languages, it is possible to generate server-side skeleton and client-side code (stub) using the same Protocol Buffers file that defines the contract.

An example of using microservices with gRPC

Figure 1. Microservices architecture segment of an online retail store
Figure 1. Microservices architecture segment of an online retail store

One of the main advantages of a microservice architecture is the ability to create various services using the programming language most suitable for each of them, eliminating the need to write the entire structure in one language. Figure 1 shows a segment of a microservice architecture for an online retail store where four microservices are implemented in Ballerina and Golangthat work together to provide the basic functionality of an online retail store. Since gRPC is supported by many major programming languages, when we define service contracts, implementation can be done using the appropriate programming language for the particular service.

Let’s define contracts for each service.

syntax="proto3";
 
package retail_shop;
 
service OrderService {
   rpc UpdateOrder(Item) returns (Order);
}  
message Item {
   string itemNumber = 1;
   int32 quantity = 2;
}
message Order {
   string itemNumber = 1;
   int32 totalQuantity = 2;
   float subTotal = 3;

Listing 1. The contract for the microservice Order (order.proto)

Order gets the items and the number of purchases and returns a subtotal. Here I am using Ballerina gRPC tool to generate gRPC service and stub / client boilerplate code respectively.

$ ballerina grpc --mode service --input proto/order.proto --output gen_code

Let’s define contracts for each service.

import ballerina/grpc;
listener grpc:Listener ep = new (9090);
 
service OrderService on ep {
   resource function UpdateOrder(grpc:Caller caller, Item value) {
       // Implementation goes here.
 
       // You should return an Order
   }
}
public type Order record {|
   string itemNumber = "";
   int totalQuantity = 0;
   float subTotal = 0.0;
  
|};
public type Item record {|
   string itemNumber = "";
   int quantity = 0;
  
|}; 

Listing 2. A snippet of the generated boilerplate code (OrderServicesampleservice.bal).

The gRPC service matches the type perfectly service Ballerina, gRPC rpc correlates with resource function Ballerina, and gRPC messages are of type record

For the Order microservice, I created a separate Ballerina project and used the generated OrderService boilerplate code to gRPC unary service implementation

Unary blocking

OrderService is called in the Cart microservice. For creating client stub and client code we can use the following Ballerina command.

$ ballerina grpc --mode client --input proto/order.proto --output gen_code

The generated client stub has both blocking and non-blocking remote methods. This code example demonstrates how the gRPC unary service interacts with the gRPC blocking client.

public remote function UpdateOrder(Item req, grpc:Headers? headers = ()) returns ([Order, grpc:Headers]|grpc:Error) {
      
       var payload = check self.grpcClient->blockingExecute("retail_shop.OrderService/UpdateOrder", req, headers);
       grpc:Headers resHeaders = new;
       anydata result = ();
       [result, resHeaders] = payload;
       return [<Order>result, resHeaders];
   }
};

Listing 3. A snippet of the generated remote object code for lock mode

Ballerina’s remote method abstraction is a well-fitting gRPC client stub, and you can tell yourself how much call code UpdateOrder clean and tidy.

The Checkout microservice issues a final invoice by aggregating all interim orders received from the Cart microservice. In our case, we are going to send all intermediate orders in the form stream Order messages.

syntax="proto3";
package retail_shop;
 
service CheckoutService {
   rpc Checkout(stream Order) returns (FinalBill) {}
}
message Order {
   string itemNumber = 1;
   int32 totalQuantity = 2;
   float subTotal = 3;
}
message FinalBill {
   float total = 1;
}

Listing 4. Service contract for the Checkout microservice (checkout.proto)

To generate boilerplate code for checkout.proto, you can use the command ballerina grpc

$ ballerina grpc –mode service –input proto / checkout.proto –output gencode

GRPC client streaming

The Cart (client) microservice streaming messages are available as an argument to the stream object, which you can loop over to process each individual message sent by the client. Here’s an example implementation:

service CheckoutService on ep {
   resource function Checkout(grpc:Caller caller, stream<Order,error> clientStream) {
       float totalBill = 0;
 
       //Iterating through streamed messages here
       error? e = clientStream.forEach(function(Order order) {
           totalBill += order.subTotal;           
       });
       //Once the client completes stream, a grpc:EOS error is returned to indicate it
       if (e is grpc:EOS) {
           FinalBill finalBill = {
               total:totalBill
           };
           //Sending the total bill to the client
           grpc:Error? result = caller->send(finalBill);
           if (result is grpc:Error) {
               log:printError("Error occurred when sending the Finalbill: " + 
result.message() + " - " + <string>result.detail()["message"]);
           } else {
               log:printInfo ("Sending Final Bill Total: " + 
finalBill.total.toString());
           }
           result = caller->complete();
           if (result is grpc:Error) {
               log:printError("Error occurred when closing the connection: " + 
result.message() +" - " + <string>result.detail()["message"]);
           }
       }
       //If the client sends an error instead it can be handled here
       else if (e is grpc:Error) {
           log:printError("An unexpected error occured: " + e.message() + " - " +
                                                   <string>e.detail()["message"]);
       }   
   }
}

Listing 5. services Service code snippet for an example implementation CheckoutService(CheckoutServicesampleservice.bal)

Upon completion of the client thread, an error is returned grpc:EOSwhich can be used to determine when to send the final response message (aggregated total) to the customer using caller object

Client code and client stub for CheckoutService can be generated using the following command:

$ ballerina grpc --mode client --input proto/checkout.proto --output gencode

Let’s take a look at the implementation of the Cart microservice. The Cart microservice has two REST APIs, one for adding items to the cart and the other for final settlement. When items are added to the cart, it will receive a subtotal subtotal order for each item by making a gRPC call to the Order microservice and storing it in memory. The call to the Checkout microservice will send all interim orders stored in memory to the Checkout microservice as a gRPC stream and return the total amount for payment. Ballerina uses the built-in Stream type and abstractions Client Object to implement gRPC client streaming. Figure 2 shows how the Ballerina client streaming works.

Figure 2. Streaming gRPC Ballerina client
Figure 2. Streaming gRPC Ballerina client

The complete implementation of streaming client CheckoutService can be found at resource function checkout microservice Cart. Finally, during the checkout process, you need to make a gRPC call to the Stock microservice, which is implemented in Golang, and update the inventory in the store by subtracting the items sold.

syntax="proto3";
package retail_shop;
option go_package = "../stock;gen";
import "google/api/annotations.proto";
 
service StockService {
   rpc UpdateStock(UpdateStockRequest) returns (Stock) {
       option (google.api.http) = {
           // Route to this method from POST requests to /api/v1/stock
           put: "/api/v1/stock"
           body: "*"
       };
   }
}
message UpdateStockRequest {
   string itemNumber = 1;
   int32 quantity = 2;
}
message Stock {
   string itemNumber = 1;
   int32 quantity = 2;

Listing 6. The contract for the Stock microservice (stock.proto)

In this scenario, the same UpdateStock service will be called by using a REST API call to the external API, and using a gRPC call as an interservice communication. grpc-gateway Is a protoc protocol plugin that reads the gRPC service definition and generates a reverse proxy that translates the RESTful JSON API to gRPC.

Figure 3: grpc-gateway
Figure 3: grpc-gateway

grpc-gateway helps you provide your APIs simultaneously in gRPC and REST style.

The following command generates gRPC stubs for Golang:

protoc -I/usr/local/include -I. 
-I$GOROOT/src 
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis 
--go_out=plugins=grpc:. 
stock.proto

The following command generates grpc-gateway code for Golang:

protoc -I/usr/local/include -I. 
-I$GOROOT/src 
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis 
--grpc-gateway_out=logtostderr=true:. 
stock.proto

The following command generates a file stock.swagger.json:

protoc -I/usr/local/include -I. 
-I$GOROOT/src 
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis 
-I$GOROOT/src 
--swagger_out=logtostderr=true:../stock/gen/. 
./stock.proto

Test run

Clone the git repository microservices-with-grpc and follow the instructions in README.md.

Conclusion

gRPC is a relatively new technology, but its fast growing ecosystem and community will definitely have an impact on the development of microservices. Because gRPC is an open standard, all major programming languages ​​support it, making it ideal for running in a multilingual microservice environment. Typically, we can use gRPC for all synchronous communication between back-end microservices, and we can also expose it as a REST API using new technologies like grpc-gateway. In addition to what we discussed in this article, gRPC features such as Deadlines, Cancellation, Channels and xDS supportwill provide developers with even more power and flexibility to build highly efficient microservices.

More related links

To find out more about Ballerina gRPC support, read the information at the following links:

Golang has already received comprehensive gRPC support, and we can extend the microservices written in it with, among others, gRPC’s Interceptor, Deadlines, Cancellation, and Channels to improve security, reliability, and resiliency. Take a look at the git repository grpc-gowhich has many working examples on these concepts.

Also watch the related video:


Learn more about the course “Software Architect”.

Sign up for an open lesson “Idempotency and API commutability in queues and HTTP”.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *