Problema de macro de microsserviços



Em apenas 20 anos, o desenvolvimento de software mudou de monólitos arquitetônicos com um único banco de dados e estado centralizado para microsserviços, onde tudo é distribuído por vários contêineres, servidores, data centers e até mesmo continentes. A distribuição torna o dimensionamento mais fácil, mas também apresenta desafios totalmente novos, muitos dos quais anteriormente resolvidos com monólitos.



Vamos fazer um rápido tour pela história dos aplicativos em rede para descobrir como chegamos aqui hoje. E então vamos falar sobre o modelo de execução stateful usado em Temporal.e como ele resolve o problema de arquiteturas orientadas a serviços (SOA). Posso ser tendencioso porque dirijo o departamento de mercearia na Temporal, mas acredito que essa abordagem é o futuro.



Uma curta aula de história



Vinte anos atrás, os desenvolvedores quase sempre criavam aplicativos monolíticos. É um modelo simples e consistente, semelhante a como você programa em seu ambiente local. Por sua natureza, os monólitos dependem de um único banco de dados, ou seja, todos os estados são centralizados. Dentro de uma única transação, um monólito pode alterar qualquer um de seus estados, ou seja, ele dá um resultado binário: funcionou ou não. Não há espaço para inconsistência. Ou seja, a coisa maravilhosa sobre o monólito é que não haverá nenhum estado inconsistente devido a uma transação com falha. E isso significa que os desenvolvedores não precisam escrever código, o tempo todo adivinhando sobre o estado dos diferentes elementos.



Por muito tempo, os monólitos fizeram sentido. Ainda não havia muitos usuários conectados, então os requisitos de dimensionamento do software eram mínimos. Mesmo os maiores gigantes do software operavam sistemas que eram insignificantes para os padrões modernos. Apenas um punhado de empresas como Amazon e Google usaram soluções em grande escala, mas essas foram as exceções à regra.



Pessoas como software







Nos últimos 20 anos, os requisitos de software têm crescido constantemente. Hoje, os aplicativos devem funcionar no mercado global desde o primeiro dia. Empresas como o Twitter e o Facebook tornaram o 24/7 online um pré-requisito. Os aplicativos não oferecem mais nada, eles próprios se tornaram uma experiência do usuário. Cada empresa hoje deve ter produtos de software. "Confiabilidade" e "disponibilidade" não são mais propriedades, mas requisitos.



Infelizmente, os monólitos começaram a desmoronar quando "escalabilidade" e "disponibilidade" foram adicionadas aos requisitos. Desenvolvedores e empresas precisavam encontrar maneiras de acompanhar o crescimento global explosivo e as exigentes expectativas dos usuários. Tive que procurar arquiteturas alternativas que reduzissem os problemas emergentes associados ao dimensionamento.



Microsserviços (bem, arquiteturas orientadas a serviços) foram a resposta. Inicialmente, eles pareciam uma ótima solução porque permitiam que você dividisse os aplicativos em módulos relativamente autocontidos que podem ser escalados independentemente. E como cada microsserviço mantinha seu próprio estado, os aplicativos não se limitavam mais à capacidade de uma única máquina! Os desenvolvedores finalmente conseguiram criar programas que podiam ser escalonados com o número crescente de conexões. Os microsserviços também proporcionaram flexibilidade às equipes e empresas em seu trabalho devido à transparência na responsabilidade e à separação de arquiteturas.





Não há queijo grátis



Embora os microsserviços tenham resolvido os problemas de escalabilidade e disponibilidade que impediram o crescimento do software, as coisas não ficaram sem nuvens. Os desenvolvedores começaram a perceber que os microsserviços têm falhas graves.



Monoliths geralmente têm um banco de dados e um servidor de aplicativos. E uma vez que o monólito não pode ser dividido, existem apenas duas maneiras de escalar:



  • Vertical : atualizando o hardware para aumentar o rendimento ou a capacidade. Esse dimensionamento pode ser eficaz, mas é caro. E certamente não resolverá o problema para sempre se seu aplicativo precisar continuar crescendo. E se você expandir o suficiente, não haverá equipamento suficiente para atualizar no final.
  • : , . , .


Os microsserviços são diferentes, seu valor está na capacidade de ter muitos "tipos" de bancos de dados, filas e outros serviços que são escalados e gerenciados independentemente uns dos outros. No entanto, o primeiro problema que começou a ser percebido ao mudar para microsserviços foi exatamente o fato de que agora você tem que cuidar de um monte de todos os tipos de servidores e bancos de dados.



Por muito tempo, tudo foi deixado ao acaso, desenvolvedores e operadores saíram por conta própria. Os problemas de gerenciamento de infraestrutura apresentados por microsserviços são difíceis de resolver, no mínimo degradando a confiabilidade do aplicativo.



No entanto, a oferta surge em resposta à demanda. Quanto mais os microsserviços se espalham, mais os desenvolvedores ficam motivados para resolver problemas de infraestrutura. Lentamente, mas com segurança, as ferramentas começaram a surgir e tecnologias como Docker, Kubernetes e AWS Lambda preencheram a lacuna. Eles tornaram a arquitetura de microsserviço muito fácil de operar. Em vez de escrever seu próprio código para orquestrar com contêineres e recursos, os desenvolvedores podem contar com ferramentas predefinidas. Em 2020, finalmente atingimos o marco em que a disponibilidade de nossa infraestrutura não interfere mais na confiabilidade de nossos aplicativos. Perfeitamente!





Claro, ainda não vivemos na utopia de um software perfeitamente estável. A infraestrutura não é mais a fonte de insegurança do aplicativo; o código do aplicativo tomou seu lugar.



Outro problema com microsserviços



Em monólitos, os desenvolvedores escrevem código que muda os estados de maneira binária: ou algo acontece ou não. E com microsserviços, o estado é distribuído por diferentes servidores. Para alterar o estado de um aplicativo, vários bancos de dados devem ser atualizados ao mesmo tempo. As chances são de que um banco de dados seja atualizado com êxito e outros travem, deixando você com um estado intermediário inconsistente. Mas, como os serviços eram a única solução para o problema do dimensionamento horizontal, os desenvolvedores não tinham outra opção.





Um problema fundamental com o estado distribuído pelos serviços é que cada chamada para um serviço externo terá um resultado aleatório em termos de disponibilidade. Obviamente, os desenvolvedores podem ignorar o problema em seu código e considerar cada chamada para uma dependência externa sempre bem-sucedida. Mas então alguma dependência pode colocar o aplicativo fora do ar sem aviso. Portanto, os desenvolvedores tiveram que adaptar seu código da era dos monólitos para adicionar verificações de falha de operações no meio das transações. O seguinte mostra a recuperação contínua do último estado registrado do armazenamento myDB dedicado para evitar condições de corrida. Infelizmente, mesmo essa implementação não ajuda. Se o estado da conta mudar sem atualizar myDB, podem ocorrer inconsistências.



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);
  }
}


Infelizmente, é impossível escrever código sem erros. E quanto mais complexo o código, mais prováveis ​​bugs aparecerão. Como você pode esperar, o código que funciona com o "middleware" não é apenas complexo, mas também complicado. Pelo menos alguma confiabilidade é melhor do que nenhuma confiabilidade, então os desenvolvedores tiveram que escrever esse código inicialmente cheio de erros para manter a experiência do usuário. Custa-nos tempo e esforço, e os empregadores muito dinheiro. Embora os microsserviços tenham uma boa escalabilidade, eles têm um preço para a diversão e produtividade do desenvolvedor, além da confiabilidade do aplicativo.



Milhões de desenvolvedores gastam tempo todos os dias reinventando uma das rodas mais reinventadas - a confiabilidade do clichê. Abordagens modernas para trabalhar com microsserviços simplesmente não refletem os requisitos de confiabilidade e escalabilidade de aplicativos modernos.





Temporal



Agora chegamos à nossa solução. Não é endossado pelo Stack Overflow e não reivindicamos ser perfeito. Queremos apenas compartilhar nossas ideias e ouvir sua opinião. Qual o melhor lugar para obter feedback sobre como melhorar seu código do que a pilha?



Até hoje, não houve uma solução que permite usar microsserviços sem resolver os problemas descritos acima. Você pode testar e emular estados de travamento, escrever código levando os travamentos em consideração, mas esses problemas ainda surgem. Acreditamos que o Temporal os resolve. É um ambiente com estado de código aberto (MIT no-nonsense) para orquestração de microsserviços.



O temporal tem dois componentes principais: um back-end com estado que é executado no banco de dados de sua escolha e uma estrutura cliente em uma das linguagens suportadas. Os aplicativos são construídos usando uma estrutura de cliente e código legado regular que salva automaticamente as mudanças de estado no back-end à medida que são executados. Você pode usar as mesmas dependências, bibliotecas e cadeias de construção como faria ao construir qualquer outro aplicativo. Para ser honesto, o back-end é altamente distribuído, então não é como o J2EE 2.0. Na verdade, é a distribuição do back-end que permite um dimensionamento horizontal quase infinito. Temporal traz consistência, simplicidade e confiabilidade para a camada de aplicativo, assim como a infraestrutura Docker, Kubernetes e arquitetura sem servidor.



O temporal fornece vários mecanismos altamente confiáveis ​​para orquestração de microsserviços. Mas o mais importante é a preservação do estado. Esta função usa a emissão de eventos para salvar automaticamente quaisquer alterações com estado em um aplicativo em execução. Ou seja, se o computador no qual o Temporal está rodando travar, o código irá automaticamente pular para outro computador, como se nada tivesse acontecido. Isso se aplica até mesmo a variáveis ​​locais, threads de execução e outros estados específicos do aplicativo.



Deixe-me fazer uma analogia. Como desenvolvedor, você provavelmente depende hoje do controle de versão do SVN (que é OG Git) para controlar as alterações feitas em seu código. O SVN apenas salva novos arquivos e vincula-os aos arquivos existentes para evitar a duplicação. Temporal é algo como SVN (analogia grosseira) para histórico de estado de aplicativos em execução. Quando seu código altera o estado do aplicativo, o Temporal salva automaticamente essa alteração (não o resultado) sem erro. Ou seja, o Temporal não apenas restaura o aplicativo travado, mas também o reverte, bifurca e faz muito mais. Assim, os desenvolvedores não precisam mais criar aplicativos com a expectativa de que o servidor possa travar.



É como alternar do salvamento manual de documentos (Ctrl + S) após cada caractere digitado para o salvamento automático na nuvem do Google Docs. Não no sentido de que você não salva mais nada manualmente, é só que não há mais nenhuma máquina associada a este documento. Statefulness significa que os desenvolvedores podem escrever um código clichê muito menos chato que teve que ser escrito devido aos microsserviços. Além disso, você não precisa mais de infraestrutura especial - filas, caches e bancos de dados separados. Isso torna mais fácil manter e adicionar novos recursos. Também torna muito mais fácil manter os novatos atualizados, porque eles não precisam entender o código de gerenciamento de estado específico e confuso.



A retenção de estado é implementada na forma de "temporizadores persistentes". Este é um mecanismo à prova de falhas que pode ser usado com um comando Workflow.sleep. Funciona exatamente da mesma maneira que sleep. No entanto, Workflow.sleeppode ser sacrificado com segurança por qualquer período de tempo. Muitos usuários temporais estão dormindo há semanas ou até anos. Isso é feito armazenando temporizadores de longa duração no armazenamento Temporal e controlando o código para ativar. Novamente, mesmo se o servidor travar (ou você apenas desligá-lo), o código irá para a máquina disponível quando o cronômetro expirar. Os processos de hibernação não consomem recursos, você pode ter milhões com sobrecarga insignificante. Pode parecer muito abstrato, então aqui está um exemplo de um 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);
    }
  }
}


Além do estado persistente, o Temporal oferece um conjunto de mecanismos para a construção de aplicativos robustos. As funções de atividade são chamadas de fluxos de trabalho, mas o código executado dentro da atividade não tem estado. Embora não persistam, as atividades contêm novas tentativas automáticas, tempos limite e pulsações. As atividades são muito úteis para encapsular código que pode falhar. Digamos que seu aplicativo use uma API bancária que geralmente não está disponível. Para software legado, você precisa agrupar todo o código que chama esta API com instruções try / catch, lógica de repetição e tempos limite. Mas se você chamar a API de banco de uma atividade, todas essas funções são fornecidas imediatamente: se a chamada falhar, a atividade será repetida automaticamente. Está tudo ótimomas às vezes você possui um serviço não confiável e deseja protegê-lo contra DDoS. Portanto, as chamadas de atividade também suportam tempos limite, apoiados por longos temporizadores. Ou seja, as pausas entre repetições de atividades podem chegar a horas, dias ou semanas. Isso é especialmente útil para código que precisa ser executado com êxito, mas você não tem certeza de quão rápido isso precisa acontecer.



Este vídeo explica o modelo de programação em Temporal em dois minutos:





Outro ponto forte do Temporal é a capacidade de observação do aplicativo em execução. A API de observação fornece uma interface semelhante a SQL para consultar metadados de qualquer fluxo de trabalho (executável ou não). Você também pode definir e atualizar seus próprios valores de metadados diretamente no processo. A API de observação é muito útil para operadores temporais e desenvolvedores, especialmente ao depurar durante o desenvolvimento. O monitoramento também oferece suporte a ações em lote nos resultados da consulta. Por exemplo, você pode enviar um sinal kill para todos os processos de trabalho que correspondem a uma solicitação com um tempo de criação> ontem. Temporal oferece suporte a um recurso de busca síncrona que permite extrair os valores de variáveis ​​locais de instâncias em execução. É como se um depurador de seu IDE estivesse trabalhando com aplicativos de produção. Por exemplo, é assim que você pode obter o valorgreeting em uma instância em execução:



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;
    }
  }


Conclusão



Os microsserviços são excelentes e têm o preço da produtividade e confiabilidade que os desenvolvedores e empresas pagam. O Temporal foi projetado para resolver esse problema, fornecendo um ambiente que paga microsserviços para desenvolvedores. Statefulness pronto para uso, falhas automáticas e watchdogs são apenas alguns dos recursos que o Temporal possui que tornam o desenvolvimento de microsserviços inteligente.



All Articles