BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Building Effective Microservices with gRPC, Ballerina, and Go

Building Effective Microservices with gRPC, Ballerina, and Go

This item in japanese

Bookmarks

Key Takeaways

  • Based on interaction and communication style, we can group microservices into two groups: external-facing microservices and internal microservices.
  • RESTful APIs are the de facto communication technology for external-facing microservices (REST’s ubiquity and rich supporting ecosystem play a vital role in its continued success).
  • gRPC is a relatively new implementation of the Remote Procedure Call (RPC) API paradigm. It can play a major role in all synchronous communications between internal microservices 
  • Here we examine key gRPC concepts, their usage and benefits of having gRPC as an inter-service communication by using a real-world microservice use case.
  • gRPC is supported by many major programming languages. We will discuss sample implementations by using Ballerinalang and Golang as the programming languages.

In modern microservice architecture, we can categorize microservices into two main groups based on their interaction and communication. The first group of microservices acts as external-facing microservices, which are directly exposed to consumers. They are mainly HTTP-based APIs that use conventional text-based messaging payloads (JSON, XML, etc.) that are optimized for external developers, and use Representational State Transfer (REST) as the de facto communication technology. 

REST’s ubiquity and rich ecosystem play a vital role in the success of these external-facing microservices. OpenAPIprovides well-defined specifications for describing, producing, consuming, and visualizing these REST APIs. API management systems work well with these APIs and provide security, rate limiting, caching, and monetizing along with business requirements. GraphQL can be an alternative for the HTTP-based REST APIs but it is out of scope for this article.

The other group of microservices are internal and don’t communicate with external systems or external developers. These microservices interact with each other to complete a given set of tasks. Internal microservices use either synchronous or asynchronous communication. In many cases, we can see the use of REST APIs over HTTP as a synchronous mode but that would not be the best technology to use. In this article, we will take a closer look at how we can leverage a binary protocol such as gRPC which can be an optimized communication protocol for inter-service communication

What is gRPC?

gRPC is a relatively new Remote Procedure Call (RPC) API paradigm for inter-service communications. Like all other RPCs, it allows directly invoking methods on a server application on a different machine as if it were a local object. Same as other binary protocols like Thrift and Avro, gRPC uses an interface description language (IDL) to define a service contract. gRPC uses HTTP/2, the latest network transport protocol, as the default transport protocol and this makes gRPC fast and robust compared to REST over HTTP/1.1. 

You can define the gRPC service contract by using Protocol Buffers where each service definition specifies the number of methods with the expected input and output messages with the data structure of the parameters and return types. Using major programming language provided tools, a server-side skeleton and client-side code (stub) can be generated using the same Protocol Buffers file which defines the service contract.

A Pragmatic Microservices Use Case with gRPC

Figure 1: A segment of an online retail shop microservices architecture

One of the main benefits of microservice architecture is to build different services by using the most appropriate programming language rather than building everything in one language. Figure 1 illustrates a segment of an online retail shop microservice architecture, where four microservices are implemented in Ballerina (referred to as Ballerina in the rest of the article) and Golang working together to provide some functionality of the retail online shop. Since gRPC is supported by many major programming languages, when we define the service contracts, implementation can be carried out with a well-suited programming language. 

Let’s define service 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: Service contract for Order microservice (order.proto)

The Order microservice will get shopping items and the quantity and return the subtotal. Here I use the Ballerina gRPC tool to generate a gRPC service boilerplate code and the stub/client respectively. 

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

This generates the OrderService server boilerplate code.

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: Code snippet of the generated boilerplate code (OrderService_sample_service.bal)

gRPC service is perfectly mapped to Ballerina’s service type, gRPC rpc mapped to Ballerina’s resource function and the gRPC messages are mapped to the Ballerina record type. 

I have created a separate Ballerina project for the Order microservice and used the generated OrderService boilerplate code to implement a gRPC unary service

Unary Blocking

OrderService is called in the Cart microservice. We can use the following Ballerina command to generate the client stub and client code.

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

The generated client stub has both blocking and non-blocking remote methods. This sample code 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: Generated remote object code snippet for blocking mode

Ballerina’s remote method abstraction is a nicely fitted gRPC client stub and you can see how the UpdateOrder invocation code is very clean and neat.

The Checkout microservice issues the final bill by aggregating all interim orders received from the Cart microservice. In this case, we are going to send all interim orders as 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 Checkout microservice (checkout.proto)

You can use the ballerina grpc command to generate boilerplate code for checkout.proto.

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

gRPC Client Streaming

The Cart microservices (client) streamed messages are made available as a stream object argument, which can be iterated through using a loop, processing each message sent by the client. See the following sample 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: Service code snippet of the sample implementation for CheckoutService(CheckoutService_sample_service.bal)

Once the client stream has completed, a grpc:EOS error is returned that can be used to identify when to send the final response message (aggregated total) to the client using the caller object.

The sample client code and client stub for the CheckoutService can be generated by using the following command:

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

Let’s look at the Cart microservice implementation. The Cart microservice has two REST APIs—one to add items to the cart and another to do the final checkout. When adding items to the cart, it will get an interim order with a subtotal for each item by doing a gRPC call to the Order microservice and storing it in memory. Calling the Checkout microservice will send all in-memory stored interim orders to the Checkout microservice as a gRPC stream and return the total amount to pay. Ballerina uses the built-in Stream type and Client Object abstractions to implement the gRPC client streaming. See Figure 2, which illustrates how Ballerina’s client streaming works.

Figure 2: Ballerina gRPC client streaming 

The full implementation of CheckoutService client streaming can be found in the Cart microservice checkout resource function. Finally, in the checkout process, do a gRPC call to the Stock microservice which is implemented in Golang, and update the stock by deducting sold items.

grpc-gateway

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: Service contract for Stock microservice (stock.proto)

In this scenario, the same UpdateStock service will be invoked by using a REST API call as an external-facing API and also invoked by using a gRPC call as an inter-service call. grpc-gateway is a plugin of protoc, which reads a gRPC service definition and generates a reverse-proxy server that translates a RESTful JSON API into gRPC. 

Figure 3: grpc-gateway

grpc-gateway helps you to provide your APIs in both gRPC and REST style at the same time.

The following command generates the Golang gRPC stubs:

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 the Golang grpc-gateway code:

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 the 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

Sample Run

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

Conclusion

gRPC is relatively new, but its fast-growing ecosystem and community will definitely make an impact in microservice development. Since gRPC is an open standard, all mainstream programming languages support it, making it ideal to work in a polyglot microservice environment. As a general practice, we can use gRPC for all synchronous communications between internal microservices, and also we can expose it as REST-style APIs by using emerging technology like grpc-gateway. In addition to what we discussed in this article, gRPC features like DeadlinesCancellationChannels, and xDS support will provide immense power and flexibility to developers to build effective microservices.

More Resources 

To read and learn more about Ballerina gRPC support, check out the following links:

Golang has comprehensive gRPC support and we can extend these microservices to enhance security, robustness, and resiliency by using gRPC Interceptor, Deadlines, Cancellation, and Channels among other things. Check out the grpc-go git repo which has many working samples on these concepts.

Recommended videos:

About the Author

Lakmal Warusawithana is the Senior Director - Developer Relations at WSO2. In 2005, Lakmal co-founded the thinkCube, the pioneers in developing the next generation of Collaborative Cloud Computing products that are tailored towards Telecom operators. He oversaw the overall engineering process, giving special attention to scalability and service delivery of thinkCube solutions. Prior to co-founding thinkCube, Lakmal spent four years at ITABS, a company that specialized in Linux based server deployments that came with a custom easy-to-use server management interface.

Rate this Article

Adoption
Style

BT