Problema de macro de microservicios



En solo 20 años, el desarrollo de software ha pasado de monolitos arquitectónicos con una sola base de datos y estado centralizado a microservicios, donde todo se distribuye en numerosos contenedores, servidores, centros de datos e incluso continentes. La distribución facilita el escalado, pero también presenta desafíos completamente nuevos, muchos de los cuales se resolvieron previamente con monolitos.



Hagamos un recorrido rápido por la historia de las aplicaciones en red para descubrir cómo llegamos aquí hoy. Y luego hablemos sobre el modelo de ejecución con estado utilizado en Temporal.y cómo resuelve el problema de las arquitecturas orientadas a servicios (SOA). Puedo ser parcial porque dirijo el departamento de comestibles en Temporal, pero creo que este enfoque es el futuro.



Una breve lección de historia



Hace veinte años, los desarrolladores casi siempre creaban aplicaciones monolíticas. Es un modelo simple y consistente, similar a cómo programa en su entorno local. Por su naturaleza, los monolitos dependen de una única base de datos, es decir, todos los estados están centralizados. Dentro de una sola transacción, el monolito puede cambiar cualquiera de sus estados, es decir, da un resultado binario: si funcionó o no. No hay lugar para la inconsistencia. Es decir, lo maravilloso del monolito es que no habrá ningún estado inconsistente debido a una transacción fallida. Y esto significa que los desarrolladores no necesitan escribir código, todo el tiempo adivinando sobre el estado de diferentes elementos.



Durante mucho tiempo, los monolitos tuvieron sentido. Todavía no había muchos usuarios conectados, por lo que los requisitos de escalado del software eran mínimos. Incluso los gigantes del software más grandes operaban sistemas que eran insignificantes para los estándares modernos. Solo un puñado de empresas como Amazon y Google utilizaron soluciones a gran escala, pero estas fueron las excepciones a la regla.



Personas como software







Durante los últimos 20 años, los requisitos de software han aumentado constantemente. Hoy, las aplicaciones deberían funcionar en el mercado global desde el primer día. Empresas como Twitter y Facebook han hecho de las 24 horas del día, los 7 días de la semana, un requisito previo. Las aplicaciones ya no proporcionan nada, se han convertido en una experiencia de usuario. Todas las empresas de hoy deben tener productos de software. "Fiabilidad" y "disponibilidad" ya no son propiedades, sino requisitos.



Desafortunadamente, los monolitos comenzaron a desmoronarse cuando se agregaron "escalabilidad" y "disponibilidad" a los requisitos. Tanto los desarrolladores como las empresas necesitaban encontrar formas de mantenerse al día con el explosivo crecimiento global y las exigentes expectativas de los usuarios. Tuve que buscar arquitecturas alternativas que redujeran los problemas emergentes asociados con el escalado.



Los microservicios (bueno, arquitecturas orientadas a servicios) fueron la respuesta. Inicialmente, parecían una gran solución porque le permitían dividir aplicaciones en módulos relativamente independientes que se pueden escalar de forma independiente. Y dado que cada microservicio mantuvo su propio estado, ¡las aplicaciones ya no se limitaron a la capacidad de una sola máquina! Los desarrolladores finalmente pudieron crear programas que pudieran escalar con el creciente número de conexiones. Los microservicios también brindaron a los equipos y empresas flexibilidad en su trabajo debido a la transparencia en la responsabilidad y la separación de arquitecturas.





No hay queso gratis



Si bien los microservicios han resuelto los problemas de escalabilidad y disponibilidad que han obstaculizado el crecimiento del software, las cosas no han sido sin nubes. Los desarrolladores comenzaron a darse cuenta de que los microservicios tienen fallas graves.



Los monolitos suelen tener una base de datos y un servidor de aplicaciones. Y dado que el monolito no se puede dividir, solo hay dos formas de escalar:



  • Vertical : actualización de hardware para aumentar el rendimiento o la capacidad. Esta escala puede ser eficaz, pero es cara. Y ciertamente no solucionará el problema para siempre si su aplicación necesita seguir creciendo. Y si se expande lo suficiente, no terminará con suficiente equipo para actualizar.
  • : , . , .


Los microservicios son diferentes, su valor radica en la capacidad de tener muchos "tipos" de bases de datos, colas y otros servicios que se escalan y administran independientemente unos de otros. Sin embargo, el primer problema que se empezó a notar al cambiar a microservicios fue precisamente el hecho de que ahora hay que encargarse de un montón de todo tipo de servidores y bases de datos.



Durante mucho tiempo, todo se dejó al azar, los desarrolladores y operadores salieron por su cuenta. Los problemas de gestión de la infraestructura que plantean los microservicios son difíciles de abordar y, en el mejor de los casos, degradan la fiabilidad de las aplicaciones.



Sin embargo, la oferta surge en respuesta a la demanda. Cuanto más se extienden los microservicios, más motivados están los desarrolladores para resolver problemas de infraestructura. De forma lenta pero segura, empezaron a surgir herramientas y tecnologías como Docker, Kubernetes y AWS Lambda llenaron el vacío. Hicieron que la arquitectura de microservicios fuera muy fácil de operar. En lugar de escribir su propio código para orquestar con contenedores y recursos, los desarrolladores pueden confiar en herramientas prediseñadas. En 2020, finalmente hemos alcanzado el hito en el que la disponibilidad de nuestra infraestructura ya no interfiere con la confiabilidad de nuestras aplicaciones. ¡Perfectamente!





Por supuesto, todavía no vivimos en la utopía de un software perfectamente estable. La infraestructura ya no es la fuente de inseguridad de las aplicaciones; el código de la aplicación ha ocupado su lugar.



Otro problema con los microservicios



En los monolitos, los desarrolladores escriben código que cambia de estado de forma binaria: o sucede algo o no. Y con los microservicios, el estado se distribuye en diferentes servidores. Para cambiar el estado de una aplicación, se deben actualizar varias bases de datos al mismo tiempo. Lo más probable es que una base de datos se actualice correctamente y otras se bloqueen, dejándolo con un estado intermedio inconsistente. Pero dado que los servicios eran la única solución al problema del escalado horizontal, los desarrolladores no tenían otra opción.





Un problema fundamental con el estado distribuido entre los servicios es que cada llamada a un servicio externo tendrá un resultado aleatorio en términos de disponibilidad. Por supuesto, los desarrolladores pueden ignorar el problema en su código y considerar cada llamada a una dependencia externa siempre exitosa. Pero luego, cierta dependencia puede dejar la aplicación sin previo aviso. Por lo tanto, los desarrolladores tuvieron que adaptar su código de la era de los monolitos para agregar verificaciones por fallas en las operaciones en medio de las transacciones. A continuación, se muestra cómo recuperar continuamente el último estado registrado de una tienda myDB dedicada para evitar condiciones de carrera. Desafortunadamente, incluso esta implementación no ayuda. Si el estado de la cuenta cambia sin actualizar myDB, pueden ocurrir inconsistencias.



public void transferWithoutTemporal(
  String fromId, 
  String toId, 
  String referenceId, 
  double amount,
) {
  boolean withdrawDonePreviously = myDB.getWithdrawState(referenceId);
  if (!withdrawDonePreviously) {
      account.withdraw(fromAccountId, referenceId, amount);      
      myDB.setWithdrawn(referenceId);
  }
  boolean depositDonePreviously = myDB.getDepositState(referenceId);
  if (!depositDonePreviously) {
      account.deposit(toAccountId, referenceId, amount);                
      myDB.setDeposited(referenceId);
  }
}


Por desgracia, es imposible escribir código sin errores. Y cuanto más complejo sea el código, es más probable que aparezcan errores. Como era de esperar, el código que funciona con el "middleware" no solo es complejo sino también intrincado. Al menos algo de confiabilidad es mejor que ninguna confiabilidad, por lo que los desarrolladores tuvieron que escribir un código inicialmente con errores para mantener la experiencia del usuario. Nos cuesta tiempo y esfuerzo, y a los empleadores mucho dinero. Si bien los microservicios escalan bien, tienen un precio de diversión y productividad para el desarrollador, y confiabilidad de la aplicación.



Millones de desarrolladores dedican tiempo todos los días a reinventar una de las ruedas más reinventadas: la confiabilidad del modelo estándar. Los enfoques modernos para trabajar con microservicios simplemente no reflejan los requisitos de confiabilidad y escalabilidad de las aplicaciones modernas.





Temporal



Ahora llegamos a nuestra solución. No está respaldado por Stack Overflow y no pretendemos ser perfectos. Solo queremos compartir nuestras ideas y escuchar tu opinión. ¿Qué mejor lugar para obtener comentarios sobre cómo mejorar su código que Stack?



Hasta el día de hoy, no ha habido ninguna solución que le permita utilizar microservicios sin resolver los problemas descritos anteriormente. Puede probar y emular estados de bloqueo, escribir código teniendo en cuenta los bloqueos, pero estos problemas aún surgen. Creemos que Temporal los resuelve. Es un entorno con estado de código abierto (MIT sensato) para la orquestación de microservicios.



Temporal tiene dos componentes principales: un backend con estado que se ejecuta en la base de datos de su elección y un marco de cliente en uno de los idiomas admitidos. Las aplicaciones se crean utilizando un marco cliente y un código heredado regular que guarda automáticamente los cambios de estado en el backend mientras se ejecutan. Puede utilizar las mismas dependencias, bibliotecas y cadenas de compilación que utilizaría al compilar cualquier otra aplicación. Para ser honesto, el backend está muy distribuido, por lo que no es como J2EE 2.0. De hecho, es la distribución del backend la que permite un escalado horizontal casi infinito. Temporal aporta consistencia, simplicidad y confiabilidad a la capa de aplicación, al igual que la infraestructura de Docker, Kubernetes y la arquitectura sin servidor.



Temporal proporciona una serie de mecanismos altamente confiables para la orquestación de microservicios. Pero lo más importante es preservar el estado. Esta función utiliza la emisión de eventos para guardar automáticamente cualquier cambio con estado en una aplicación en ejecución. Es decir, si la computadora en la que se está ejecutando Temporal falla, el código saltará automáticamente a otra computadora como si nada hubiera pasado. Esto incluso se aplica a variables locales, subprocesos de ejecución y otros estados específicos de la aplicación.



Dejame darte una analogía. Como desarrollador, probablemente confíe hoy en el control de versiones de SVN (eso es OG Git) para realizar un seguimiento de los cambios en su código. SVN simplemente guarda archivos nuevos y luego vincula a archivos existentes para evitar la duplicación. Temporal es algo así como SVN (analogía aproximada) para el historial con estado de las aplicaciones en ejecución. Cuando su código cambia el estado de la aplicación, Temporal guarda automáticamente ese cambio (no el resultado) sin error. Es decir, Temporal no solo restaura la aplicación bloqueada, también la revierte, se bifurca y hace mucho más. Por lo tanto, los desarrolladores ya no necesitan crear aplicaciones con la expectativa de que el servidor se bloquee.



Es como cambiar de guardar documentos manualmente (Ctrl + S) después de cada carácter escrito al guardado automático en la nube de Google Docs. No en el sentido de que ya no guarde nada manualmente, es solo que ya no hay ninguna máquina asociada con este documento. Statefulness significa que los desarrolladores pueden escribir un código repetitivo mucho menos aburrido que tuvo que ser escrito debido a los microservicios. Además, ya no necesita una infraestructura especial: colas, cachés y bases de datos independientes. Esto facilita el mantenimiento y la incorporación de nuevas funciones. También hace que sea mucho más fácil poner al día a los novatos, ya que no necesitan comprender el código de administración estatal confuso y específico.



La retención de estado se implementa en forma de "temporizadores persistentes". Es un mecanismo a prueba de fallas que se puede utilizar con un comando Workflow.sleep. Funciona exactamente de la misma forma que sleep. Sin embargo, Workflow.sleepse puede sacrificar de forma segura durante cualquier período de tiempo. Muchos usuarios de Temporal han estado durmiendo durante semanas o incluso años. Esto se logra almacenando temporizadores de larga duración en la tienda Temporal y realizando un seguimiento del código para despertar. Nuevamente, incluso si el servidor falla (o simplemente lo apaga), el código irá a la máquina disponible cuando expire el temporizador. Los procesos de suspensión no consumen recursos, puede tener millones de ellos con una sobrecarga insignificante. Puede sonar demasiado abstracto, así que aquí hay un ejemplo de un código temporal funcional:



public class SubscriptionWorkflowImpl implements SubscriptionWorkflow {
  private final SubscriptionActivities activities =
      Workflow.newActivityStub(SubscriptionActivities.class);
  public void execute(String customerId) {
    activities.onboardToFreeTrial(customerId);
    try {
      Workflow.sleep(Duration.ofDays(180));
      activities.upgradeFromTrialToPaid(customerId);
      while (true) {
        Workflow.sleep(Duration.ofDays(30));
        activities.chargeMonthlyFee(customerId);
      }
    } catch (CancellationException e) {
      activities.processSubscriptionCancellation(customerId);
    }
  }
}


Además del estado persistente, Temporal ofrece un conjunto de mecanismos para crear aplicaciones robustas. Las funciones de actividad se llaman desde flujos de trabajo, pero el código que se ejecuta dentro de la actividad no tiene estado. Aunque no guardan su estado, las actividades contienen reintentos automáticos, tiempos de espera y latidos. Las actividades son muy útiles para encapsular código que podría fallar. Supongamos que su aplicación utiliza una API bancaria que a menudo no está disponible. Para el software heredado, debe ajustar todo el código que llama a esta API con declaraciones try / catch, lógica de reintento y tiempos de espera. Pero si llama a la API bancaria desde una actividad, todas estas funciones se proporcionan listas para usar: si la llamada falla, la actividad se reintentará automáticamente. Todo es genialpero a veces usted mismo posee un servicio poco confiable y desea protegerlo de DDoS. Por lo tanto, las llamadas de actividad también admiten tiempos de espera, respaldados por temporizadores prolongados. Es decir, las pausas entre repeticiones de actividades pueden llegar a horas, días o semanas. Esto es especialmente útil para el código que debe ejecutarse correctamente, pero no está seguro de qué tan rápido debe hacerlo.



Este video explica el modelo de programación temporal en dos minutos:





Otro punto fuerte de Temporal es la observabilidad de la aplicación en ejecución. La API de observación proporciona una interfaz similar a SQL para consultar metadatos de cualquier flujo de trabajo (ejecutable o no). También puede definir y actualizar sus valores de metadatos directamente dentro del proceso. La API de observación es muy útil para los operadores y desarrolladores temporales, especialmente cuando se depura durante el desarrollo. La supervisión incluso admite acciones por lotes sobre los resultados de las consultas. Por ejemplo, puede enviar una señal de interrupción a todos los procesos de trabajo que coincidan con una solicitud con una hora de creación> ayer. Temporal admite una función de búsqueda sincrónica que le permite extraer los valores de las variables locales de instancias en ejecución. Es como si un depurador de su IDE hubiera estado trabajando con aplicaciones de producción. Por ejemplo, así es como puede obtener el valorgreeting en una instancia en ejecución:



public static class GreetingWorkflowImpl implements GreetingWorkflow {

    private String greeting;

    @Override
    public void createGreeting(String name) {
      greeting = "Hello " + name + "!";
      Workflow.sleep(Duration.ofSeconds(2));
      greeting = "Bye " + name + "!";
    }

    @Override
    public String queryGreeting() {
      return greeting;
    }
  }


Conclusión



Los microservicios son excelentes, tienen el precio de productividad y confiabilidad que pagan los desarrolladores y las empresas. Temporal está diseñado para resolver este problema proporcionando un entorno que paga microservicios para los desarrolladores. El estado listo para usar, los fallos automáticos y los perros guardianes son solo algunas de las capacidades de Temporal que hacen que el desarrollo de microservicios sea inteligente.



All Articles