problemas y soluciones / Sudo Null IT News

En los últimos años, hemos visto un aumento en el desarrollo de herramientas y plataformas que permiten la ejecución duradera. Déjame explicarte un poco su principio.

Hoy en día, las computadoras han alcanzado velocidades tales que pueden escribir el resultado de cada tarea no trivial en un almacenamiento permanente. Esto, a su vez, les permite recuperarse perfectamente de una falla temporal al volver a ejecutar todas las tareas completadas en el registro hasta el momento de esta falla. Una vez completadas estas tareas, el sistema continúa funcionando silenciosamente desde el punto donde se interrumpió. Con suficiente atención y cuidado, dicho mecanismo se puede implementar con un impacto mínimo en el modelo de programación o en el rendimiento, lo que sin duda es muy valioso. ¿No es?

Sin embargo, implementar un mecanismo de ejecución robusto plantea una serie de desafíos, el más desafiante de los cuales, como ocurre con muchos aspectos de la infraestructura, es cómo actualizar el código de forma segura.

En el caso típico, sin un mecanismo de ejecución persistente, la actualización del código no suele suponer un gran problema. Suponiendo que se implemente alguna forma de reproducción, lo que podría suceder es que la solicitud comience en la versión anterior del código, se interrumpa a mitad de la ejecución y luego se reproduzca en la versión actualizada. En la práctica, esto no es un problema si ambos controladores son idempotentes (que deben serlo para implementar la reproducción), reciben los mismos parámetros de entrada y tienen un comportamiento compatible más o menos. Puede darse una situación en la que algunos aspectos de la lógica empresarial del código antiguo y el nuevo se mezclen, pero en la mayoría de los casos esto no creará problemas graves.

Pero en el contexto de una ejecución estable, incluso cambios leves en el controlador durante la ejecución de la solicitud pueden provocar su falla y la necesidad de intervención manual. Veamos un ejemplo en el que mi controlador es parte del flujo de pago. En este caso, es posible que necesite agregar un paso de llamada a un servicio externo al comienzo del flujo para verificar si el producto tiene descuento. Lógicamente, espero que las solicitudes activas que ya hayan pasado el punto en el que se introdujo el nuevo paso no se verán afectadas. Pero en realidad resulta diferente.

Cualquier solicitud activa que comenzó en la versión anterior del código y se reproduce en la nueva fallará o incluso provocará un comportamiento indefinido. El hecho es que volverá a pasar por el paso en el que se debe verificar el descuento, no encontrará un registro de esta operación en su registro y le resultará difícil elegir otra acción. El registro en este caso pierde coherencia con el código.

Este es un problema de inmutabilidad. El código que ejecuta una solicitud particular nunca debería cambiar su comportamiento, a pesar de la posibilidad de reproducir las solicitudes mucho después de que se iniciaron.

Soluciones avanzadas

Cada plataforma con un mecanismo de ejecución persistente tiene su propia solución a este problema. Veamos algunos de ellos.

▍ Funciones duraderas de Azure

Las funciones duraderas son esencialmente consumidores de eventos que se implementan en la plataforma Azure Functions. Por lo general, no se implementan entre sí, sino que implementan la solidez de un conjunto de métodos dentro de una única función implementada. El código en este caso es mutable y la reproducción siempre se realizará utilizando la última versión de la función.

La estrategia de actualización recomendada es implementar la nueva versión del código en paralelo con la anterior para que las solicitudes activas no vean la actualización. Para implementar este mecanismo se proponen dos métodos:

  1. Copiar y pegar los métodos modificados de un flujo de trabajo completo como nuevos métodos en el mismo artefacto (llamados “funciones” en la aplicación Functions) y actualizar las personas que llaman para usar esos nuevos métodos para consultas. Sin embargo, las actualizaciones también requerirán la implementación de nuevas versiones, lo que significa que esto deberá hacerse de forma recursiva hasta que la cadena de llamadas conduzca a un punto de entrada y, por lo tanto, no se recomienda este enfoque.
  2. No actualice la implementación existente, sino cree una completamente nueva para todo el paquete de código y actualice las funciones estables que llaman a la API para usar esta nueva implementación. En este caso, las llamadas activas continuarán ejecutándose en relación con el código anterior.

A Azure se le ocurrió la solución adecuada. Es muy bueno cuando es posible hacer que las consultas activas sigan funcionando con la versión del código con la que comenzaron. Pero esta solución todavía tiene un inconveniente. Idealmente, queremos que las nuevas llamadas a una función persistente determinada utilicen automáticamente la última versión del código. Implementar una nueva función persistente y actualizar las personas que llaman es un proceso bastante tedioso. Entonces, en la práctica, algunos pueden optar por vivir con este problema mediante pequeños cambios y aceptar fallas ocasionales.

▍ temporales

Los trabajadores temporales son consumidores de eventos desplegados en infraestructura dedicada, como contenedores de Kubernetes. Como resultado, el código es mutable; simplemente puede implementar una nueva imagen de contenedor.

En los últimos años han surgido varias buenas formas de manejar el control de versiones, pero la mejor práctica actual es versionirovanie vorkerov. En este modelo, al trabajador (que probablemente incluirá el código de muchos subprocesos de trabajo) se le debe asignar un ID de ensamblaje. Como resultado, cuando reciba trabajo, solo solicitará reejecuciones que ya se hayan iniciado en esa compilación. De forma predeterminada, se configura una ID de ensamblaje. En este caso, el trabajador también recibirá solicitudes que se inicien por primera vez.

Para que este mecanismo funcione, el ensamblaje asignado al trabajador debe continuar ejecutándose hasta que se completen todas las solicitudes activas en él. Si estamos viendo un hilo de trabajo corto, esto rara vez es un problema, aunque aún requiere atención al eliminar trabajadores antiguos, y eventualmente es necesario eliminarlos, ya que en una infraestructura dedicada requieren recursos para su mantenimiento.

Pero en el caso de flujos de trabajo a largo plazo, esta solución ya no es adecuada. En teoría, la repetición puede ocurrir meses o incluso años después del inicio de la ejecución; esta es una de las características más interesantes de Temporal. En tales escenarios, existen dos problemas con el almacenamiento de código a largo plazo.

Primero, los costos. Si mis consultas se ejecutan durante un mes y realizo 5 cambios disruptivos en ese mes, necesitaré ejecutar 5 trabajadores en paralelo. En segundo lugar, está la cuestión de la seguridad y la fiabilidad, es decir, la ejecución de código arbitrariamente antiguo en la infraestructura. Es posible que los cambios no afecten la lógica empresarial, pero afectarán los parámetros de conexión de la base de datos o las dependencias que tienen vulnerabilidades. También puede haber cambios que deban aplicarse a las consultas activas. Es decir, debe haber un punto extremo en el que consideramos que el código es demasiado antiguo para ejecutarlo. Y necesitaremos respaldar periódicamente los cambios en las versiones antiguas que continúan ejecutándose.

En los casos en los que el control de versiones de los trabajadores no es suficiente, Temporal ofrece una API para parchear los subprocesos de los trabajadores. Con ellos puedes agregar o quitar pasos, rodeándolos de instrucciones. if. Esto asegurará que los nuevos pasos se ejecuten solo para llamadas nuevas y nunca para repeticiones o, por el contrario, que los pasos eliminados continúen ejecutándose durante las repeticiones. Este mecanismo flexible le ayudará a afrontar casos complejos, pero debe tener en cuenta que estos parches se acumularán en el código y deberán eliminarse con sumo cuidado.

▍ Funciones de paso de AWS

Las funciones de pasos de AWS se describen en JSON utilizando un lenguaje de flujo de trabajo llamado ASL (Amazon States Language). Aunque aquí debe escribir código como funciones lambda, la ejecución sólida solo se extiende a las etapas dentro de la definición del flujo de trabajo. Y esta definición es completamente inmutable: las actualizaciones crean una nueva versión que se usa para ejecutar nuevos subprocesos de trabajo, pero los procesos activos siempre usan la versión con la que comenzaron a ejecutarse. Esto resuelve completamente el problema. Guardar versiones anteriores no cuesta nada; después de todo, sólo se guarda un archivo.

La naturaleza de los flujos de trabajo de Step Functions significa que, al mantener versiones anteriores, rara vez tendrá que preocuparse por parches o cambios de infraestructura. El punto radica en las lambdas que llama el subproceso de trabajo y que no son un objeto de ejecución persistente y, por lo tanto, no tienen problemas de control de versiones más allá del seguimiento estándar de solicitud/respuesta.

▍ Nuestra solución

Al crear Restate, queríamos combinar lo mejor de estos enfoques. Step Functions proporciona la experiencia de usuario óptima hasta el momento: aquí, gracias a los subprocesos de trabajo inmutables, no tiene que pensar en el control de versiones, pero al mismo tiempo se ve obligado a escribir estos subprocesos en ASL.

Realmente admiramos la capacidad que ofrecen Azure y Temporal para implementar flujos de trabajo como código, pero dicho código es inevitablemente mutable, lo que crea problemas. ¿Cómo combinar estos dos enfoques?

En Restate, los “subprocesos de trabajo” se parecen más a un código repetitivo que a subprocesos de trabajo. Más específicamente, están representados como controladores RPC. Aquí no hay consumidores de eventos. El tiempo de ejecución siempre realiza solicitudes a sus servicios, que pueden ejecutarse como contenedores de larga duración o funciones lambda. Todo lo que necesita hacer es registrar un punto final HTTP o Lambda con Restate, lo que determinará qué servicios ejecutar en él, creará una nueva versión de esos servicios y comenzará a usar esa versión para nuevas solicitudes.

Como efecto secundario del uso de funciones lambda, obtenemos inmutabilidad del código. Las versiones publicadas de las funciones lambda son inmutables: cualquier actualización del código o la configuración da como resultado la implementación de una nueva versión. Al mismo tiempo, las versiones se pueden guardar indefinidamente y las antiguas se pueden recuperar del mismo modo que las nuevas, sin costes adicionales. Al integrar la abstracción de versiones con Lambda, podemos brindar la misma experiencia que Step Functions. Las consultas activas siempre se ejecutarán con el código con el que comenzaron y las nuevas utilizarán la última versión.

Sin embargo, los controladores de larga duración siguen siendo un problema. Aunque las funciones lambda normalmente tienen pocas dependencias más allá del SDK de AWS, es posible que aún se requieran parches de seguridad y la infraestructura puede cambiar de manera que las versiones anteriores de las funciones lambda dejen de funcionar. Además, si limitamos la duración de nuestras solicitudes a, digamos, una hora, tenemos un problema con otros tipos de implementaciones, en particular con los contenedores de Kubernetes. Necesitamos poder guardar versiones antiguas de código durante una hora. La forma más sencilla de implementar esto es implementar ambos contenedores en el mismo pod de Kubernetes, sirviéndolos en diferentes puertos o rutas. Con el tiempo, planeamos proporcionar operadores y herramientas de integración continua que simplificarán este proceso.

Sin embargo, sigue existiendo la complejidad de los controladores que funcionan durante varias semanas. Quizás deberíamos hacernos la pregunta: “¿Por qué la gente escribe código como este en primer lugar?” Y discutiremos este tema en la segunda parte del artículo. Mientras tanto, si está escribiendo código de esta manera y desea discutirlo, únase a nuestro canal de discordia.

Canal de Telegram con descuentos, sorteos y novedades informáticas

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *