O problema macro com microsserviços

Em apenas 20 anos, a engenharia de software passou de monólitos arquitetos com um único banco de dados e estado centralizado para microsserviços onde tudo é distribuído em vários contêineres, servidores, data centers e até continentes. Distribuir coisas resolve preocupações de es

Neste artigo, discutirei como chegamos a este ponto com um breve passeio pela história dos aplicativos em rede. Depois disso, falaremos sobre o modelo de execução imponente da Temporal e como ele tenta resolver os problemas introduzidos pelas arquiteturas orientadas ao serviço (SOA). Em plena divulgação, eu sou o Chefe de Produto da Temporal, então eu posso ser tendencioso, mas eu acho que essa abordagem é o futuro.

 

Uma pequena lição de história

 

Vinte anos atrás, os desenvolvedores quase sempre construíram aplicações monolíticas. O modelo é simples, consistente e semelhante à experiência que você tem na programação em um ambiente local.

 

 

Os monólitos por natureza dependem de um único banco de dados, o que significa que todo estado é centralizado. Um monólito pode mutar qualquer um de seu estado dentro de uma única transação, o que significa que ele produz um resultado binário — funcionou ou não. Não há espaço para inconsistência. Portanto, os monólitos proporcionaram uma grande experiência para os desenvolvedores, pois significavam que não havia chance de transações fracassadas resultando em um estado inconsistente. Por sua vez, isso significava que os desenvolvedores não tinham que escrever constantemente códigos para adivinhar o estado das coisas.

 

Por muito tempo, os monólitos faziam sentido. Não havia uma tonelada de usuários conectados, o que significava que os requisitos de escala para software eram mínimos. Até os maiores gigantes do software operavam em uma escala que parece minúscula hoje. Havia um punhado de empresas como Amazon e Google que estavam funcionando em "escala", mas eram a rara exceção, não a regra.

 

Pessoas gostam de software

 

Nos últimos 20 anos, a demanda por software não parava de crescer. Hoje, espera-se que as aplicações atendam a um mercado global desde o primeiro dia. Empresas como Twitter e Facebook fizeram apostas de tabela 24 horas por dia, 7 horas por dia, 7 horas por dia, 70%. O software não está mais apenas alimentando as coisas nos bastidores, ele se tornou a própria experiência do usuário final. Espera-se que todas as empresas tenham produtos de software. Confiabilidade e disponibilidade não são mais recursos, são requisitos.

 

Infelizmente, os monólitos começam a desmoronar quando a escala e a disponibilidade se tornam requisitos. Desenvolvedores e empresas precisavam encontrar maneiras de acompanhar o rápido crescimento global e exigindo expectativas dos usuários. Eles começaram a procurar arquiteturas alternativas que aliviassem os problemas de escalabilidade que estavam enfrentando.

 

A resposta encontrada foram microsserviços (bem, arquiteturas orientadas a serviços). Os microsserviços pareciam ótimos inicialmente porque permitiam que os aplicativos fossem divididos em unidades relativamente independentes que poderiam ser dimensionadas independentemente. E como cada microsserviço mantinha seu próprio estado, significava que sua aplicação não se limitava mais ao que cabia em uma única máquina! Os desenvolvedores poderiam finalmente criar aplicativos que atendam às demandas de escala de um mundo cada vez mais conectado. Os microsserviços também trouxeram flexibilidade para equipes e empresas, pois forneceram linhas claras de responsabilidade e separação para arquiteturas organizacionais.

 

Não existe almoço grátis.

 

Embora os microsserviços resolvesse os problemas de escalabilidade e disponibilidade que estavam bloqueando fundamentalmente o crescimento do software, nem tudo estava bem. Os desenvolvedores começaram a perceber que os microsserviços vinham com algumas desvantagens sérias.

 

Com monólitos, geralmente havia uma instância de banco de dados e um servidor de aplicativo. E como um monólito não pode ser quebrado, há apenas duas opções práticas para escalar. A primeira opção é o dimensionamento vertical, o que significa atualizar o hardware para aumentar o throughput/capacidade. O dimensionamento vertical pode ser eficiente, mas definitivamente não é uma solução permanente se a sua aplicação precisar continuar crescendo. Se você escala verticalmente o suficiente, você eventualmente ficar sem hardware para atualizar. A segunda opção é o dimensionamento horizontal, que no caso de um monólito significa apenas criar cópias de si mesmo para que cada um sirva a um conjunto específico de usuários/solicitações etc. Os monólitos de dimensionamento horizontal resultam em subutilização de recursos e em escalas altas o suficiente simplesmente não funcionarão. Não é o caso de microsserviços, cujo valor vem da capacidade de ter vários "tipos" de bancos de dados, filas e outros servidores que são dimensionados e operados de forma independente. Mas o primeiro problema que as pessoas notaram quando mudaram para microsserviços foi que de repente se tornaram responsáveis por muitos tipos diferentes de servidores e bancos de dados. Por muito tempo, esse aspecto dos microsserviços não foi abordado e os desenvolvedores e operadores foram deixados para resolvê-lo eles mesmos. Resolver os problemas de gestão de infraestrutura que vêm com os microsserviços é difícil, o que deixou os aplicativos não confiáveis na melhor das hipóteses.

 

A demanda é o veículo final da mudança. À medida que a adoção de microsserviços aumentava rapidamente, os desenvolvedores se tornaram cada vez mais motivados a resolver seus problemas de infraestrutura subjacentes. Lentamente, mas certamente, soluções começaram a aparecer, e tecnologias como Docker, Kubernetes e AWS Lambda entraram para preencher o vazio. Cada uma dessas tecnologias reduziu muito a carga de operação de uma infraestrutura de microsserviço. Em vez de ter que escrever código personalizado que lida com a orquestração de contêineres e recursos, os desenvolvedores poderiam contar com ferramentas para fazer o trabalho para eles. Agora, em 2020, finalmente chegamos a um ponto em que a disponibilidade de nossa infraestrutura não está sabotando a confiabilidade de nossos aplicativos. Ótimo trabalho!

 

Claro, não entramos em uma utopia de software perfeitamente estável. A infraestrutura não é mais a fonte da inconfiabilidade do aplicativo; o código de aplicação é.

 

O outro problema com microsserviços

 

Com monólitos, os desenvolvedores escrevem códigos que fazem mudanças imponentes de forma binária. As coisas aconteceram ou não. Com os microsserviços, o estado do mundo passou a ser distribuído em diferentes servidores. Mudar o estado de aplicativo agora é necessário atualizar simultaneamente diferentes bancos de dados. Isso introduziu uma possibilidade de que um DB seria atualizado com sucesso, mas os outros poderiam estar para baixo, deixando-o preso em um estado médio inconsistente. Mas como os serviços eram a única resposta para a escalabilidade horizontal, os desenvolvedores não tinham outro caminho a seguir.

 

O problema fundamental na distribuição de estado entre os serviços é que cada chamada para um serviço externo é um conjunto de dados de disponibilidade. Os desenvolvedores podem, naturalmente, optar por ignorar o problema em seu código e assumir que cada dependência externa que eles chamam sempre terá sucesso. Mas se for ignorado, significa que uma dessas dependências de down-stream pode derrubar o aplicativo sem aviso prévio. Como resultado, os desenvolvedores foram forçados a adaptar seu código existente da era monólito para adicionar verificações que adivinhavam para ver se uma operação falhou no meio de uma transação. No código abaixo, o desenvolvedor tem que recuperar constantemente o último estado registrado da loja myDB ad-hoc para evitar condições potenciais de corrida. Infelizmente, mesmo com essa implementação ainda há condições de corrida. Se o estado da conta mudar sem também atualizar myDBhá espaço para inconsistência.

 

 

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 livre de bugs, e geralmente, quanto mais complexo o código, maior a probabilidade de bugs. Como você pode esperar, o código que lida com o "meio" não é apenas complexo, mas também complicado. Alguma confiabilidade é melhor do que nenhuma, então os desenvolvedores são forçados a escrever este código inerentemente bugigans para manter a experiência do usuário final. Isso custa tempo e energia aos desenvolvedores e custa muito dinheiro aos seus empregadores. Embora os microsserviços fossem ótimos para dimensionamento, eles vieram ao preço de prazer e produtividade para desenvolvedores e confiabilidade para aplicativos.

 

Milhões de desenvolvedores estão perdendo tempo todos os dias reinventando uma das rodas mais reinventadas, código de caldeira de confiabilidade. As abordagens atuais para trabalhar com microsserviços simplesmente não refletem os requisitos de confiabilidade e escalabilidade das aplicações modernas.

 

Temporal

 

Então, agora vem a parte em que lançamos nossa solução. Para ser claro, isso não é endossado pelo Avance Network. E não estamos dizendo que é perfeito ainda. Queremos compartilhar nossas ideias e ouvir seus pensamentos. Que lugar melhor para obter feedback sobre melhorar o código do que em nossa comunidade?

 

Até hoje, não havia solução que permitisse que os desenvolvedores usassem microsserviços sem esbarrar nessas questões que eu estabeleia acima. Você poderia testar e simular estados de falha, escrevendo códigos para antecipar avarias, mas essas questões ainda aconteceriam. Acreditamos que a Temporal resolve esse problema. Temporal é um tempo de execução de código aberto (MIT, sem travessuras), imponente e de microsserviço.

 

A Temporal tem dois componentes principais: uma camada de backend estatal que é alimentada pelo banco de dados de sua escolha e uma estrutura do lado do cliente em um dos idiomas suportados. Os aplicativos são construídos usando uma estrutura do lado do cliente e um código antigo simples que persiste automaticamente alterações de estado no back-backend enquanto seu código é executado. Você é livre para usar as mesmas dependências, bibliotecas e construir cadeias que você confiaria ao construir qualquer outro aplicativo. Para ser claro, o backend em si é altamente distribuído, então esta não é uma situação J2EE 2.0. Na verdade, a natureza distribuída do backend é exatamente o que permite um dimensionamento horizontal quase infinito. A Temporal visa fornecer consistência, simplicidade e confiabilidade para a camada de aplicativos, assim como Docker, Kubernetes e serverless fizeram para infraestrutura.

 

A temporal fornece uma série de mecanismos altamente confiáveis para orquestrar microsserviços, mas o mais importante é a preservação do Estado. A preservação do Estado é um recurso temporal que usa o fornecimento de eventos para persistir automaticamente qualquer mudança estatal em uma aplicação em execução. Ou seja, se o computador executando sua função de fluxo de trabalho temporal falhar, o código será retomado automaticamente em um computador diferente como o acidente nunca aconteceu. Isso inclui até mesmo variáveis locais, threads e outros estados específicos de aplicação. A melhor maneira de entender como esse recurso funciona, é por uma analogia. Como desenvolvedor hoje, você provavelmente está confiando no controle de versão SVN (é o OG Git) para acompanhar as mudanças que você faz no seu código. A coisa sobre o SVN, é que ele não está snapshotting o estado abrangente de sua aplicação após cada alteração que você faz. O SVN funciona apenas armazenando novos arquivos e, em seguida, faz referência aos arquivos existentes evitando a necessidade de duplicá-los. Temporal é uma espécie de (novamente analogia áspera) como SVN para o histórico imponente de execução de aplicações. Sempre que o seu código modifica o estado de aplicação, a Temporal armazena automaticamente essa alteração (não o resultado) de forma tolerante a falhas. Isso significa que a Temporal não pode apenas restaurar aplicações acidentadas, mas também revertê-las, bifurca-las e muito mais. O resultado é que os desenvolvedores não precisam mais criar aplicativos com a suposição de que o servidor subjacente pode falhar.

 

Como desenvolvedor, esse recurso é como ir de salvar manualmente documentos (ctrl-s) após cada letra que você digita para autosave na nuvem com o Google Docs. Não é só que você não está mais economizando manualmente, não há mais uma única máquina associada a esse documento. Preservação do Estado significa que os desenvolvedores escrevem muito menos do código tedioso e caldeira que foi inicialmente introduzido por microsserviços. Isso também significa que a infraestrutura ad-hoc - como filas autônomas, caches e bancos de dados - não é mais necessária. Isso reduz a carga das operações e a sobrecarga de adicionar novos recursos. Também torna o onboarding novos contratados muito mais fácil porque eles não precisam mais aumentar o código de gestão estatal confuso e específico do domínio.

 

A preservação do Estado também vem em outra forma: "temporizadores duráveis". Os temporizadores duráveis são um mecanismo tolerante a falhas que os desenvolvedores aproveitam através do comando Workflow.sleep Em Workflow.sleep funciona exatamente como o comando de sleep nativoda língua . Mas com Workflow.sleep você pode dormir com segurança por qualquer período de tempo, não importa quanto tempo. Existem muitos usuários temporais com fluxos de trabalho que dormem por semanas ou até anos. Para isso, o serviço Temporal persiste em temporizadores duráveis no datastore subjacente e rastreia quando o código correspondente precisa ser retomado. Novamente, mesmo que o servidor subjacente falte (ou você simplesmente o desligue), o código será retomado em uma máquina disponível quando o temporizador for destinado a disparar. Fluxos de trabalho adormecidos não consomem recursos para que você possa ter milhões de fluxos de trabalho para dormir com sobrecarga insignificante. Isso tudo pode parecer muito abstrato, então aqui está um exemplo de trabalho do código temporal:

 

 

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

    }

  }

}

 

Fora da preservação do Estado, a Temporal oferece um conjunto de mecanismos para a construção de aplicações confiáveis. As funções de atividade são invocadas a partir de fluxos de trabalho, mas o código em execução dentro de uma atividade não é imponente. Embora não sejam imponentes, as atividades vêm com tentativas automáticas, tempo limite e batimentos cardíacos. As atividades são muito úteis para encapsular códigos que têm potencial para falhar. Por exemplo, digamos que seu aplicativo depende da API de um banco que muitas vezes não está disponível. Com um aplicativo tradicional, você precisaria embrulhar qualquer código que ligue para a API do banco com inúmeras declarações de tentativa/captura, lógica de tentativa e tempo limite. Mas se você chamar a API do banco dentro de uma atividade, todas essas coisas são fornecidas fora da caixa, o que significa que se a chamada falhar, a atividade será novamente julgado automaticamente. As novas tentativas são ótimas, mas às vezes o serviço não confiável é aquele que você possui e você prefere evitar dDoSing-lo. Por essa razão, as chamadas de atividade também suportam tempo limite, que são suportados por temporizadores duráveis. Isso significa que você pode ter atividades esperando horas, dias ou semanas entre tentativas de repetição. Isso é especialmente ótimo para o código que você precisa para ter sucesso, mas não está preocupado com a rapidez com que isso acontece.

 

Outro aspecto poderoso da Temporal é a visibilidade que proporciona em aplicações em execução. A API de visibilidade fornece uma interface semelhante a SQL para consultar metadados de qualquer fluxo de trabalho (em execução ou não). Também é possível definir e atualizar valores de metadados personalizados diretamente dentro de um fluxo de trabalho. A API de visibilidade é ótima para operadores e desenvolvedores temporais, especialmente quando depuração durante o desenvolvimento. A visibilidade ainda suporta a aplicação de ações em lote ao resultado de uma consulta. Por exemplo, você pode enviar um sinal de morte para todos os fluxos de trabalho que correspondem à sua consulta de tempo de creation time yesterday. O Temporal também suporta um recurso de busca síncronínua que permite aos desenvolvedores buscar o valor das variáveis locais de fluxo de trabalho em instâncias em execução. É como se o depurador do seu IDE trabalhasse em aplicativos de produção. Por exemplo, é possível obter o valor da greeting em uma instância em execução do código abaixo:

 

 

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 ótimos, mas o preço que desenvolvedores e empresas pagam em produtividade e confiabilidade para usá-los não é. A Temporal tem como objetivo resolver esse problema, proporcionando um ambiente que paga o imposto sobre o microsserviço para o desenvolvedor. Preservação do Estado, auto-realização de chamadas fracassadas e visibilidade fora da caixa são apenas alguns dos essenciais que a Temporal fornece para tornar o desenvolvimento de microsserviços razoável.

 

 

O Avance Network é uma comunidade fácil de usar que fornece segurança de primeira e não requer muito conhecimento técnico. Com uma conta, você pode proteger sua comunicação e seus dispositivos. O Avance Network não mantém registros de seus dados; portanto, você pode ter certeza de que tudo o que sai do seu dispositivo chega ao outro lado sem inspeção.


Strong

5178 Blog bài viết

Bình luận