O blog da AWS

Comparando abordagens de design para criar microsserviços Serverless

Comparando abordagens de design para criar microsserviços Serverless

Este post foi escrito por Luca Mezzalira, Principal SA, e Matt Diamond, Principal, SA.

Projetar uma aplicação com o AWS Lambda pode deixar algumas perguntas para os desenvolvedores devido à modularidade que pode ser expressa no nível do código ou da infraestrutura. O uso de Serverless para executar código requer planejamento adicional para extrair a lógica de negócios dos componentes funcionais subjacentes. Essa separação deliberada de preocupações garante uma modularidade robusta, abrindo caminho para arquiteturas evolutivas.

Esta postagem se concentra nas cargas de trabalho (workloads) síncronas, mas considerações semelhantes se aplicam a outros tipos de carga de trabalho. Depois de identificar o contexto limitado de sua API e concordar com os contratos de API com os consumidores, é hora de estruturar a arquitetura de seu contexto limitado e a infraestrutura associada.

As duas formas mais comuns de estruturar uma API usando funções Lambda são responsabilidade única e Lambda-lith. No entanto, esta postagem explora uma alternativa para essas abordagens, que pode fornecer o melhor de ambas.

Funções Lambda de responsabilidade única

As funções Lambda de responsabilidade única são projetadas para executar uma tarefa específica ou lidar com uma operação específica acionada por eventos em uma arquitetura serverless:

c:\temp\design1.png

Essa abordagem fornece uma forte separação de preocupações entre a lógica de negócios e os recursos. Você pode testar isoladamente recursos específicos, implantar uma função Lambda de forma independente, reduzir a superfície de introdução de bugs e facilitar a depuração de problemas no Amazon CloudWatch.

Além disso, as funções de propósito único permitem a alocação eficiente de recursos, pois o Lambda é escalado automaticamente com base na demanda, otimizando o consumo de recursos e minimizando os custos. Isso significa que você pode modificar o tamanho da memória, a arquitetura e qualquer outra configuração disponível por função. Além disso, solicitar uma atualização da execução simultânea da função por meio de um ticket de suporte se torna mais fácil porque você não está agregando o tráfego a uma única função do Lambda que trata de cada solicitação, mas você pode solicitar um aumento específico com base no tráfego de uma única tarefa.

Outra vantagem é o tempo de execução mais rápido. Considerando a lógica de negócios de uma função Lambda de propósito único projetada para uma única tarefa, você pode otimizar o tamanho de uma função com mais facilidade, sem a necessidade de bibliotecas adicionais exigidas em outras abordagens. Isso ajuda a reduzir o tempo de cold start devido ao tamanho menor do pacote.

Apesar desses benefícios, existem alguns problemas quando se confia apenas em funções Lambda de propósito único. Embora o tempo de cold start seja atenuado, você pode experimentar um número maior de cold starts, principalmente para funções com invocações esporádicas ou pouco frequentes. Por exemplo, uma função que exclui usuários em uma tabela do Amazon DynamoDB provavelmente não será acionada com tanta frequência quanto uma que lê dados do usuário. Além disso, depender muito de funções Lambda de propósito único pode levar ao aumento da complexidade do sistema, especialmente à medida que o número de funções aumenta.

Uma boa separação de interesses ajuda a manter sua base de código, às custas da falta de coesão. Em funções com tarefas semelhantes, como operações de gravação de uma API (POST, PUT, DELETE), você pode duplicar códigos e comportamentos em várias funções. Além disso, a atualização de bibliotecas comuns compartilhadas por meio do Lambda Layers ou de outros sistemas de gerenciamento de dependências exige várias alterações em todas as funções, em vez de uma alteração atômica em um único arquivo. Isso também vale para qualquer outra alteração em várias funções, por exemplo, atualização da versão em tempo de execução.

Lambda-lith: usando uma única função Lambda

Quando muitas cargas de trabalho usam funções Lambda de propósito único, os desenvolvedores acabam com uma proliferação de funções Lambda em uma conta da AWS. Um dos principais desafios que os desenvolvedores enfrentam é atualizar dependências comuns ou configurações de funções. A menos que haja uma estratégia clara de governança implementada para resolver esse problema (como usar o Dependabot para impor a atualização de dependências ou parâmetros parametrizados que são recuperados no momento do provisionamento), os desenvolvedores podem optar por uma estratégia diferente.

Como resultado, muitas equipes de desenvolvimento se movem na direção oposta, agregando todo o código relacionado a uma API dentro da mesma função do Lambda.

Lambda-lith: Using one single Lambda function

Essa abordagem geralmente é chamada de Lambda-lith, porque reúne todos os verbos HTTP que compõem uma API e, às vezes, várias APIs na mesma função.

Isso permite que você tenha uma maior coesão de código e colocalização nas diferentes partes do aplicativo. Nesse caso, a modularidade é expressa no nível do código, em que padrões como responsabilidade única, injeção de dependência e fachada são aplicados para estruturar seu código. A disciplina e as melhores práticas de código aplicadas pelas equipes de desenvolvimento são cruciais para manter grandes bases de código.

No entanto, considerando o número reduzido de funções do Lambda, é possível atualizar uma configuração ou implementar um novo padrão em várias APIs com mais facilidade em comparação com a abordagem de responsabilidade única.

Além disso, como cada solicitação invoca a mesma função do Lambda para cada verbo HTTP, é mais provável que partes pouco usadas do seu código tenham um tempo de resposta melhor, pois é mais provável que um ambiente de execução esteja disponível para atender à solicitação.

Outro fator a ser considerado é o tamanho da função. Isso aumenta ao colocar verbos na mesma função com todas as dependências e lógica de negócios de uma API. Isso pode afetar o cold start de suas funções do Lambda com cargas de trabalho que apresentam picos. Os clientes devem avaliar os benefícios dessa abordagem, especialmente quando os aplicativos têm SLAs restritivos, que seriam afetados por cold starts. Os desenvolvedores podem mitigar esse problema prestando atenção às dependências usadas e implementando técnicas como tree-shaking, minificação e eliminação de código morto, quando a linguagem de programação permite.

Essa abordagem geral não permitirá que você ajuste suas configurações de função individualmente. Mas você deve encontrar uma configuração que corresponda a todos os recursos de código com um tamanho de memória possivelmente maior e permissões de segurança mais flexíveis que possam entrar em conflito com os requisitos definidos pela equipe de segurança.

Funções de leitura e gravação

Essas duas abordagens têm vantagens e desvantagens, mas há uma terceira opção que pode combinar seus benefícios.

Muitas vezes, o tráfego da API se inclina para mais leituras ou gravações, o que força os desenvolvedores a otimizar mais o código e as configurações de um lado em vez do outro.

Por exemplo, considere criar uma API de usuário que permita aos consumidores criar, atualizar e excluir um usuário, mas também encontrar um usuário ou uma lista de usuários. Nesse cenário, você pode alterar um usuário por vez sem operações em massa disponíveis, mas pode obter um ou mais usuários por solicitação de API. Dividir o design da API em operações de leitura e gravação resulta nessa arquitetura:

Read and write functions

A coesão do código para operações de gravação (criar, atualizar e excluir) é benéfica por vários motivos. Por exemplo, talvez seja necessário validar o corpo da solicitação, garantindo que ele contenha todos os parâmetros obrigatórios. Se a carga de trabalho for pesada em gravações, as operações menos usadas (por exemplo, Delete) se beneficiarão de ambientes de execução quentes. A colocação de código permite a reutilização do código em ações semelhantes, reduzindo a carga cognitiva para estruturar seus projetos com bibliotecas compartilhadas ou Lambda layers, por exemplo.

Ao analisar o lado das operações de leitura, você pode reduzir o código fornecido com essa função, ter uma cold start mais rápida e otimizar fortemente o desempenho em comparação com uma operação de gravação. Você também pode armazenar resultados de consultas parciais ou totais na memória de um ambiente de execução para melhorar o tempo de execução de uma função do Lambda.

Essa abordagem ajuda você ainda mais com sua natureza evolutiva. Imagine se essa plataforma se tornasse muito mais popular. Agora, você deve otimizar ainda mais a API melhorando as leituras e adicionando um padrão “cache aside” com o ElastiCache e o Redis. Além disso, você decidiu otimizar as consultas de leitura com um segundo banco de dados otimizado para a capacidade de leitura quando o cache é perdido.

No lado da escrita, você concordou com os consumidores da API que receber e confirmar a criação ou exclusão do usuário é adequado, considerando que eles adotaram totalmente a consistencia eventual característica dos sistemas distribuídos.

Agora, você pode melhorar o tempo de resposta das operações de gravação adicionando uma fila SQS antes da função Lambda. Você pode atualizar o banco de dados de gravação em lotes para reduzir o número de invocações necessárias para lidar com operações de gravação, em vez de lidar com cada solicitação individualmente.

CQRS pattern

A segregação de responsabilidade de consulta de comando (CQRS) é um padrão bem estabelecido que separa a mutação de dados, ou a parte de comando de um sistema, da parte de consulta. Você pode usar o padrão CQRS para separar atualizações e consultas se elas tiverem requisitos diferentes de taxa de transferência, latência ou consistência.

Embora não seja obrigatório começar com um padrão CQRS completo, você pode evoluir a partir da infraestrutura destacada com mais facilidade na implementação inicial de leitura e gravação, sem a refatoração massiva da sua API.

Comparação das três abordagens

Aqui está uma comparação das três abordagens:

 

Responsabilidade única Lambda-Lith Leia e escreva
Benefícios
  • Forte separação de preocupações
  • Configuração granular
  • Melhor depuração
  • Tempo de execução rápido
  • Menos invocações de cold start
  • Maior coesão de código
  • Manutenção mais simples
  • Coesão do código quando necessário
  • Arquitetura evolutiva
  • Otimização das operações de leitura e gravação
Problemas
  • Duplicação de código
  • Manutenção complexa
  • Maiores invocações de cold start
  • Configuração menos granular
  • Maior tempo de cold start
  • Usando o CQRS com dois modelos de dados
  • O CQRS adiciona consistência eventual ao seu sistema

Conclusão

Os desenvolvedores geralmente mudam de funções de responsabilidade única para o Lambda-lith à medida que suas arquiteturas evoluem, mas ambas as abordagens têm desvantagens relativas. Esta postagem mostra como é possível ter o melhor das duas abordagens dividindo suas cargas de trabalho por operações de leitura e gravação.

Todas as três abordagens são viáveis para projetar APIs serverless, e entender o que você está otimizando é a chave para tomar a melhor decisão. Lembre-se de que entender seu contexto e os requisitos de negócios a serem expressos em seus aplicativos leva você às compensações aceitáveis a serem especificadas em uma carga de trabalho específica. Mantenha a mente aberta e encontre a solução que resolva o problema e equilibre segurança, experiência do desenvolvedor, custo e capacidade de manutenção.

Para obter mais recursos de aprendizado sem servidor, visite Serverless Land.

Este blog é uma tradução do conteúdo original em inglês (link aqui).

 

Biografia do Autor

Luca Mezzalira, Principal Solutions Architech na AWS.
Matt Diamond, Principal Solutions Architech na AWS.

Biografia do Tradutor

Rodrigo Peres é Arquiteto de Soluções na AWS, com mais de 20 anos de experiência trabalhando com arquitetura de soluções, desenvolvimento de sistemas e modernização de sistemas legados.

Biografia do Revisor

Daniel Abib é Enterprise Solution Architect na AWS, com mais de 25 anos trabalhando com gerenciamento de projetos, arquiteturas de soluções escaláveis, desenvolvimento de sistemas e CI/CD, microsserviços, arquitetura Serverless & Containers e segurança. Ele trabalha apoiando clientes corporativos, ajudando-os em sua jornada para a nuvem.

https://www.linkedin.com/in/danielabib/