Back-End

3 jul, 2017

Mais DEV no seu OPS – Teste de carga com Arquitetura Serverless e Golang

Publicidade

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.