Comprimir respuestas en GRPC para ASP.NET CORE 3.0

La traducción del artículo se preparó antes del inicio del curso "C # ASP.NET Core Developer" .








En este episodio de mi serie sobre gRPC y ASP.NET Core, veremos cómo conectar la funcionalidad de compresión de respuesta de los servicios de gRPC.



NOTA : En este artículo, cubro algunos de los detalles de compresión que aprendí al aprender sobre la configuración y los métodos de las llamadas. Es probable que haya enfoques más precisos y efectivos para lograr los mismos resultados.



Este artículo es parte de una serie sobre gRPC y ASP.NET Core .



¿Cuándo debería habilitar la compresión en GRPC?



Respuesta corta: depende de sus cargas útiles.

Respuesta larga:

gRPC utiliza un búfer de protocolo como herramienta para serializar los mensajes de solicitud y respuesta enviados a través de la red. El búfer de protocolo crea un formato de serialización binario que está diseñado para cargas útiles pequeñas y eficientes de forma predeterminada. En comparación con las cargas útiles JSON normales, protobuf proporciona un tamaño de mensaje más modesto. JSON es bastante detallado y legible. Como resultado, incluye nombres de propiedad en los datos enviados a través de la red, lo que aumenta la cantidad de bytes que deben transferirse.



El búfer de protocolo utiliza números enteros como identificadores de los datos transmitidos a través de la red. Utiliza el concepto de variantes base 128, que permite que los campos con valores de 0 a 127 requieran solo un byte para el transporte. En muchos casos, es posible limitar sus mensajes a campos en este rango. Los enteros grandes requieren más de un byte.



Entonces, recuerde, la carga útil de protobuf ya es bastante pequeña porque el formato apunta a reducir los bytes enviados a través de la red al tamaño más pequeño posible. Sin embargo, todavía existe la posibilidad de una mayor compresión sin pérdidas utilizando un formato como GZip. Este potencial debe probarse en sus cargas útiles porque solo verá una reducción de tamaño si su carga útil tiene suficientes datos textuales repetitivos para obtener algún beneficio de la compresión. Quizás para mensajes de respuesta pequeños, intentar comprimirlos puede resultar en más bytes que usar un mensaje sin comprimir; lo que claramente no es bueno.



También es de destacar la sobrecarga de compresión del procesador, que puede superar la ganancia que obtiene de la reducción de tamaño. Debe realizar un seguimiento de la CPU y la sobrecarga de memoria para las solicitudes después de cambiar el nivel de compresión para obtener la imagen completa de sus servicios.



ASP.NET Core Server Integration no usa compresión de forma predeterminada, pero podemos habilitarla para todo el servidor o servicios específicos. Esto parece un valor predeterminado razonable, ya que puede rastrear sus respuestas para diferentes métodos a lo largo del tiempo y evaluar los beneficios de comprimirlas.



¿Cómo habilito la compresión de respuesta en GRPC?



Hasta ahora he encontrado dos enfoques principales para conectar la compresión de respuesta de gRPC. Puede configurar esto en el nivel del servidor para que todos los servicios de gRPC apliquen compresión a las respuestas, o en el nivel de servicio individual.



Configuración a nivel de servidor



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


Startup.cs GitHub



Al registrar un servicio gRPC en un contenedor de inyección de dependencia utilizando un método AddGrpcinterno ConfigureServices, tenemos la oportunidad de configurarlo en GrpcServiceOptions. En este nivel, los parámetros afectan a todos los servicios de gRPC que implementa el servidor.



Usando una sobrecarga de método de extensión AddGrpc, podemos proporcionar Action<GrpcServiceOptions>. En el fragmento de código anterior, hemos elegido el algoritmo de compresión "gzip". También podemos establecer CompressionLevelmanipulando el tiempo que sacrificamos por la compresión de datos para obtener un tamaño menor. Si no se especifica el parámetro, la implementación actual utiliza de forma predeterminada CompressionLevel.Fastest. En el fragmento anterior, permitimos más tiempo para que la compresión redujera la cantidad de bytes al tamaño más pequeño posible.



Configuración del nivel de servicio



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


Startup.cs GitHub



AddGrpcVuelve la llamada IGrpcServerBuilder. Podemos llamar al constructor un método de extensión llamado AddServiceOptionspara proporcionar parámetros para cada servicio por separado. Este método es genérico y toma el tipo de servicio gRPC al que se deben aplicar los parámetros.



En el ejemplo anterior, elegimos proporcionar parámetros para las llamadas que maneja la implementación WeatherService. En este nivel, están disponibles las mismas opciones que discutimos para la configuración del nivel del servidor. En este escenario, los otros servicios de gRPC en este servidor no recibirán las opciones de compresión que configuramos para ese servicio en particular.



Solicitudes de clientes GRPC



Ahora que la compresión de respuesta está habilitada, debemos asegurarnos de que nuestras solicitudes indiquen que nuestro cliente está aceptando contenido comprimido. De hecho, esto está habilitado de forma predeterminada cuando se usa GrpcChannelcon un método creado ForAddress, por lo que no necesitamos hacer nada en nuestro código de cliente.



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


Program.cs GitHub



Los canales creados de esta manera ya envían un encabezado "grpc-accept-encoding" que incluye el tipo de compresión gzip. El servidor lee este encabezado y determina que el cliente permite que se devuelvan respuestas comprimidas.



Una forma de visualizar el efecto de compresión es habilitar el registro para nuestra aplicación en el momento del diseño. Esto se puede hacer modificando el archivo de la appsettings.Development.jsonsiguiente manera:



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


appsettings.Development.json GitHub



Cuando iniciamos nuestro servidor, obtenemos registros de consola mucho más detallados.



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



En la línea 16 de este registro, vemos que WeatherReply (de hecho, una matriz de 100 elementos WeatherData en este ejemplo) se serializó en protobuf y tiene un tamaño de 2851 bytes.



Más adelante, en la línea 20, vemos que el mensaje se comprimió usando la codificación gzip, y en la línea 26, podemos ver el tamaño del marco de datos para esta llamada, que es de 978 bytes. En este caso, los datos se comprimieron bien (66%) porque los elementos WeatherData repetidos contienen texto y muchos de los valores del mensaje se repiten.



En este ejemplo, la compresión gzip tuvo un buen efecto en el tamaño de los datos.



Deshabilitar la compresión de respuesta en la implementación del método de servicio



La compresión de la respuesta se puede controlar en cada método. Actualmente he encontrado una manera de simplemente apagarlo. Cuando la compresión está habilitada para un servicio o servidor, podemos optar por no recibir compresión como parte de la implementación del método de servicio.



Echemos un vistazo al registro del servidor al llamar a un método de servicio que transmite mensajes WeatherData desde el servidor. Si desea obtener más información sobre la transmisión al servidor, puede leer mi artículo anterior Transmisión de datos a un servidor con gRPC y .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



En la sexta línea, vemos que el mensaje WeatherData individual tiene un tamaño de 30 bytes. La línea 8 está comprimida y la línea 10 muestra que los datos ahora tienen 50 bytes de longitud, más que el mensaje original. En este caso, no hay ningún beneficio para nosotros de la compresión gzip, vemos un aumento en el tamaño total del mensaje enviado a través de la red.



Podemos deshabilitar la compresión para un mensaje específico configurándolo WriteOptionspara que llame a un método de servicio.



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

   //  ,    
}


WeatherService.cs GitHub



Podemos establecerlo WriteOptionsen ServerCallContextla parte superior de nuestro método de servicio. Estamos pasando una nueva instancia WriteOptionsque está WriteFlagsconfigurada en NoCompress. Estos parámetros se utilizan para la siguiente entrada.



Al transmitir respuestas, este valor también se puede establecer en IServerStreamWriter.



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

   //     
}


WeatherService.cs GitHub



Cuando usamos este parámetro, los registros muestran que no se aplica compresión a las llamadas a este método de servicio.



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



Ahora, un mensaje de 30 bytes tiene una longitud de 35 bytes en la trama DATA. Hay una pequeña sobrecarga de 5 bytes adicionales por la que no debemos preocuparnos aquí.



Deshabilitar la compresión de respuesta del cliente GRPC



De forma predeterminada, un canal gRPC incluye parámetros que determinan qué codificaciones acepta. Estos se pueden configurar al crear un canal si desea deshabilitar la compresión de respuestas de su cliente. Generalmente, evitaría esto y dejaría que el servidor decida qué hacer, ya que sabe mejor qué se puede y qué no se puede comprimir. Sin embargo, a veces es posible que deba controlar esto desde el cliente.



La única forma que he encontrado en mi investigación de API hasta la fecha es configurar un canal pasando una instancia GrpcChannelOptions. Una de las propiedades de esta clase es para CompressionProviders- IList<ICompressionProvider>. De forma predeterminada, cuando este valor es nulo, la implementación del cliente agrega automáticamente un proveedor de compresión Gzip. Esto significa que el servidor puede usar gzip para comprimir los mensajes de respuesta, como hemos visto.



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

En este código de cliente de ejemplo, estamos configurando GrpcChannely enviando una nueva instancia GrpcChannelOptions. Estamos asignando CompressionProvidersuna lista vacía a la propiedad . Dado que ahora no estamos especificando proveedores en nuestro canal cuando se crean y envían llamadas a través de ese canal, no incluirán ninguna codificación de compresión en el encabezado grpc-accept-encoding. El servidor ve esto y no hace gzip la respuesta.



Resumen



En esta publicación, exploramos la posibilidad de comprimir los mensajes de respuesta del servidor gRPC. Descubrimos que en algunos casos (pero no en todos), esto puede llevar a una carga útil menor. Hemos visto que, de forma predeterminada, las llamadas de cliente incluyen el valor gzip "grpc-accept-encoding" en los encabezados. Si el servidor está configurado para aplicar compresión, solo lo hará si el tipo de compresión admitido coincide con el encabezado de la solicitud.



Podemos configurar GrpcChannelOptionsal crear un canal para que el cliente desactive la compresión gzip. En el servidor, podemos configurar todo el servidor a la vez o un servicio separado para comprimir las respuestas. También podemos anular y deshabilitar esto en el nivel de cada método de servicio.



Para obtener más información sobre gRPC, puede leer todos los artículos que forman parte de miSeries gRPC y ASP.NET Core .






TODO SOBRE EL CURSO






Lee mas






All Articles