Como escolher a linguagem certa para uma Azure Function?
Computação serverless é um serviço computacional onde o desenvolvedor de software dedica seu tempo para o código da aplicação, enquanto os detalhes de implementação, como sistema operacional ou número de instâncias, são transparentes. O Azure Function é um dos serviços da Azure que realiza o processamento de código sem servidor dedicado.
Existem alguns detalhes que devem ser levados em conta quanto nosso código ou solução irá usar esse tipo de serviço.Um tópico que você deve ter em mente na solução: você sabe como escolher uma linguagem que possa diminuir custos de execução de funções sem servidor na Azure?
Tipicamente funções sem servidor cobram pelo uso computacional, medido pelo número de requisições (associado à demanda), memória disponibilizada (a depender do plano de hospedagem) e tempo de resposta. Logo, o parâmetro do custo que pode ser diferenciado a depender da linguagem é o tempo de resposta. Esse parâmetro se torna ainda mais importante quando se leva em consideração que alguns anos atrás a Amazon verificou que cada 100ms de latência os causava uma perda de 1% na receita[1].
Neste artigo, iremos comparar o tempo de resposta das linguagens Go, Java, Python e TypeScript. Esta comparação pode ser levada em consideração, junto a fatores como disponibilidade do conhecimento no mercado de trabalho, para determinar a stack certa para a aplicação de uma Azure Function.
Construção de Código
Para comparar a performance, foi desenvolvido um CRUD para uma tabela de conteúdo, que armazena o título e uma string que representa uma cor (no formato #ffffff, onde cada f é um dígito hexadecimal), se conectando a um banco de dados Postgres. Além do CRUD, um endpoint “Hello, World” também está disponível. O código utilizado para a execução dos testes está disponível no Github.
Go
A Microsoft não disponibiliza um SDK específico para desenvolvimento em Go, mas disponibiliza uma API genérica que pode ser utilizada por qualquer linguagem [2]. Cada função foi criada utilizando o Visual Studio Code, que gera uma estrutura de pastas com arquivos de configuração, que determinam, entre outras coisas, as rotas e os verbos aceitos por cada function.
A aplicação foi desenvolvida utilizando o framework Echo e o ORM GORM. Para facilitar o desenvolvimento, o Air foi utilizado para oferecer hot module reload. Para utilização de log estruturado, optamos pelo slog.
Devido ao SDK genérico, foi possível utilizar uma única function (único endpoint) mapear o CRUD completo no padrão REST. Porém não foi possível mapear path parameters, e a rota da aplicação tinha que ter uma combinação exata com a rota da function.
Seria possível também criar essa function sem a dependência do Echo ou do GORM, utilizando o pacote “net/http”, o método http.NewServeMux, e o pacote database/sql. Provavelmente, se tivessemos optado por essa combinação, os resultados seriam ligeiramente superiores.
O endpoint de Hello World precisou aceitar conexões POST pois para o custom handler, todos os endpoints deveriam aceitar esse tipo de conexão. Essa restrição também se confirmou nos SDKs Python e TypeScript.
Java
Há duas maneiras de se implementar Azure Functions no Java, com ou sem o Spring Cloud. Optamos pelo Spring Cloud pela integração com o ecossistema Spring Boot, o que inclui a ORM Spring Data JPA. Como a documentação do Azure Functions relativo à Spring Cloud está desatualizada, vimos pela documentação do Spring Cloud que é possível utilizar as bibliotecas spring-cloud-function-adapter-azure ou a spring-cloud-function-adapter-azure-web, sendo a última especializada em funções disparadas por requisições Http[3].
Optamos pelo adaptador especializado pela simplicidade de desenvolvimento, e por não encontrar muitos exemplos funcionais com o outro adaptador, especialmente com integração com Java 17 (ou 21), e o Spring Boot 3.
Python
O SDK para Azure Function no Python possui 2 versões. Desde a concepção inicial, percebe-se que o SDK brilha no quesito de fazer uma função curta, numa linguagem de fácil compreensão, e num único arquivo. Ao criar um projeto Azure Functions com Python, é criado uma estrutura que contém um requirements.txt (onde se deve registrar as dependências) e um arquivo function_app.py, onde as functions são registradas.
Apesar da simplicidade de um único arquivo ser vantajosa, o SDK não respeita a hierarquia de pacotes do Python, nem mesmo sendo possível mover o function_app.py para uma pasta separada, o que aumenta a dificuldade para a construção de sistema maiores. Para importar código de outros arquivos, a versão v2 do SDK introduz o conceito de Blueprint, que permite definir funções em vários arquivos.
Optamos por desenvolver utilizando o conceito de desenvolvimento proposto na v1, isto é, trabalhar com um único arquivo. Mesmo se a opção escolhida tivesse sido o Blueprint, não seria possível mover os arquivos python para uma pasta separada, o que tornaria a pasta raiz da function muito grande.
Para comunicação com o banco de dados, utilizamos o SqlAlchemy.
Uma dificuldade que encontramos ao trabalhar com o SDK da Azure foi a necessidade de fazer todo endpoint também aceitar uma solicitação do tipo POST.
TypeScript
O SDK para Azure Function no TypeScript foi uma das melhores experiências com SDKs da Azure que tivemos. Fazendo uma análise no package-lock.json vê-se que o SDK possivelmente utiliza o Fastify, que notadamente possui um desempenho melhor que do Express.
Recomendados pelo LinkedIn
Na aplicação, escolhemos o ORM Drizzle principalmente pelo desempenho superior ao do maior concorrente, o Prisma. Para a validação dos dados, o Zod se mostrou extremamente útil.
Assim como no Python, um dos desafios encontrados ao realizar o deploy para este ambiente é que toda função deveria também aceitar POST, senão o plugin do Visual Studio Code não reconhece a function para fazer a implantação.
Outro desafio desta implantação foi a necessidade de uso de uma connection string para acessar um Storage Account, apesar de não armazenarmos nada explicitamente pela aplicação.
Testes e Resultados
Para testar as aplicações utilizamos o Locust. Para isso, criamos um locustfile.py contendo testes para os endpoints de “Hello, World”, criação de recurso, leitura de recurso e atualização de recurso. Os parâmetros do teste foram 100 usuários de pico, mas inicialmente crescendo num passo de 5 usuários por segundo, em 1 minuto de execução.
O banco de dados foi implantado na mesma região que a aplicação para evitar que a latência de rede interfira nos resultados. Os resultados brutos se encontram nos reports de cada linguagem no repositório do Github.
Na Tabela 1, podemos verificar as principais métricas obtidas nos testes. Consideramos que para efeito de custo, a mediana pode ser a métrica mais importante, uma vez que simboliza que metade das requisições tiveram respostas abaixo do valor da mediana. Também notamos que as requisições cuja respostas se aproximam de segundos de execução representam menos de 10% do total de requisições.
Conclusão
Desenvolvemos uma aplicação em quatro linguagens diferentes e analisamos o desempenho. A análise dos resultados nos levam ao seguinte ranking de linguagens:
Go se destaca pela baixíssima latência.
TypeScript foi uma surpresa agradável, mas com alto número de erros.
Java teve um bom desempenho e nenhum erro.
Python teve o pior desempenho entre as linguagens, mas com apenas um erro durante a execução.
Apesar do desempenho ser um fator importante para o custo de uma função serverless, ele não é o único a ser considerado na hora de escolher uma linguagem. O material humano, que desenvolerá a solução pode ser igualmente importante na escolha de linguagens.
Referências