Neste artigo, vou falar sobre um uso prático de Arquitetura Serverless. Amazon Web Services, Google Cloud e Microsoft Azure oferecem versões desse produto, que é a automação para rodar uma porção limitada de código sem a necessidade de dependência ou gerenciamento de sua infraestrutura. O nome adotado em inglês é Serverless Architecture, uma referência comercial à alternativa de provisionar servidores e infraestrutura para uma aplicação. A oferta mais famosa e que vamos usar neste artigo é o AWS Lambda.
É um tema interessante para estudar por alguns motivos: internamente, a automação usa linux containers e cgroups para limitar o impacto da função. Essa arquitetura requer que um processo longo seja quebrado em funções – não tem como fazer um ERP “rodar” na Arquitetura Serverless só enviando o mesmo código para lá.
A ativação das funções acontece por eventos e a escalabilidade horizontal é alta. Eventos podem ser gerados por operações distintas, como chamadas em uma API Gateway, criação de arquivos em um object storage e mensagens em uma fila.
Cada função recebe eventos e gera eventos, possibilitando o encadeamento por meio de filas de mensagens ou APIs. Cada chamada usa uma instância da função, e elas podem ser executadas em paralelo. É fácil gerar volume nesse modelo se sua função for desenhada da maneira correta.
Para usar o AWS Lambda, só é necessário conhecer duas coisas: cloud e programação. O conhecimento de cloud é o cliente de linha de comando e configuração de credenciais. Conhecimento de permissões e papéis (ARN) são úteis para integrar com outros produtos, mas não indispensáveis, pois vamos usar uma ferramenta que abstrai a configuração inicial. Em programação, é o básico: funções, utilizar bibliotecas e tipos de dados. Vou fornecer um exemplo utilizando a linguagem Go, que é bem simples.
Minha motivação de compartilhar esta experiência é que os temas em torno da cultura devops têm se tornado repetitivos: processamento de logs, automação com Docker, Openstack, Continuous Deployment. Resolvi escrever minha experiência com AWS Lambda e Go para falar um pouco mais de programação e solução de um problema que é difícil para o dono em uma equipe de tecnologia encontrar: testes de carga.
Meu objetivo era adotar ou criar uma ferramenta para teste de carga de um e-commerce como preparação para a Black Friday. É um evento esperado o ano todo, com tradição de falta de disponibilidade por problemas de arquitetura e desenvolvimento em quase todas as empresas, portanto, existia uma ansiedade de criar um modo de testar até o último minuto e, ao mesmo tempo, não fazer o “freeze” de novas features e correções. Fazer o “freeze” significa achar um ponto estável da plataforma e torcer para que esse ponto seja bom, evitando modificar até o evento.
Testamos algumas ferramentas e empresas para testes específicos de fluxo de navegação e automação. Os testes das aplicações eram úteis, mas não conseguimos escala para ver efeitos de tráfego repentino. A variação repentina de carga de acesso é um teste muito importante para código e infraestrutura.
Queríamos ver falhas em tempo de reação de autoscaling, de escolha de métricas para a medição da performance, queries de banco mal construídas, impacto de cache frio e quente, entre outros detalhes que só aparecem quando o sistema está em produção. É difícil criar um perfil de carga parecido com o de produção em volumes diferentes, mas podemos usar algumas estratégias para chegar perto.
Uma das estratégias escolhidas foi construir testes funcionais para serem executados semanalmente em fluxos de compra e fechamento de pedidos, naturalmente menores que os fluxos de navegação, e criar ferramentas que pudessem elevar o baseline de acesso da plataforma toda em até 10 vezes mais que o fluxo de um dia normal.
Assim, a plataforma já estaria ocupada quando os testes mais elaborados fossem executados e eles teriam impacto próximo da realidade. Isso nos permitiria entender como a busca de produtos, adição de produtos em carrinho, cálculo de fretes e fechamento de pedidos se comportam com a plataforma fora do uso normal, algo a que nenhum outro tipo de teste conseguia responder até então.
É importante ter sua plataforma instrumentada para coletar métricas de performance e uso e coletar alguns períodos como dias de semana, finais de semana, promoções e resultado de e-mail marketing, propaganda na televisão e push notifications para dispositivos móveis.
Eu estava estudando uma ferramenta chamada Apex, pois queria usar Golang para programar para o Lambda. Já havíamos utilizado AWS Lambda para automatizar regras de detecção de robôs, e eu estava curioso sobre como usar o AWS Lambda para fazer uma monitoração simples de disponibilidade que tinha em mente.
Faz algum tempo que programo em Golang e queria usar essa linguagem em vez de Python ou JavaScript. Escolhi Golang pois o scheduler e as goroutines usam bem os cores da máquina. Além disso, a biblioteca padrão oferece um bom suporte para o protocolo HTTP.
Havia feito alguns programas para executar no terminal e gerar tráfego na plataforma, e um deles foi usando a biblioteca Vegeta, que consegue fazer requisições em paralelo para um alvo com perfis de quantidade e intervalo variáveis.
package main import ( "fmt" "time" vegeta "github.com/tsenart/vegeta/lib" ) func main() { rate := uint64(100) // per second duration := 4 * time.Second targeter := vegeta.NewStaticTargeter(vegeta.Target{ Method: "GET", URL: "http://localhost:9100/", }) attacker := vegeta.NewAttacker() var metrics vegeta.Metrics for res := range attacker.Attack(targeter, rate, duration) { metrics.Add(res) } metrics.Close() fmt.Printf("99th percentile: %s\n", metrics.Latencies.P99) }
* Exemplo de teste de carga com o Vegeta
Pensei em usar o Lambda para rodar o pequeno gerador de carga em paralelo. Gerar carga de uma máquina ou da sua máquina local não chega ao volume necessário para saturar uma arquitetura com CDN, cache e autoscale. Se eu quisesse estimular o crescimento de infraestrutura, teria que gerar requisições suficientes para deixar a capacidade ocupada e também variar as URLs para não ter resultados servidos de cache como resposta. Essa parte depende muito da aplicação testada.
Usando o exemplo acima, criei uma função que executava um número fixo de testes em uma URL. Minha intenção era deixar o serviço ocupado com requisições normais que eu poderia gerar em um arquivo e disparar funções para eles.
Você pode utilizar a biblioteca GoQuery para fazer parsing do html e distribuir requests para links encontrados aleatoriamente. Para efeitos práticos, neste artigo vou manter o código simples para executar uma URL por vez:
package main import ( "encoding/json" "time" "github.com/apex/go-apex" "github.com/tsenart/vegeta/lib" ) type message struct { Value string `json:"value"` } type ReturnMessage struct { Value interface{} `json:"value"` } func loadtest(url string) (*vegeta.Metrics, error) { rate := uint64(100) // per second duration := 5 * time.Second targeter := vegeta.NewStaticTargeter(vegeta.Target{ Method: "GET", URL: url, }) attacker := vegeta.NewAttacker() var metrics vegeta.Metrics for res := range attacker.Attack(targeter, rate, duration) { metrics.Add(res) } metrics.Close() return &metrics, nil } func main() { apex.HandleFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) { var r ReturnMessage var m message // var j []byte var err error r = ReturnMessage{} if err = json.Unmarshal(event, &m); err != nil { return nil, err } if r.Value, err = loadtest(m.Value); err != nil { return nil, err } return r, nil }) }
* Função para teste de carga com o Vegeta no AWS Lambda
O programa é bem simples: o corpo da função principal consiste em um handler (apex.HandleFunc) que recebe uma função com assinatura func(event json.RawMessage, ctx *apex.Context) (interface{}, error). O ponto de entrada é a função main().
Toda a chamada da função de Lambda recebe um evento do tipo json.RawMessage. Em nosso exemplo, o evento atributo “value” contém a URL a ser testada. A função loadtest recebe uma URL e executa 100 requisições por segundo, durante 5 segundos. Quanto mais funções você executar, mais testes serão aplicados nessa URL.
É uma boa ideia limitar o tempo e o escopo de execução de sua função abaixo dos limites do lambda – ele pode chegar a 30 segundos usando 1.5GB de memória. Isso garante uma execução rápida e reaproveitamento de instâncias da função.
O AWS Lambda funciona desta maneira: um evento chega ao sistema, e ele executa uma cópia da função, que vive dentro de um container. Essa cópia pode ser reaproveitada sem prejuízo ao tempo da requisição. A primeira requisição enviada cria o artefato que executa a função e pode demorar um pouco mais para responder.
Criar o ambiente para uma função pelo painel não é complicado, mas não é reproduzível. Aí é que o runtime do Apex entra. Se a sua configuração para o CLI da AWS está funcionando, o Apex vai funcionar sem problemas. Instale o Apex seguindo as instruções do site, instale o ambiente de Golang de acordo com as instruções do seu sistema operacional.
$ mkdir loadtest $ cd loadtest $ apex init
Responda às questões, e pode usar o default. Talvez tenha que aumentar o timeout, dependendo de sua conexão e do tempo de resposta do servidor testado.
Coloquei o código e o roteiro de testes no repositório. É só clonar com git e não precisa dos passos acima. Só é necessário executar apex deploy.
Com a função instalada, você pode usar comandos do bash para gerar a quantidade de testes em paralelo que quiser. Por exemplo, para mandar 10000 gets em paralelo para o site www.example:
$ for a in `seq 1 10000`; do echo '{"event":{"value":"http://www.example"}}' | apex invoke loadtest & done
Pode usar xargs também ou criar uma função que coordene as outras funções. A chamada da função echo ‘{“event”:{“value”:”http://www.example”}}’ | apex invoke loadtest significa que estamos enviando um evento para a função loadtest. É isso que a API Gateway da AWS ou kinesis faz em paralelo. O custo de 5 baterias de teste com aproximadamente 100 mil requests não ultrapassou US$ 2.
Use apex logs para ver o progresso da execução e erros. Você pode coletar métricas utilizando SQS ou outra fila, e criar um mecanismo para abortar o teste – da maneira apresentada, a função é executada completamente, mas se a função ouvir um tópico ou mensagem de fila, pode verificar de tempos em tempos se existe uma mensagem de interrupção.
Originalmente, o AWS Lambda suporta Java, JavaScript e Python. Mas, por meio de um shim feito em javascript (pequeno programa que serve para carregar outro programa) e um build usando cross compile de golang, o Apex cria um pacote que permite executar o código em Go. Além disso, a biblioteca que ele fornece traduz os eventos e chama uma função padrão para eles. Você pode dividir seu código como faria normalmente, mas deve ter um arquivo main.go no diretório da função.
Como exercício, faça a configuração do Gateway API para disparar uma rodada dessa função por um POST.
Este é o tipo de projeto que vale a pena ser feito para quebrar a barreira de dev e ops, pois usa uma deficiência de entrega e conhecimento de arquitetura – a dificuldade de saber como o todo funciona quando as equipes de dev estão trabalhando em partes ou serviços – para gerar uma oportunidade de colaboração. E também sai do comum da programação em equipes de devops, que geralmente gira em torno de ferramentas de automação de configuração.