Compressing responses in GRPC for ASP.NET CORE 3.0

Translation of the article was prepared on the eve of the start of the course “C # ASP.NET Core Developer”


In this episode of my gRPC and ASP.NET Core series we’ll look at how to hook up the response compression function of the gRPC services.

NOTE: In this article, I share some of the compression details I learned by learning about call settings and methods. There are likely more accurate and more effective approaches to achieve the same results.

This article is part of gRPC and ASP.NET Core series

WHEN SHOULD I INCLUDE COMPRESSION IN GRPC?

The short answer is: it depends on your payloads.
Long answer:

gRPC uses a protocol buffer as a tool for serializing request and response messages sent over the network. Protocol buffer creates a binary serialization format that is designed for small, efficient payloads by default. Compared to regular JSON payloads, protobuf provides a more modest message size. JSON is quite verbose and readable. As a result, it includes property names in the data sent over the network, which increases the number of bytes that must be transferred.

The protocol buffer uses integers as identifiers for data transmitted over the network. It uses the concept of base 128 variants, which allows fields with values ​​from 0 to 127 to require only one byte for transport. In many cases it is possible to limit your messages to fields in this range. Large integers require more than one byte.

So, remember, the protobuf payload is already quite small, as the format aims to reduce the bytes sent over the network to the smallest possible size. However, there is still potential for further lossless compression using a format such as GZip. This potential needs to be tested on your payloads, as you will only see size reduction if your payload has enough repetitive textual data to benefit from compression. Perhaps for small response messages, attempting to compress them might result in more bytes than using an uncompressed message; which is clearly no good.

Also of note is the compression overhead of the processor, which can outweigh the gain you get from size reduction. You should track the CPU and memory overhead for requests after changing the compression level to get a full picture of your services.

ASP.NET Core Server Integration does not use compression by default, but we can enable it for the whole server or specific services. This seems like a reasonable default as you can track your responses for different methods over time and evaluate the benefits of compressing them.

HOW DO I ENABLE RESPONSE COMPRESSION IN GRPC?

So far I have found two main approaches to connect gRPC response compression. You can configure this at the server level so that all gRPC services apply compression to responses, or at the individual service level.

CONFIGURATION AT THE SERVER LEVEL

services.AddGrpc(o =>
{
   o.ResponseCompressionLevel = CompressionLevel.Optimal;
   o.ResponseCompressionAlgorithm = "gzip";
});

Startup.cs на GitHub

When registering a gRPC service in a dependency injection container using the method AddGrpc inside ConfigureServices, we have the ability to customize in GrpcServiceOptions… At this level, parameters affect all gRPC services that the server implements.

Using an extension method overload AddGrpc, we can provide Action<GrpcServiceOptions>... In the above code snippet, we have chosen the “gzip” compression algorithm. We can also set CompressionLevelby manipulating the time we sacrifice for data compression to get a smaller size. If the parameter is not specified, the current implementation defaults to CompressionLevel.Fastest... In the previous snippet, we allowed more time for compression to reduce the number of bytes to the smallest possible size.

SETUP AT THE SERVICE LEVEL

services.AddGrpc()
   .AddServiceOptions(o =>
       {
           o.ResponseCompressionLevel = CompressionLevel.Optimal;
           o.ResponseCompressionAlgorithm = "gzip";
       });

Startup.cs на GitHub

As a result of the call AddGrpc returns IGrpcServerBuilder... We can call an extension method for the builder called AddServiceOptionsto provide parameters for each service separately. This method is generic and takes the type of gRPC service to which the parameters should apply.

In the previous example, we decided to provide parameters for calls that are handled by the implementation WeatherService... At this level, the same options are available that we discussed for the server level configuration. In this scenario, the other gRPC services on this server will not receive the compression options we set for that particular service.

INQUIRIES FROM GRPC CUSTOMER

Now that response compression is enabled, we need to make sure our requests indicate that our client is accepting compressed content. In fact, this is enabled by default when using GrpcChannelcreated with the method ForAddressso we don't need to do anything in our client code.

var channel = GrpcChannel.ForAddress("https://localhost:5005");

Program.cs на GitHub

Channels created this way already send a “grpc-accept-encoding” header that includes the gzip compression type. The server reads this header and determines that the client allows compressed responses to be returned.

One way to visualize the compression effect is to enable logging for our application at design time. This can be done by modifying the file appsettings.Development.json in the following way:

{
 "Logging": {
   "LogLevel": {
       "Default": "Debug",
       "System": "Information",
       "Grpc": "Trace",
       "Microsoft": "Trace"
   }
 }
}

appsettings.Development.json на GitHub

When starting our server, we get much more detailed console logs.

info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
     Executing endpoint 'gRPC - /WeatherForecast.WeatherForecasts/GetWeather'
dbug: Grpc.AspNetCore.Server.ServerCallHandler[1]
     Reading message.
dbug: Microsoft.AspNetCore.Server.Kestrel[25]
     Connection id "0HLQB6EMBPUIA", Request id "0HLQB6EMBPUIA:00000001": started reading request body.
dbug: Microsoft.AspNetCore.Server.Kestrel[26]
     Connection id "0HLQB6EMBPUIA", Request id "0HLQB6EMBPUIA:00000001": done reading request body.
trce: Grpc.AspNetCore.Server.ServerCallHandler[3]
     Deserializing 0 byte message to 'Google.Protobuf.WellKnownTypes.Empty'.
trce: Grpc.AspNetCore.Server.ServerCallHandler[4]
     Received message.
dbug: Grpc.AspNetCore.Server.ServerCallHandler[6]
     Sending message.
trce: Grpc.AspNetCore.Server.ServerCallHandler[9]
     Serialized 'WeatherForecast.WeatherReply' to 2851 byte message.
trce: Microsoft.AspNetCore.Server.Kestrel[37]
     Connection id "0HLQB6EMBPUIA" sending HEADERS frame for stream ID 1 with length 104 and flags END_HEADERS
trce: Grpc.AspNetCore.Server.ServerCallHandler[10]
     Compressing message with 'gzip' encoding.
trce: Grpc.AspNetCore.Server.ServerCallHandler[7]
     Message sent.
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
     Executed endpoint 'gRPC - /WeatherForecast.WeatherForecasts/GetWeather'
trce: Microsoft.AspNetCore.Server.Kestrel[37]
     Connection id "0HLQB6EMBPUIA" sending DATA frame for stream ID 1 with length 978 and flags NONE
trce: Microsoft.AspNetCore.Server.Kestrel[37]
     Connection id "0HLQB6EMBPUIA" sending HEADERS frame for stream ID 1 with length 15 and flags END_STREAM, END_HEADERS
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
     Request finished in 2158.9035ms 200 application/grpc

Log.txt на GitHub

In the 16th line of this log, we see that WeatherReply (in fact, an array of 100 WeatherData elements in this example) was serialized to protobuf and is 2851 bytes in size.

Later, on line 20, we see that the message was compressed using gzip encoding, and on line 26, we can see the data frame size for this call, which is 978 bytes. In this case, the data was compressed well (by 66%) because the repeating WeatherData elements contain text and many of the values ​​in the message are repeated.

In this example, gzip compression had a good effect on the size of the data.

DISABLING RESPONSE COMPRESSION IN IMPLEMENTATION OF THE SERVICE METHOD

The compression of the response can be controlled in each method. Currently I have found a way to just turn it off. When compression is enabled for a service or server, we can opt out of compression as part of the service method implementation.

Let's take a look at the server log when calling a service method that transmits WeatherData messages from the server. If you would like to know more about streaming on the server, you can read my previous article "Streaming data to a server with gRPC and .NET Core"...

info: WeatherForecast.Grpc.Server.Services.WeatherService[0]
     Sending WeatherData response
dbug: Grpc.AspNetCore.Server.ServerCallHandler[6]
     Sending message.
trce: Grpc.AspNetCore.Server.ServerCallHandler[9]
     Serialized 'WeatherForecast.WeatherData' to 30 byte message.
trce: Grpc.AspNetCore.Server.ServerCallHandler[10]
     Compressing message with 'gzip' encoding.
trce: Microsoft.AspNetCore.Server.Kestrel[37]
     Connection id "0HLQBMRRH10JQ" sending DATA frame for stream ID 1 with length 50 and flags NONE
trce: Grpc.AspNetCore.Server.ServerCallHandler[7]
     Message sent.

Log.txt на GitHub

In the 6th line, we see that the individual WeatherData message is 30 bytes in size. Line 8 is compressed, and line 10 shows that the data is now 50 bytes long - more than the original message. In this case, there is no benefit to us from gzip compression, we see an increase in the total size of the message sent over the network.

We can disable compression for a specific message by setting WriteOptions to be called in a service method.

public override async Task GetWeatherStream(Empty _, IServerStreamWriter responseStream, ServerCallContext context)
{
   context.WriteOptions = new WriteOptions(WriteFlags.NoCompress);

   // реализация метода, который записывает в поток
}

WeatherService.cs на GitHub

We can install WriteOptions in ServerCallContext at the top of our service method. We are transferring a new copy WriteOptionsfor which the value WriteFlags installed in NoCompress... These parameters are used for the next entry.

When streaming responses, this value can also be set to IServerStreamWriter...

public override async Task GetWeatherStream(Empty _, IServerStreamWriter responseStream, ServerCallContext context)
{   
   responseStream.WriteOptions = new WriteOptions(WriteFlags.NoCompress);

   // реализация метода записи в поток
}

WeatherService.cs на GitHub

When we use this parameter, the logs show that no compression is applied to calls to this service method.

info: WeatherForecast.Grpc.Server.Services.WeatherService[0]
     Sending WeatherData response
dbug: Grpc.AspNetCore.Server.ServerCallHandler[6]
     Sending message.
trce: Grpc.AspNetCore.Server.ServerCallHandler[9]
     Serialized 'WeatherForecast.WeatherData' to 30 byte message.
trce: Microsoft.AspNetCore.Server.Kestrel[37]
     Connection id "0HLQBMTL1HLM8" sending DATA frame for stream ID 1 with length 35 and flags NONE
trce: Grpc.AspNetCore.Server.ServerCallHandler[7]
     Message sent.

Log.txt на GitHub

Now a 30 byte message is 35 bytes long in the DATA frame. There is a small overhead that is an extra 5 bytes that we don't need to worry about here.

DISABLING RESPONSE COMPRESSION FROM THE GRPC CLIENT

By default, a gRPC channel includes parameters that determine which encodings it accepts. These can be configured when creating a channel if you want to disable compression of responses from your client. Generally, I would avoid this and let the server decide what to do, since it knows better what can and cannot be compressed. However, sometimes you may need to monitor this from the client.

The only way I've found in my API research to date is to set up a channel by passing an instance GrpcChannelOptions... One of the properties of this class is for CompressionProviders - IList<ICompressionProvider>... By default, when this value is null, the client implementation automatically adds a Gzip compression provider. This means that the server can use gzip to compress response messages, as we have seen.

private static async Task Main()
{
   using var channel = GrpcChannel.ForAddress("https://localhost:5005", new GrpcChannelOptions { CompressionProviders = new List() });
   var client = new WeatherForecastsClient(channel);
   var reply = await client.GetWeatherAsync(new Empty());
   foreach (var forecast in reply.WeatherData)
  {
       Console.WriteLine($"{forecast.DateTimeStamp.ToDateTime():s} | {forecast.Summary} | {forecast.TemperatureC} C");
   }
   Console.WriteLine("Press a key to exit");
   Console.ReadKey();
}

Program.cs на GitHub

In this example client code, we set GrpcChannel and transfer a new copy GrpcChannelOptions... We assign to the property CompressionProviders empty list. Since we now do not specify providers in our channel when calls are created and sent through that channel, they will not include any compression encodings in the grpc-accept-encoding header. The server sees this and does not gzip the response.

SUMMARY

In this post, we explored the possibility of compressing response messages from the gRPC server. We found that in some (but not all) cases, this can lead to a smaller payload. We have seen that by default client calls include the gzip value "grpc-accept-encoding" in the headers. If the server is configured to apply compression, it will only do so if the supported compression type matches the request header.

We can customize GrpcChannelOptions when creating a channel for a client to disable gzip compression. On the server, we can configure the entire server at once or a separate service to compress responses. We can also override and disable this at the level of each service method.

To learn more about gRPC, you can read all the articles that are part of my gRPC and ASP.NET Core series...


ALL ABOUT THE COURSE


Read more:

  • Combining Blazor and Razor Pages in one ASP.NET Core 3 app
  • The Complete Guide to Hardening Your Asp.Net Core Web Application and API
  • C # .NET cache implementations

Similar Posts

Leave a Reply

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