Compressing responses in GRPC for ASP.NET CORE 3.0

The translation of the article was prepared in anticipation of the start of the "C # ASP.NET Core Developer" course .








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



NOTE : In this article, I cover some of the compression details that 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 a series on gRPC and ASP.NET Core .



When should you enable compression in GRPC?



Short answer: 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 transmitted over the network, which increases the number of bytes that must be transmitted.



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.



Server-level configuration



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


Startup.cs GitHub



When registering a gRPC service in a dependency injection container using a method AddGrpcinside ConfigureServices, we have the opportunity to configure 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 establish 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 using CompressionLevel.Fastest. In the previous snippet, we allowed more time for compression to reduce the number of bytes to the smallest possible size.



Service level configuration



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


Startup.cs GitHub



The call AddGrpcreturns IGrpcServerBuilder. We can call an extension method called on the builder 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 chose 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.



GRPC Client Requests



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 used GrpcChannelwith a created method ForAddress, so 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.jsonas follows:



{
 "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



On 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.



Disable response compression in service method implementation



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'd like to learn more about streaming to 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 turn off compression for a specific message by setting WriteOptionsto call in a service method.



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

   //  ,    
}


WeatherService.cs GitHub



We can set WriteOptionsat ServerCallContextthe top of our service method. We are passing in a new instance WriteOptionsthat is WriteFlagsset to 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<WeatherData> 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.



Disable response compression from 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 in 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<ICompressionProvider>() });
   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 are setting GrpcChanneland pushing a new instance GrpcChannelOptions. We are assigning CompressionProvidersan empty list to the property . 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 configure GrpcChannelOptionswhen creating a channel for the 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 mygRPC and ASP.NET Core series .






ALL ABOUT THE COURSE






Read more






All Articles