Build High Performant Microservices using gRPC and .NET 6
Learn how to leverage server streaming to deliver 5 Million records in a breeze
Microsoft has rolled out its fastest .NET release yet — .NET 6, with long term stable version of .NET Core and lots of new APIs, performance, and language improvements. In this article, we will see what’s new in .NET 6 for gRPC Services (blazing fast performance and serialization, better fault tolerance, client-side load balancing, and HTTP/3 support).
We will also learn how we can leverage these features to build high performant microservice using gRPC and .NET 6. We will also create a real-world gRPC service utilizing server streaming to process and deliver five million records.
Note: If you are new to the gRPC Services in .NET, It’s recommended to go through this article first which explains some basic concepts and also explains how gRPC compares with other technologies like WCF and REST.
A Brief Recap:
- gRPC is a popular open-source RPC Framework run by CNCF.
- It’s a contract first language-independent platform — which simply means that client and server have to agree on a contract about what and how the messages will be delivered. The contract is defined in a .proto file which derives the code generation process through the tooling that .NET6 provides.
- It’s cross-platform so both client and server may use a different technology stack.
- It uses HTTP/2 and sends Protobuf (a high performant serialization technology for messages). Unlike JSON which stores data in human-readable text format, Protobuf uses the binary-interchange format and isn’t human-readable and hence needs tooling to create strongly typed client and base classes. Luckily with .NET, it’s as easy as referencing any NuGet package.
- HTTP/2 enables multiplexing where you send multiple requests simultaneously on a single connection.
- It also supports streaming where a server can send multiple responses to the client and vice versa.
- Bi-Directional Streaming — where both the client and the server send multiple messages back and forth.
Creating your first gRPC Service in .NET 6
Without further adieu, Let’s create a first gRPC service in .NET 6.
Prerequisites:
- Create a new project and select the ‘ASP.NET Core gRPC’ Service template
Make sure .NET 6.0 is selected as a target framework
- Docker Support can be enabled if you want to run this service in a container.
Clicking on create will create the project with the default greeter service as shown below.
Let’s analyze the greet.proto file
syntax = “proto3”;option csharp_namespace = “myGRPCService”;package greet;// The greeting service definition.service Greeter {// Sends a greetingrpc SayHello (HelloRequest) returns (HelloReply);}// The request message containing the user’s name.message HelloRequest {string name = 1;}// The response message containing the greetings.message HelloReply {string message = 1;}
This is a language-neutral contract that simply defines what the service will look like and the messages it consumes.
Let’s look at the GreeterService.cs which is the implementation of this contract (.proto file)
public class GreeterService : Greeter.GreeterBase{private readonly ILogger<GreeterService> _logger;public GreeterService(ILogger<GreeterService> logger){_logger = logger;}public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context){return Task.FromResult(new HelloReply{Message = “Hello “ + request.Name});}}
This service simply implements the SayHello action and returns the HelloReply message back to the client. You may notice that types GreeterBase, HelloReply, HelloRequest are not yet defined. If you check the definition of GreeterBase type — you will that these types were auto-generated by .NET6 tooling.
How does the code-generation work?
if you edit the .csproj file you will notice the protobuf element referencing greet.proto and a property indicating server code generation.
To run the service simply open the command prompt and type ‘dotnet run’
You can notice the service is running on https://localhost:7057. Now, unlike REST APIs, we can’t test this on a browser and need to create a client.
Create a new console app to consume the service. Right-click on dependencies and choose ‘Manage Connected Service’
Now, we will add a service reference — Select gRPC.
Locate your proto file and select the ‘Client’ type that will generate the necessary code to consume the gRPC service.
Make sure to verify the properties of greet.proto file to generate ‘Client only’ stub classes.
Now open the program.cs and add the below lines to invoke your gRPC Servi
var channel = GrpcChannel.ForAddress(“https://localhost:7057");var client = new Greeter.GreeterClient(channel);var reply = await client.SayHelloAsync( new gRPCDemo.HelloRequest { Name = “gRPC Demo” });Console.WriteLine(“from server: “ + reply);
The above code simply creates a new gRPC channel which is passed to a greeter client which then invokes the SayHello method.
Running the project to see the response from the service.
Now that we have created our first gRPC service in .NET 6, let’s explore some new features and advanced concepts in .NET 6.
What’s new for gRPC in .NET 6
- Performance Improvements:
- In NET 6 ASCII string serialization for protobuf uses SIMD — Single Instruction multiple data to process characters in parallel using high-performance CPU instructions, yielding 20% performance overall improvement.
- gRPC sends and receives raw bytes using ByteString. The new .NET 6 introduces Zero Copy ByteString API which avoids the need to create a copy /allocate an internal array. This is really helpful if you are sending and receiving large messages to avoid unnecessary allocation.
- In .NET 5 HTTP/2 library was using fixed buffer size limiting download speed when there’s latency. This issue has been solved in .NET 6 by updating the HTTP/2 library to use dynamic buffer size which improved the download performance by 118%.
2. Transient Fault Handling
You can now catch RPC Exceptions, detect transient faults (like loss of network connectivity or timeouts) and use built-in logic for automatic retries which can now be configured on a channel.
Tip: Check this link If you want to learn more about transient fault handling with gRPC retries.
3. Client-Side Load Balancing
With client-side load balancing, you can have your gRPC clients distribute the load optimally across your servers. It eliminates the need to have a proxy for load balancing. We can configure client-side load balancing while creating a new channel. It consists of two components:
- Service Discovery: It acts as a resolver and makes a DNS query to get IPs of the server where gRPC is hosted.
- The load balancer, which creates a connection and picks the address using various configurations such as PickFirst and RoundRobin logic.
If any of the endpoints fails, the load balancer will automatically switch to the other healthy endpoints.
3. HTTP/3 Support
.NET 6 is the first gRPC implementation to support end-to-end HTTP/3.
HTTP/2 enables multiple streams and uses framing to enable multiple requests to be handled at the same time over the same connection, however, the world has gone mobile using Wi-Fi and cellular connections which can be unreliable at times, and in those cases, the TCP packet will be lost and all of the streams will be blocked — head of line blocking problem. HTTP/3 Solves this by using a new connection protocol called QUIC which uses UDP and has TLS built-in, so it's faster to establish a connection and is independent of the IP address so mobile clients can switch between wifi and cellular networks — keeping the same logical connection.
.NET 6 supports QUIC and has an open-source implementation of it — MSQUIC and provides the following benefits:
- Faster response time of the first request
- improved experience when there is connection packet loss
- support transitioning between networks
Note: The RFC for HTTP/3 is not yet finalized and may change so it’s a preview feature in NET6.
In the next section, we will learn how to leverage gRPC server streaming to build high performant Microservice that delivers 5M records to the client.
If we go back to our greeter service, you will notice that it’s using the unary call — which starts with the client sending a request message and a response message is returned when the service has finished processing.
service Greeter {rpc SayHello (HelloRequest) returns (HelloReply);}
gRPC Server Streaming
Server streaming enables the gRPC client to send a request to the service and gets a stream of responses. The client reads from the returned stream until there are no more messages. gRPC takes care of the message ordering which is guaranteed.
In our example, we will use this sample CSV file of 196MB, containing 5million sales records. Now, delivering those records will not be efficient in a single call. Furthermore, traditional rest-style paging requires multiple client requests — back and forth communication from client to server.
gRPC Server streaming solves this problem efficiently.
- The client will be simply invoking the service method.
- Our gRPC service will be reading from the CSV file using StreamReader line by line, converting the rows to the model gRPC understands, and sending the record back to the client — one row at a time.
- The Client will be receiving the stream of responses.
let’s begin by defining a proto file to deliver messages with the following fields.
Protos-> sales.proto
syntax = “proto3”;import “google/protobuf/timestamp.proto”;option csharp_namespace = “gRPCDemoUsingNET6.Protos”;package sales;service SalesService {rpc GetSalesData(Request) returns (stream SalesDataModel) {}}message Request{string filters=1;}message SalesDataModel {int32 OrderID = 1;string Region = 2;string Country = 3;string ItemType=4;google.protobuf.Timestamp OrderDate=5;google.protobuf.Timestamp ShipDate=6;int32 UnitsSold=7;float UnitCost=8;float UnitPrice=9;int32 TotalRevenue=10;int32 TotalCost=11;int32 TotalProfit=12;}
the stream keyword indicates that the SalesDataModel will be delivered as a stream
rpc GetSalesData(Request) returns (stream SalesDataModel) {}
Let’s add a new service — SalesaDataService.cs as below
public class SalesDataService : Protos.SalesService.SalesServiceBase{public override async TaskGetSalesData(Protos.Request request,IServerStreamWriter<Protos.SalesDataModel> responseStream, ServerCallContext context){using (var reader = new StreamReader(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “Data”, “sales_records.csv”))){string line; bool isFirstLine = true;while ((line = reader.ReadLine()) != null){var pieces = line.Split(‘,’);var _model = new Protos.SalesDataModel();try{if (isFirstLine){isFirstLine = false;continue;}_model.Region = pieces[0];_model.Country = pieces[1];_model.OrderID = int.TryParse(pieces[6], out int _orderID) ? _orderID : 0;_model.UnitPrice = float.TryParse(pieces[9], out float _unitPrice) ? _unitPrice : 0;_model.ShipDate = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime((DateTime.TryParse(pieces[7], out DateTime _dateShip) ? _dateShip : DateTime.MinValue).ToUniversalTime());_model.UnitsSold = int.TryParse(pieces[8], out int _unitsSold) ? _unitsSold : 0;_model.UnitCost = float.TryParse(pieces[10], out float _unitCost) ? _unitCost : 0;_model.TotalRevenue = int.TryParse(pieces[11], out int _totalRevenue) ? _totalRevenue : 0;_model.TotalCost = int.TryParse(pieces[13], out int _totalCost) ? _totalCost : 0;await responseStream.WriteAsync(_model);}catch (Exception ex){throw new RpcException(new Status(StatusCode.Internal, ex.ToString()));}}}}}
Let’s break it down to understand it better.
public class SalesDataService : Protos.SalesService.SalesServiceBase
The class implements the SalesServiceBase which is autogenerated by .NET6 tooling using proto file configuration.
public override async TaskGetSalesData(Protos.Request request,IServerStreamWriter<Protos.SalesDataModel> responseStream, ServerCallContext context){}
We then override the method GetSalesData to read the data from the file and return it as a stream by simply writing our data model to the responseStream object as shown below:
await responseStream.WriteAsync(_model);
Now let’s run the service to see it in action.
The Service is up and running so we can create our client.
Create a new console project, add gRPC service reference using protobuf (as shared previously)
This is how our client implementation looks like
//Create a channel for your gRPC Service
var channel = GrpcChannel.ForAddress("https://localhost:7143");//Create SalesService Client to open a connection
var client = new SalesService.SalesServiceClient(channel);//Invoke the method using var call = client.GetSalesData(new Request { Filters = "" });int Count = 0;//Get response stream
await foreach (var each in call.ResponseStream.ReadAllAsync()){Console.WriteLine(String.Format("New Order Receieved from {0}-{1},Order ID = {2}, Unit Price ={3}, Ship Date={4}", each.Country, each.Region, each.OrderID, each.UnitPrice,each.ShipDate));Count++;}
Console.WriteLine("Stream ended: Total Records: "+Count.ToString());Console.Read();
Let’s run the client and you can see the incoming messages stream from your gRPC Service.
It takes almost 2minutes to load all 5m sales records from the gRPC service. However, in some cases, you may want to specify for how long a call should run. To do this, you need to configure the deadline.
using var call = client.GetSalesData(new Request { Filters = “” }, deadline: DateTime.UtcNow.AddSeconds(5));
so when this time exceeds, the client will stop processing stream and throw an exception which can be handled as below:
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded){Console.WriteLine(“Service timeout.”);}
The below example uses an upper limit of 5 seconds so it only processed 77829 records.
Conclusion
In this article, we have learned about new performance enhancements for gRPC .NET 6 like client-side load balancing, transient fault handling, and HTTP/3. It also explains how gRPC Server streaming can be leveraged to build high performant microservice able to process and deliver millions of records.
You can download the source code from this repo.