O blog da AWS
Otimizando extensões do AWS Lambda em C# e Rust
Os clientes usam extensões do AWS Lambda para integrar ferramentas de monitoramento, observabilidade, segurança e governança às suas funções do Lambda.
A AWS, junto com os parceiros do AWS Lambda, como Datadog, Dynatrace e New Relic, fornece extensões prontas para execução. Você também pode desenvolver suas próprias extensões para atender às suas necessidades específicas.
As extensões externas do Lambda são projetadas como um processo complementar executado no mesmo ambiente de execução do código da função. Isso significa que a função Lambda compartilha recursos como memória, CPU e E/S de disco com a extensão. Extensões projetadas incorretamente podem resultar na degradação do desempenho e em custos adicionais.
Esta postagem mostra como medir o impacto de uma extensão no desempenho da função usando as principais métricas de desempenho em um painel do Amazon CloudWatch.
Esta postagem se concentra nas extensões do Lambda escritas em C# e Rust. Ele mostra os benefícios de escolher escrever extensões Lambda no Rust. Além disso, explica como você pode otimizar uma extensão do Lambda escrita em C# para oferecer um desempenho três vezes melhor. A solução pode ser convertida para as linguagens de programação de sua escolha.
Visão geral
Uma função C# Lambda (executada em o.NET 6) chamada HeaderCounter é usada como um baseline. A função conta o número de cabeçalhos em uma solicitação e retorna o número na resposta. Um atraso estático (sleep) de 500 ms é inserido no código da função para simular cálculos extras. A função tem a configuração mínima de memória (128 MB), o que amplia o impacto que a extensão tem no desempenho.
Um teste de carga é realizado por meio de um comando curl que está emitindo 5.000 solicitações (com 250 solicitações em execução simultaneamente) em um endpoint público do Amazon API Gateway suportado pela função Lambda. Um painel do CloudWatch, chamado lambda-performance-dashboard, exibe métricas de desempenho da função.
Métricas capturadas pelo painel / dashboard:
- As métricas Duração máxima e Duração média permitem avaliar o impacto que a extensão tem na duração da execução da função.
- A métrica PostRuntimeExtensionsDuration mede o tempo extra que a extensão leva após a invocação da função.
- As métricas Média de Memória Usada e Memória Alocada permitem avaliar o impacto que a extensão tem no consumo de memória da função.
- As métricas Cold Start Duration e Cold Starts permitem avaliar o impacto que a extensão tem na função cold start.
Executando as extensões
Há algumas diferenças entre a forma como as extensões escritas em C# e Rust são executadas.
A extensão escrita em Rust é publicada como um executável. A vantagem de um executável é que ele é compilado em código nativo e está pronto para ser executado. A extensão é independente do ambiente, portanto, pode ser executada junto com uma função Lambda escrita em outro tempo de execução.
A desvantagem de um executável é o tamanho. As extensões são servidas como camadas Lambda, e o tamanho da extensão conta para o tamanho do pacote de implantação. O tamanho máximo do pacote de implantação descompactado para o Lambda é 250 MB.
A extensão escrita em C# é publicada como uma biblioteca de vínculo dinâmico (DLL). A DLL contém a Common Intermediate Language (CIL), que deve ser convertida em código nativo por meio de um compilador just-in-time (JIT). O tempo de execução.NET deve estar presente para que a extensão seja executada. O comando dotnet executa a DLL no exemplo fornecido com a solução.
Extensão em branco
Três instâncias da função HeaderCounter são implantadas:
- A primeira instância, disponível por meio de um endpoint sem extensão, não tem extensões.
- A segunda instância, disponível por meio de um endpoint de extensão dotnet, é instrumentada com uma extensão em branco escrita em C#. A extensão não fornece nenhuma funcionalidade extra, exceto registrar o evento recebido no CloudWatch.
- A terceira instância, disponível por meio de um endpoint de extensão rust, é instrumentada com uma extensão em branco escrita em Rust. A extensão não fornece nenhuma funcionalidade extra, exceto registrar o evento recebido no CloudWatch.
O painel mostra que as extensões adicionam uma sobrecarga mínima à função Lambda. A extensão escrita em C# adiciona mais sobrecarga às métricas de percentil mais altas, como a duração máxima do Cold Start e a Duração máxima.
Extensão EventCollector
Três instâncias da função HeaderCounter são implantadas:
1. A primeira instância, disponível por meio de um endpoint sem extensão, não tem extensões.
2. A segunda instância, disponível por meio de um endpoint de extensão dotnet, é instrumentada com uma extensão EventCollector escrita em C#. A extensão está enviando todos os eventos de invocação da extensão para o Amazon S3.
3. A terceira instância, disponível por meio de um endpoint de extensão rust, é instrumentada com uma extensão EventCollector escrita em Rust. A extensão está enviando todos os eventos de invocação da extensão para o S3.
A extensão Rust adiciona pouca sobrecarga em termos de duração, número de Cold Starts e métricas médias de PostRuntimeExtensionDuration. No entanto, há uma clara degradação do desempenho da função que é instrumentada com uma extensão escrita em C#. A duração média aumentou quase três vezes, e a duração máxima agora é cerca de seis vezes maior.
A função agora está consumindo quase toda a memória alocada. A CPU, a rede e o armazenamento para funções do Lambda são alocados com base na quantidade de memória selecionada. Atualmente, a memória está configurada para 128 MB, a configuração mais baixa possível. Recursos restritos influenciam o desempenho da função.
Aumentar a memória para 512 MB e executar novamente o teste de carga melhora o desempenho. A duração máxima agora é de 721 ms (incluindo o atraso estático de 500 ms).
Para a função C#, a duração média agora é apenas 59 ms maior do que a linha de base (baseline). A duração média do PostRuntimeExtensionDuration é de 36,9 ms (em comparação com 584 ms anteriormente). Esse ganho de desempenho se deve ao aumento da memória sem nenhuma alteração no código.
Você também pode usar o Lambda Power Tuning para determinar a configuração de memória ideal para uma função Lambda.
Coleta de lixo
Ao contrário do C#, o Rust não é uma linguagem que realiza a coleta de lixo. A coleta de lixo (GC) é um processo de gerenciamento da alocação e liberação de memória para um aplicativo. Esse processo pode consumir muitos recursos e afetar métricas de percentil mais altas. O impacto do GC é visível com as métricas da extensão em branco e da extensão EventCollector.
O Rust usa recursos de propriedade e empréstimo, permitindo a liberação segura de memória sem depender do GC. Isso torna o Rust uma boa opção de tempo de execução para ferramentas como extensões Lambda.
Extensão AOT nativa do EventCollector
A compilação nativa antecipada (Native AOT) (disponível em .NET 7 e .NET 8) permite que as extensões escritas em C# sejam entregues como executáveis, de forma semelhante às extensões escritas em Rust.
O AOT nativo não usa um compilador JIT. O aplicativo é compilado em um executável independente (todos os recursos necessários são encapsulados). O executável é executado no ambiente de destino (por exemplo, Linux x64) especificado no momento da compilação.
Estes são os resultados da compilação da extensão.NET usando o AOT nativo e da reexecução do teste de desempenho (com a memória funcional definida para 128 MB):
Para a extensão C#, a duração média agora está próxima da linha de base (em comparação com três vezes a linha de base de uma DLL). A duração média de PostRuntimeExtensionDuration agora é de 0,77 ms (em comparação com 584 ms como uma DLL). A extensão C# também supera a extensão Rust para a métrica Maximum PostRuntimeExtensionDuration — 297 ms versus 497 ms.
No geral, a extensão Rust ainda tem melhor duração média/máxima, duração média/máxima de inicialização a frio e consumo de memória. A função Lambda com a extensão C# ainda usa quase toda a memória alocada.
Outra métrica a ser considerada é o tamanho binário. A extensão Rust compila em um binário de 12,3 MB, enquanto a extensão C# compila em um binário de 36,4 MB.
Exemplos
Para seguir o exemplo passo a passo, visite o repositório do GitHub. O passo a passo explica:
- Os pré-requisitos necessários.
- Um passo a passo detalhado da implantação da solução.
- O processo de limpeza.
- Considerações de custo.
Conclusão
Esta postagem demonstra técnicas que podem ser usadas para executar e criar perfis de diferentes tipos de extensões do Lambda. Esta postagem se concentra nas extensões do Lambda escritas em C# e Rust. Esta postagem descreve os benefícios de escrever extensões Lambda em Rust e mostra as técnicas que podem ser usadas para melhorar a extensão Lambda escrita em C# para oferecer melhor desempenho.
Comece a escrever extensões do Lambda com o Rust usando as extensões Runtime para o AWS Lambda crate. Isso faz parte de um tempo de execução do Rust para o AWS Lambda.
Para obter mais recursos de aprendizado sem servidor, visite Serverless Land.