A anatomia de uma função lambda
Quando entrei no Zé, fui apresentado a uma maneira de construir aplicações totalmente diferente da maneira que eu construía até então, utilizando serverless.
Serverless é uma maneira de executar aplicações sem a necessidade de gerenciar os servidores. Isso não quer dizer que nesse modelo os servidores deixam de ser utilizados, eles são! Mas, o gerenciamento deles é feito pelo provedor de nuvem, assim, os desenvolvedores não precisam se preocupar com eles.
Alguns provedores oferecem este modelo através de funções como serviço, do inglês Function-as-a-Service(FAAS). Nesse modelo, fazemos o upload do código que queremos executar e configuramos como e quando ele deve ser executado, seja através de requisições ou de eventos, e o provedor aloca os recursos necessários de acordo com o que foi especificado.
Dentro da AWS, esse serviço é chamado de AWS lambda, e pagamos de acordo com o número de vezes que executamos, a quantidade de tempo que essas execuções levam e a quantidade de memória que especificamos para ser utilizada. Por isso, é importante entender como funciona o processo de execução dessas funções lambda, para sempre ter em mente como podemos usar esse serviço da melhor maneira possível.
Quando a execução irá acontecer?
Existem duas formas de provocar a execução de uma função lambda: síncrona e assincronamente. Quando provocamos a execução de forma síncrona, esperamos o resultado do código que será executado, por exemplo, em uma requisição que o cliente faz para uma API(Interface de Programação de Aplicação).
Quando provocamos a execução de forma assíncrona, configuramos gatilhos que vão provocar a execução em um determinado horário ou quando algum tipo de evento acontecer. O resultado pode ser variado: uma atualização no banco de dados, o envio de uma notificação para o usuário ou a geração de um novo evento.
Qual a estrutura de uma função lambda?
Acima temos um exemplo de função lambda, onde podemos ressaltar três características:
- Muitas vezes, para executar o código precisamos importar algumas bibliotecas e essas importações são feitas no começo do arquivo, da linha 1 até a 5;
- Além disso, algumas dependências precisam ser configuradas antes de utilizarmos, da linha 7 até a 9;
- Para executarmos uma função lambda precisamos fornecer um ponto de entrada. Dentro do código, esse ponto de entrada será uma função, comumente chamada de handler, onde estará concentrada a lógica da tarefa que queremos executar. Neste exemplo estamos copiando uma imagem de um lugar para outro, da linha 11 até a 22.
O ciclo de vida
Apesar dos provedores abstrairem o gerenciamento dos servidores, sabemos que eles são utilizados e que existe um ciclo de vida na hora de executar as funções lambda. A imagem abaixo mostra uma visão ampla desse ciclo.
Baixar código
Uma vez provocada a execução de uma função lambda, o primeiro passo é baixar o código que será executado. Na AWS, temos o AWS Simple Storage Service(Amazon S3), sendo este o serviço que nos permite armazenar arquivos e o lugar onde a Amazon vai buscar o código para ser executado.
Criação de um novo ambiente de execução
Em seguida, um ambiente de execução isolado é criado de acordo com as configurações que fornecemos, como a linguagem utilizada para escrever o código, a quantidade máxima de memória que será utilizada e o tempo máximo que queremos deixar o código executando.
Inicialização
A seguir, temos uma fase de inicialização (Init phase) que executará três tarefas.
A primeira delas é a inicialização das extensões. As extensões são integrações que podemos adicionar às nossas execuções para que sejam executadas lado a lado com o código. Um exemplo é o Amazon CloudWatch Lambda Insights. Com ele, podemos monitorar tudo que acontece durante a execução do código, fornecendo a coleta e, posteriormente, a visualização das métricas.
A segunda é a inicialização do ambiente de execução, ou seja, um ambiente de execução isolado é criado de acordo com as configurações que fornecemos como a linguagem de programação, a quantidade máxima de memória e o tempo máximo que queremos deixar o código executando.
A terceira é a inicialização da função lambda. Nessa etapa, é executado todo o código que está presente no arquivo, mas não está dentro da função. Geralmente, esse código é composto por importações de bibliotecas e dependências que serão utilizadas pela função.
Essa fase tem um tempo limite de 10 segundos para acontecer, se as inicializações não acontecerem nesse tempo, a fase será reiniciada e terá mais 10 segundos para acontecer.
Execução
Com tudo pronto, começa a fase de execução(Invoke phase), onde a função e as extensões terão um tempo limite, que foi configurado, para serem executados. Por exemplo, se esse tempo for configurado em 30 segundos, todos terão que completar suas execuções dentro desse tempo.
Encerramento
Em ambos os casos, a função executada com sucesso dentro do tempo ou caso a execução não tenha conseguido finalizar dentro do limite de tempo, a fase de encerramento(Shutdown phase) será iniciada.
Essa fase é limitada em 2 segundos, e nela a execução do código e das extensões serão finalizadas caso já não tenham sido. Em seguida, o ambiente de execução será mantido por algum tempo para ser aproveitado para uma nova execução e, caso isso não ocorra, os recursos utilizados serão liberados.
Cold start
Como os recursos utilizados para executar o código são alocados conforme a demanda, ou seja, só são alocados recursos como memória, cpu e ip quando provocamos a execução de uma função lambda, os provedores precisam executar um conjunto de etapas para de fato executar o código, como descrito acima. Esse conjnto é chamado de cold start e, ele sempre acontece quando nenhum ambiente de execução, em estado de encerramento, está disponível para ser aproveitado.
Caso aconteça um pico de acessos ao site ou a uma API que usa funções lambda, não existirá ambientes de execução para serem aproveitados causando assim o cold start na grande maioria desses acessos. Além disso, se a função lambda está sendo executada dentro de um ambiente isolado dentro do provedor(Virtual Private Cloud), será necessário criar uma interface de rede elástica, aumentando a complexidade, pois estaremos adicionando mais um passo antes de executar o código.
Por isso, é muito importante levar esse conceito em consideração no momento em que a arquitetura de um sistema está sendo discutida, juntamente com os requisitos desse sistema.
Otimização da aws X Otimização do desenvolvedor
O serviço AWS lambda tem uma estrutura complexa nos bastidores para gerenciar os servidores e fazer o download do código, quanto a isso os programadores não podem fazer nada. Mas, quanto a fase de inicialização e execução do código, medidas podem ser tomadas para otimizá-las, tais como:
- Diminuir ao máximo o tamanho do pacote com todo o código, evitando sempre funções monolíticas, diminuirá o tempo para baixar o pacote;
- Otimizar importações de bibliotecas e dependências e adiar ao máximo a inicialização de bibliotecas para diminuir a etapa de inicialização da função;
- Minificar o código que vai para o ambiente de produção para ter um pacote ainda menor, removendo espaços, comentários, agrupando todo o código em apenas um arquivo e otimizando alguns padrões de programação, diminuindo assim o tempo ao baixá-lo.
Além disso, a única característica que podemos configurar, quando o assunto é poder de processamento, é a quantidade de memória. Nesse sentido, podemos utilizar ferramentas como AWS Lambda Power Tuning para conseguirmos visualizar qual a quantidade de memória ideal para a função lambda.
Conclusão
O Serverless surgiu para ser mais uma ferramenta no cinto de utilidades dos desenvolvedores. Em situações com processamento de arquivos, análise de dados e aplicações web podemos utilizá-la de maneira muito eficiente mas, como qualquer ferramenta, é preciso análisar a situação em que ela pode ser aplicada, levantando os prós e contras.