In this post, part of my gRPC with ASP.NET Core series, we’ll explore enabling response compression for gRPC services.
NOTE: I’ll cover a few details about compression in this post which are from my early investigation into the options and techniques to configure calls. There may be more precise or more proficient approaches to accomplish the same results.
When Should I Enable Compression with gRPC?
Short answer: It depends on your payload(s).
gRPC utilises protocol buffers as the serialisation formation for request and response messages sent over the wire. Protocol buffers produce a binary serialisation format which is designed for efficient and small payloads by default. When compared to regular JSON payloads, protocol buffers result in more modest message sizes. JSON is quite verbose and designed to be reasonably human-readable. As a result, it includes the property names in the data transmitted over the wire, which inflates the bytes that must be transmitted.
Protocol buffers use integers as the identifiers for data sent over the wire. It uses a concept of base 128 varints which allows fields with values 0 through 127 to only require a single byte on the wire. In many cases, it should be possible to limit your messages to fields in that range. Larger integers require more than a single byte.
So, to recap, protocol buffers payloads are already quite small as they aim to reduce the bytes sent over the wire to the smallest size possible. There is, however, still a potential for further lossless compression with a format such as GZip. This potential needs to be tested for your payloads since you’ll only see a reduction in bytes if your payload has enough repeated text data to benefit from compression. It’s possible, for small response messages, that attempting to compress them may result in more bytes than the uncompressed message used; which is clearly far from ideal.
There’s also a small CPU cost overhead to compression which may outweigh the gains you achieve in the reduces bytes over the wire. You should monitor CPU and memory overhead of calls after changing the compression level to assess the complete picture for your services.
By default, the ASP.NET Core server integration does not use compression, but we can enable this for a server or specific services. This seems like a sensible default as you can measure your responses for various methods over time and assess the benefit of compressing them.
How Do I Enable Response Compression with gRPC?
There are two main approaches that I’ve found so far to enable the compression of gRPC responses. You can either configure this at the server level so that all gRPC services apply compression to responses, or at a per-service level.
Server Level Options
When registering the gRPC services into the dependency injection container with the
AddGrpc method inside ConfigureServices, it’s possible to set properties on the
GrpcServiceOptions.At this level, the options affect all gRPC services which the server implements.
Using the overload of the
AddGrpc extension method, we can supply an
Action<GrpcServiceOptions>. In the above snippet we’ve set the compression algorithm to “gzip”. We can also optionally control the
CompressionLevel which trades the time needed to compress the data against the final size that is achieved by compression. If not specified the current implementation defaults to using CompressionLevel.Fastest. In the preceding snippet, we’ve chosen to allow more time for the compression to reduce the bytes to the smallest possible size.
Service Level Options
IGrpcServerBuilder is returned. We can call an extension method on that builder called
AddServiceOptions to provide per service options. This method is generic and accepts the type of the gRPC service that the options should apply.
In the preceding example, we have decided to provide options specifically for calls that are handled by the
WeatherService implementation. The same options are available at this level as we discussed for the server level configuration. In this scenario, if we mapped other gRPC services within this server, they would not recieve the compression options.
Making Requests From a gRPC Client
Now that response compression is enabled, we need to ensure our requests state that our client accepts compressed content. In fact, this is enabled by default when using a
GrpcChannel created using the
ForAddress method so we have nothing to do in our client code.
Channels created in this way already send a “grpc-accept-encoding” header which includes the gzip compression type. The server reads this header and determines that the client allows gzipped responses to be returned.
One way to visualise the effect of compression is to enable trace level logging for our application while in development. We can achieve this by modifying the appsettings.Development.json file as follows:
When running our server, we now get much more verbose console logging.
On line 16 of this log, we can see that the WeatherReply, essentially an array of 100 WeatherData items in this sample, has been serialised to protocol buffers and has a size of 2851 bytes.
Later, in line 20, we can see that the message has been compressed with gzip encoding and on line 26, we can see the size of the data frame for this call which is 978 bytes. The data, in this case, has compressed quite well (66% reduction) because the repeated WeatherData items contain text and many of the values repeat within the message.
In this example, gzip compression has a good effect on the over the wire size of the data.
Disabling Response Compression Within a Service Method Implementation
It’s possible to control the response compression on a per-method basis. At this time, I’ve only found a way to do this on an opt-out approach. When compression is enabled for a service or server, we can opt-out of compression within the service method implementation.
Let’s look at the server log output when calling a service method which streams WeatherData messages from the server. If you’d like to know more about server streaming, you can read my previous blog post “Server Streaming Streaming with gRPC and .NET Core“.
On line 6, we can see that an individual WeatherData message is 30 bytes in size. On line 8, this gets compressed, and on line 10, we can see that the data length is now 50 bytes, larger than the original message. In this case, there is no gain from gzip compression, and we see an increase in the overall message size sent over the wire.
We can avoid compression for a particular message by setting the
WriteOptions for the call within the service method.
At the top of our service method, we can set the
WriteOptions on the
ServerCallContext. We pass in a new
WriteOptions instance which the
WriteFlags value set to
NoCompress. These write options are used for the next write.
With streaming responses, it’s also possible to set this value on the
When this option is applied, the logs now show that compression for calls to this service method is not applied.
Now the 30 byte message has a length of 35 bytes in the DATA frame. There is a small overhead which accounts for the extra 5 bytes which we don’t need to concern ourselves with here.
Disabling Response Compression from a gRPC Client
By default, the gRPC channel includes options that control which encodings it accepts. It is possible to configure these when creating the channel if you wish to disable compression of responses from your client. Generally, I would avoid this and let the server decide what to do, since it knows best what can and cannot be compressed. That said, you may sometimes need to control this from the client.
The only way I’ve found to do this in my exploration of the API to date is to configure the channel by passing in a
GrpcChannelOptions instance. One of the properties on this options class is for the CompressionProviders, an
IList<ICompressionProvider>. By default, when this is null, the client implementation adds the Gzip compression provider for you automatically. This means that the server can choose to gzip the response message(s) as we have already seen.
In this sample client code, we establish the
GrpcChannel and pass in a new instance of
GrpcChannelOptions. We set the
CompressionProviders property to an empty list. Since we now specify no providers in our channel, when the calls are created and sent via this channel, they won’t include any compression encodings in the “grpc-accept-encoding” header. The server acknowledges this and not apply gzip compression to the response.
In this post, we’ve explored the possibility of compressing response messages from a gRPC server. We’ve identified that in some cases, but crucially not all, this may result in smaller payloads. We’ve seen that by default clients calls include the gzip “grpc-accept-encoding” value in the headers. If the server is configured to apply compression, it only does so if a supported encoding type is matched from the request header.
We can configure the
GrpcChannelOptions when creating a channel for the client, to disable the inclusion of the gzip compression encoding. On the server, we can configure the whole server, or a specific service to enable compression for responses. We can override and disable that on a per service-method level as well.