Cómo funcionan los reintentos en los clientes del SDK de Amazon Web Services


Recientemente, revisando código que teníamos en algunos de los servicios que estamos construyendo, nos preguntamos si tenía sentido hacer reintentos en caso de obtener una excepción al utilizar el SDK de AWS para cualquiera de sus servicios (SQS, Kinesis, SNS, etc)... y esto me llevó a revisar un poco más en detalle cómo funcionan las librerías cliente que nos proporciona Amazon.

Dado que los servicios de Amazon se exponen como APIs consumibles por HTTP, cuando utilizamos las librerías del SDK para invocar a algún servicio, éstas finalmente acaban realizando una invocación HTTP... y todos sabemos que la red falla, ¿verdad? Así pues, no es ninguna sorpresa encontrarnos con que las librerías cliente de AWS realizan reintentos.

Reintentos de las librerías clientes del SDK de AWS

A menudo se producen errores debidos a problemas transitorios de la red o el servicio, errores que se solventarían fácilmente con un reintento de la petición y que si se trasladan al consumidor le obligan a lidiar con estas excepciones fácilmente evitables. Por ello, Amazon por defecto utiliza reintentos en los clientes de sus servicios. Aunque un reintento conlleva una penalización en el tiempo de ejecución, es normalmente preferible al "ruido" de transmitir excesivas excepciones al consumidor.

¿Dónde está implementada la lógica de los reintentos?

Los clientes de los distintos servicios de AWS acaban utilizando internamente la clase AmazonHttpClient, que está situada en la librería aws-java-sdk-core. Pues bien, la clase  dispone de un método para obtener un builder que finalmente construye un RequestExecutor (una inner class dentro de AmazonHttpClient), que sería quien realmente ejecuta la petición HTTP. Y es aquí donde se gestionan los reintentos; en concreto, en el método RequestExetutor.executeHelper(). Simplificando su comportamiento, este método:
  • Tiene un bucle que es el responsable de que se realicen reintentos.
  • Durante su ejecución, hay un par de puntos (bien dentro de executeOneRequest(), bien dentro de handleRetryableException()) donde se acaba invocando al método shouldRetry() para determinar si debe reintentarse la petición.
  • En función del tipo de excepción, la política de reintentos establecida (ahora hablamos de ello), etc. el método shouldRetry() decide si se realiza o no un reintento.

¿Cómo se define las política de reintentos?

La política de reintentos la marca la interfaz RetryPolicy, que tiene un par de métodos:
  • shouldRetry(RetryPolicyContext context): dado un contexto que define la situación de la ejecución (la petición que se ha ejecutado, el httpStatusCode, la excepción que se ha producido, etc) decide si debe o no repetirse la petición.
  • computeDelayBeforeNextRetry(RetryPolicyContext context): indica cuántos milisegundos esperar para realizar el reintento.
La forma más directa y sencilla de establecer una política de reintentos es utilizar la clase ClientConfiguration. Esta clase permite configurar muchos parámetros relacionados con la conexión HTTP, entre los que se encuentra la política de reintentos. Y todos los clientes del sdk de Amazon (o sus builders) permiten establecer una instancia de ClientConfiguration con los valores que deseemos.

Por ejemplo, para crear un cliente AmazonKinesis con una política de reintentos propia, podríamos hacer algo así:
 

¿Cuál es la política de reintentos por defecto?

Por defecto, si no hacemos ninguna configuración especial, se aplica la siguiente política:
  • Son reintentables las peticiones en las que se produzcan:
    • Excepciones de tipo IOException.
    • Excepciones debido a status codes 500 (internal server error), 502 (bad gateway), 503 (service unavailable) o 504 (gateway timeout)
    • Excepciones causadas por throttling. Esto se refiere a excepciones debidas a que estamos superando el nivel de servicio que nos da Amazon para determinados servicios (estamos excediento el ancho de banda, la tasa de peticiones, el total de peticiones permitido, etc.).
  • El tiempo de espera entre reintentos se calcula aplicando un retraso que crece exponencialmente con cada reintento. Dependiendo del tipo de excepción, se aplican estrategias un poco distintas:
    • Si se produjo una excepción debida a throttling: aplica una estrategia EqualJitterBackoffStrategy configurada con un tiempo base de 500 milisegundos y un máximo de 20 segundos.
    • En otro caso: aplica una estrategia FullJitterBackoffStrategy configurada con un tiempo base de 100 milisegundos y un tiempo máximo de 20 segundos.
  • El máximo número de reintentos es 3.
Se observa que el tiempo de espera de reintentos a aplicar en caso excepciones por throttling es mayor y entiendo que se debe a que estas excepciones son casusadas un consumo excesivo por parte del cliente y así se está dando más tiempo a que el servicio de Amazon se "recupere del esfuerzo".
Para DynamoDB se configura por defecto una política de reintentos diferente. Para quien tenga curiosidad por echar un ojo al código fuente, las políticas por defecto están definidas en la clase PredefinedRetryPolicies.

Throttled Retries

Siendo en principio una buena opción aplicar reintentos para evitar errores debidos a un problema puntual en una petición, los reintentos son menos útiles cuando el problema de de mayor duración. En esos casos el consumidor queda esperando un tiempo innecesario mientras se realizan los reintentos, cuando van a ser reintentos infructuosos y al final se va a lanzar una excepción de todos modos. Para evitar esto, Amazon incluyó throttled retries y desde hace un tiempo están activados por defecto.

El modo de funcionar es sencillo: se da una capacidad de reintentos inicial, que se va agotando cuando se producen reintentos infructuosos y, cuando se ha agotado el cliente de AWS ya no reintenta. De este modo, mientras el servicio o la red no estén operativos subsiguientes invocaciones fallarán directamente y no harán reintendos. Una vez vuelve a funcionar bien el servicio, se recupera la capacidad de reintentos.
Es importante señalar como detalle que las excepciones debidas a throttling no gastan capacidad de reintentos. Esto es así porque se deben a un consumo excesivo (no a un problema de conexión o funcionamiento del servicio) y en estos casos sí interesa seguir reintentando tras un tiempo prudencial de espera.

Entonces, ¿merece la pena que nosotros hagamos reintentos?

Para que no haya dudas, con aplicar reintentos me estoy refiriendo a, tras un fallo en una petición utilizando el cliente del servicio AWS, capturar en nuestro código la excepción y repetir exactamente la misma petición con la esperanza de que en esta siguiente ocasión funcione.

Como casi siempre, la respuesta a si merece la pena sería... depende. Hay casos en que, en base a la necesidad del servicio, la lógica de se debe aplicar, etc. nos interesa capturar excepciones que puedan dar los servicios de AWS para realizar reintentos. Por ejemplo, nos ha ocurrido en alguna ocación tener un servicio que escribía en un stream de Kinesis y, al escalar, en ocasiones saturaba la capacidad y recibíamos una ProvisionedThroughputExceededException ... y, dado que en nuestro caso no debíamos propagar la excepción, optamos por introducir un control de reintentos por nuestra parte.

Dicho lo anterior, por mi parte la recomendación general sería confiar en los reintentos que realizan las librerías de Amazon por su cuenta en lugar de introducir gestión de reintentos en nuestro código, dejando esto último para situaciones en las que no nos queda otra. Como hemos visto, además, sus políticas de reintentos son configurables, de modo que podemos adaptarlas por nuestra cuenta para ajustar el número total de reintentos, el tiempo de espera entre reintentos, etc.

Comentarios

Entradas populares de este blog

Creando un proyecto con Spring Initializr: diferencias entre un empaquetado jar y un war

Revisando la jerarquía de dependencias de Spring Cloud