Si al principio no tiene éxito: reintentos en microservicios de Eventide

Permitir que un servicio finalice es el curso de acción más común que se debe tomar cuando se encuentra un error inesperado. La palabra clave aquí es inesperada .

Algunos errores son inesperados y deberían hacer que un servicio finalice. Pero hay algunos errores que se esperan, y deben manejarse de manera apropiada, generalmente reintentando cualquier lógica que estuviera en progreso cuando se produjo el error.

Si se espera que ocurra un error durante el manejo de algún mensaje, y si se espera que el error sea recuperable , entonces se debe tener cuidado para implementar la lógica del controlador dentro de un mecanismo de reintento , como la propia palabra clave de retry Ruby, o una biblioteca que implementa la lógica de reintento.

Por ejemplo, si un controlador invoca una API HTTP, entonces se deben tener en cuenta la inevitabilidad de fallas de red intermitentes y momentáneas. Cualquier operación que llegue fuera del proceso actual y aproveche la E / S, especialmente la E / S de red, es susceptible a este tipo de fallas predecibles y recuperables.

Debido a que este tipo de fallas a menudo son momentáneas, por lo general, es suficiente con intentar nuevamente realizar la solicitud HTTP. Si la falla es de hecho una falla transitoria, entonces el problema de la red se resolverá solo, y el segundo intento (o tercero, etc.) de invocar la API a menudo funcionará.

Sin embargo, si la API está experimentando algún tipo de interrupción prolongada, los intentos posteriores también fallarán. En ese momento, debería volverse a intentar después de un número (generalmente un número pequeño) de intentos, y debería permitirse que el error resultante de la falla de la red termine el proceso.

Dado que no es práctico saber con absoluta certeza si una falla de la red es una falla momentánea o una interrupción prolongada, es mejor ajustar la lógica de reintento para realizar un número fijo de intentos, y luego abandonar el intento y permitir que se levante el error para que pueda terminar el servicio.

En el siguiente ejemplo, la ejecución de la API HTTP volverá a intentar si se HTTPError un HTTPError . Reintentará un máximo de tres veces, con un retraso de 100 milisegundos entre el primer y el segundo intento, y un retraso de 200 milisegundos entre el segundo y el tercer intento.

La biblioteca Retry que se envía con la pila Eventide se usa en el ejemplo, pero no es estrictamente necesario usar esta biblioteca. Hay varias implementaciones de reintento disponibles en el ecosistema Ruby. La propia palabra clave de retry Ruby también se puede usar, siempre que se ajuste bien a las circunstancias.

Tenga cuidado de evitar implementar un bucle infinito de reintentos al implementar la lógica de reintento utilizando los bloques de construcción de inicio / rescate / reintento de Ruby. Será difícil salir de un bucle infinito de reintentos de forma segura, y puede obligar a un operador a matar con fuerza bruta un proceso, en lugar de permitir que salga con gracia.

Tratar con los errores de concurrencia esperados cuando se ejecutan servicios en paralelo en una configuración de conmutación por error en caliente es el uso más común de la lógica de reintento en los controladores.

Debido a que la lógica del controlador debe implementarse con protecciones de idempotencia y concurrencia independientemente de si ejecutar servicios en paralelo, volver a intentar la lógica del controlador y reprocesar un mensaje es una operación segura.

Nota: Una discusión detallada de idempotencia y concurrencia está más allá del alcance de este artículo.

El siguiente ejemplo es una representación más real de la implementación de lógica de controlador real. El controlador procesa un comando de Deposit para una Account . Utiliza el mecanismo de versión esperado para proteger contra escrituras concurrentes en la misma secuencia desde dos instancias diferentes del controlador que procesa los mismos comandos de entrada al mismo tiempo. También implementa protecciones de idempotencia utilizando el número de secuencia del comando como clave de idempotencia .

Nuevamente, la implementación ilustrada arriba solo es útil cuando se ejecuta más de una instancia de un servicio que consume simultáneamente los mismos mensajes de entrada. No es una solución para la paralelización a escala horizontal, ya que ese tipo de paralelización requiere múltiples instancias de un servicio para no consumir exactamente los mismos mensajes. Esto se lograría mediante la partición de las secuencias que se están alimentando a un servicio para que cada servicio reciba mensajes diferentes.

El mecanismo de manejo de errores integrado en los consumidores ofrece otra opción para reintentos. Sin embargo, es una solución completamente generalizada y no ofrece flexibilidad para especificar intervalos de retraso de variación o los errores que se manejan. Hacer reintentos directamente en la lógica del controlador sigue siendo la mejor manera de tener un control preciso sobre el comportamiento de reintento.

No obstante, es posible implementar un reintento generalizado en el método error_raised un consumidor.

No es realmente práctico usar una biblioteca como Retry al efectuar reintentos en un consumidor, ya que causaría tres intentos de manejar un mensaje, en lugar de dos.

Si implementa un manejo generalizado de algo como MessageStore::ExpectedVersion::Error , se debe usar un mecanismo más primitivo como se muestra arriba. Como es habitual cuando se implementa el método error_raised , es crítico que el error pasado al método se vuelva a error_raised para garantizar que el error pueda terminar el proceso como una condición excepcional.

El monitoreo de procesos a nivel de sistema, como systemctl, ‘s systemctl, proporciona un medio aún más primitivo de reintentos.

Si el monitor de proceso está configurado para reiniciar un proceso en caso de falla, entonces un error que hace que el proceso finalice se reintentará efectivamente cuando el proceso se reinicie y el mensaje que estaba en proceso cuando se produjo el error se vuelve a procesar.

Debido a que se supone que los controladores se implementan con protección de idempotencia explícita, generalmente es seguro reiniciar un controlador incluso después de que un servicio haya finalizado debido a un error fatal.

A nivel del sistema, la supervisión básica del proceso proporciona controles limitados en los reinicios del proceso, y necesariamente así. A menos que se utilice una solución de monitoreo de procesos de terceros más elaborada, el monitoreo de procesos típico permite lo básico, como reiniciar en caso de falla.

Cuando se utiliza el reinicio de un monitor de proceso a nivel de sistema como mecanismo de reintento, el proceso se puede colocar en un bucle infinito de reinicios. Por lo general, es una condición benigna, pero es algo a tener en cuenta.

Cada reintento es un reinicio . Un reintento en un controlador reinicia la lógica en el controlador. Un reintento en un consumidor reintenta el procesamiento de un mensaje por parte de sus manejadores. Un reintento de un servicio es un reinicio del proceso del servicio a nivel del sistema.

Reintentar y reiniciar son capacidades esenciales de la implementación del servicio. Podría decirse que la lógica de servicio que no se puede reiniciar de manera segura es una lógica de servicio que aún no está lista para ser lanzada y utilizada en un entorno operativo. Independientemente de por qué se efectúa un reinicio, es fundamental estar absolutamente seguro de que la lógica de servicio se implementa para que pueda reiniciarse de manera segura.

Si alguna vez hay alguna duda sobre si uno de los controladores de un servicio se puede reiniciar de manera segura en cualquier nivel, entonces los reinicios no se deben efectuar en ningún nivel, ya sea en el nivel del controlador, el nivel del consumidor o el nivel del proceso del sistema.