Control de errores transitorios con reintentos de gRPC

Por James Newton-King

Los reintentos de gRPC son una característica que permite a los clientes de gRPC volver a intentar efectuar automáticamente las llamadas que no han tenido éxito. En este artículo se describe cómo configurar una directiva de reintentos para crear aplicaciones de gRPC resistentes y tolerantes a errores en .NET.

Los reintentos de gRPC requieren la versión 2.36.0 u otra posterior de Grpc.Net.Client.

Control de errores transitorios

Las llamadas de gRPC pueden verse interrumpidas por errores transitorios. Entre estos errores transitorios se incluyen lo siguientes:

  • Pérdida puntual de la conectividad de red
  • Servicios no disponibles temporalmente
  • Tiempos de espera agotados debidos a la carga del servidor

Cuando una llamada de gRPC se interrumpe, el cliente produce una excepción RpcException con detalles sobre el error. La aplicación cliente debe detectar la excepción y decidir cómo controlar el error.

var client = new Greeter.GreeterClient(channel);
try
{
    var response = await client.SayHelloAsync(
        new HelloRequest { Name = ".NET" });

    Console.WriteLine("From server: " + response.Message);
}
catch (RpcException ex)
{
    // Write logic to inspect the error and retry
    // if the error is from a transient fault.
}

La duplicación de la lógica de reintento en una aplicación es una tarea demasiado prolija y propensa a errores. Por suerte, el cliente de gRPC de .NET tiene compatibilidad integrada con los reintentos automáticos.

Configuración de una directiva de reintentos de gRPC

Una directiva de reintentos se configura una sola vez, cuando se crea un canal de gRPC:

var defaultMethodConfig = new MethodConfig
{
    Names = { MethodName.Default },
    RetryPolicy = new RetryPolicy
    {
        MaxAttempts = 5,
        InitialBackoff = TimeSpan.FromSeconds(1),
        MaxBackoff = TimeSpan.FromSeconds(5),
        BackoffMultiplier = 1.5,
        RetryableStatusCodes = { StatusCode.Unavailable }
    }
};

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
    ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }
});

El código anterior:

  • Crea una interfaz MethodConfig. Las directivas de reintento se pueden configurar según el método, y los métodos se relacionan mediante la propiedad Names. Este método se configura con MethodName.Default, por lo que se aplica a todos los métodos de gRPC a los que llama este canal.
  • Configura una directiva de reintentos. Esta directiva indica a los clientes que vuelvan a intentar realizar automáticamente las llamadas de gRPC que producen un error con el código de estado Unavailable.
  • Configura el canal creado para usar la directiva de reintentos estableciendo GrpcChannelOptions.ServiceConfig.

Los clientes de gRPC creados con el canal volverán a intentar realizar automáticamente las llamadas que hayan generado errores:

var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(
    new HelloRequest { Name = ".NET" });

Console.WriteLine("From server: " + response.Message);

Casos en los que los reintentos son válidos

Se reintentan las llamadas cuando:

  • El código de estado con errores coincide con un valor en RetryableStatusCodes.
  • El número de intentos anterior es menor que MaxAttempts.
  • La llamada no se ha confirmado.
  • No se ha superado la fecha límite.

Una llamada a gRPC se confirma en dos escenarios:

  • El cliente recibe los encabezados de respuesta. El servidor envía los encabezados de respuesta cuando se llama a ServerCallContext.WriteResponseHeadersAsync o cuando el primer mensaje se escribe en la secuencia de respuesta del servidor.
  • El mensaje saliente del cliente (o mensajes en caso de secuencias) ha superado el tamaño máximo del búfer del cliente. MaxRetryBufferSize y MaxRetryBufferPerCallSizese configuran en el canal.

Las llamadas confirmadas no se reintentan, independientemente del código de estado o el número anterior de intentos.

Llamadas en secuencias

Las llamadas en secuencias se pueden usar con los reintentos de gRPC, pero hay consideraciones importantes cuando se usan juntas:

  • Secuencias de servidor y secuencias bidireccionales: las RPC de secuencias que devuelven varios mensajes del servidor no se reintentan después de que se haya recibido el primer mensaje. Las aplicaciones deben agregar lógica adicional para volver a establecer manualmente las llamadas de streaming de servidor y bidireccionales.
  • Secuencias de cliente y secuencias bidireccionales: las RPC de secuencias que envían varios mensajes al servidor no se reintentan si los mensajes salientes han superado el tamaño máximo del búfer del cliente. El tamaño máximo del búfer se puede aumentar mediante configuración.

Para obtener más información, vea Casos en los que los reintentos son válidos.

Retraso de retroceso para reintentos

El retraso de retroceso entre los reintentos se configura con InitialBackoff, MaxBackoff y BackoffMultiplier. Puede encontrar más información sobre cada opción en la sección de opciones de reintento de gRPC.

El retraso real entre los reintentos se establece de manera aleatoria. Un retraso aleatorio entre 0 y el retroceso actual determina cuándo se va a realizar el siguiente reintento. Tenga en cuenta que, incluso con el retroceso exponencial configurado, si aumenta el retroceso actual entre intentos el retraso real entre los intentos no siempre es mayor. El retraso se establece de manera aleatoria para evitar que los reintentos de varias llamadas se agrupen en clústeres y puedan sobrecargar el servidor.

Detección de reintentos con metadatos

Los reintentos de gRPC se pueden detectar por la presencia de metadatos de grpc-previous-rpc-attempts. Los metadatos de grpc-previous-rpc-attempts:

  • Se agregan automáticamente a las llamadas de reintento y se envían al servidor.
  • El valor representa el número de reintentos anteriores.
  • El valor es siempre un entero.

Considere el escenario de reintento siguiente:

  1. El cliente realiza una llamada de gRPC al servidor.
  2. El servidor genera un error y devuelve una respuesta de código de estado que admite reintentos.
  3. El cliente reintenta la llamada de gRPC. Dado que hubo un intento anterior, los metadatos de grpc-previous-rpc-attempts tienen un valor de 1. Los metadatos se envían al servidor con el reintento.
  4. El servidor realiza la operación correctamente y devuelve OK.
  5. El cliente informa de que la operación se ha realizado correctamente. grpc-previous-rpc-attempts está en los metadatos de respuesta y tiene un valor de 1.

Los metadatos de grpc-previous-rpc-attempts no están presentes en la llamada de gRPC inicial, son 1 para el primer reintento, 2 para el segundo reintento y así sucesivamente.

Opciones de reintento de gRPC

En la siguiente tabla se describen las opciones disponibles para configurar directivas de reintentos de gRPC:

Opción Descripción
MaxAttempts Número máximo de intentos de llamada, incluido el intento original. Este valor está limitado por GrpcChannelOptions.MaxRetryAttempts, que está establecido de forma predeterminada en 5. Es obligatorio indicar un valor, que debe ser mayor que 1.
InitialBackoff Retraso de retroceso inicial que transcurre entre los reintentos. Un retraso aleatorio entre 0 y el retroceso actual determina cuándo se va a realizar el siguiente reintento. Después de cada intento, el retroceso actual se multiplica por BackoffMultiplier. Es obligatorio indicar un valor, que debe ser mayor que 0.
MaxBackoff El retroceso máximo establece un límite para el crecimiento de retroceso exponencial. Es obligatorio indicar un valor, que debe ser mayor que 0.
BackoffMultiplier El retroceso se multiplicará por este valor después de cada reintento y aumentará exponencialmente cuando el multiplicador sea mayor que 1. Es obligatorio indicar un valor, que debe ser mayor que 0.
RetryableStatusCodes Colección de códigos de estado. Las llamadas de gRPC que produzcan un error con un estado coincidente se reintentarán automáticamente. Para obtener más información sobre los códigos de estado, vea Códigos de estado y su uso en gRPC. Se requiere al menos un código de estado de reintento de llamadas.

Cobertura

La cobertura es otra estrategia de reintento. La cobertura permite enviar de forma agresiva varias copias de una sola llamada de gRPC sin esperar una respuesta. Las llamadas de gRPC con cobertura se pueden ejecutar varias veces en el servidor, y se usa el primer resultado correcto. Es importante señalar que la cobertura solo se puede usar en los métodos que pueden ejecutarse varias veces sin que ello tenga ningún efecto adverso.

La cobertura tiene ventajas y desventajas en comparación con los reintentos:

  • Una ventaja de la cobertura es que es capaz de devolver un resultado correcto más rápidamente. Permite varias llamadas de gRPC a la vez y se completará cuando el primer resultado correcto esté disponible.
  • Un inconveniente de la cobertura es que puede ser un despilfarro, en el sentido de que se pueden realizar varias llamadas y que todas ellas sean correctas, de modo que solo se usa el primer resultado y el resto se descarta.

Configuración de una directiva de cobertura de gRPC

Una directiva de cobertura se configura como una de reintentos. A este respecto, cabe decir que una directiva de cobertura no se puede combinar con una de reintentos.

var defaultMethodConfig = new MethodConfig
{
    Names = { MethodName.Default },
    HedgingPolicy = new HedgingPolicy
    {
        MaxAttempts = 5,
        NonFatalStatusCodes = { StatusCode.Unavailable }
    }
};

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
    ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }
});

Opciones de cobertura de gRPC

En la siguiente tabla se describen las opciones disponibles para configurar directivas de cobertura de gRPC:

Opción Descripción
MaxAttempts La directiva de cobertura enviará ese número de llamadas como máximo. MaxAttempts representa el número total de intentos, incluido el intento original. Este valor está limitado por GrpcChannelOptions.MaxRetryAttempts, que está establecido de forma predeterminada en 5. Es obligatorio indicar un valor, que debe ser mayor que 2.
HedgingDelay La primera llamada se enviará de inmediato, pero las llamadas de cobertura posteriores se retrasarán de acuerdo con este valor. Si el retraso se establece en cero o null, todas las llamadas de cobertura se envían inmediatamente. HedgingDelay es opcional y tiene cero como valor predeterminado. Un valor debe ser cero o mayor.
NonFatalStatusCodes Colección de códigos de estado que indican que hay otras llamadas de cobertura que todavía pueden realizarse correctamente. Si el servidor devuelve un código de estado no irrecuperable, las llamadas de cobertura continuarán. Si no, se cancelarán las solicitudes pendientes y se devolverá el error a la aplicación. Para obtener más información sobre los códigos de estado, vea Códigos de estado y su uso en gRPC.

Recursos adicionales