Teste de carga e desempenho
Testando a capacidade e desempenho de uma API com o framework Locust, em Python
Inquietação
Uma preocupação que surgiu na empresa em que trabalho, a CARGOBR, é saber se nossa nova API vai dar conta do recado. E eis que alguém pode perguntar "mas se é nova, qual a preocupação, não se garantem"? Bem, ela é nova, mas usa em grande parte o motor de cotações que está em sistemas legados, que datam do surgimento da empresa.
Mas o que é dar conta do recado? Conversando com o pessoal que está mais ligado com as métricas e resultados, consegui números sobre o comportamento de nossos clientes, que me ajudaram a definir como executar esses testes, o que é desejável, o que é aceitável.
Eu já tinha em minha cabeça que coisas simples como Apache Bench não dizem muita coisa. E também que instalar um JMeter estava além de minha necessidade de um primeiro experimento.
E confesso, são duas ferramentas que já utilizei e me ajudaram muito.
A busca e o achado
Depois de uma googlada encontrei um framework, em Python, ponto a mais para isso, que me chamou a atenção. É o Locust.
A proposta, simples e direta:
"Define user behaviour with Python code, and swarm your system with millions of simultaneous users."
Daí (olha o curitibano se entregando) pensei comigo: "É disso que o velho gosta, é isso que o velho quer!".
O MVP
Dadas as instruções contidas no CARD (viva o JIRA!), construí rapidamente um modelo, copiado da excelente documentação. Aqui apresento algo mais simples ainda, pra testar localmente:
from locust import HttpLocust, TaskSet, task
class UserBehavior(TaskSet):
@task
def get_antifascist(self):
self.client.get('/antifascist.png')
@task
def get_chupacabra(self):
self.client.get('/chupacabra.jpg')
@task
def get_democracia(self):
self.client.get('/democracia.jpg')
class WebsiteUser(HttpLocust):
task_set = UserBehavior
min_wait = 500
max_wait = 3000
Uma informação importante, é que esse framework usa a biblioteca requests, então use todo esse poder a seu favor.
Para testar esse script, subiu um servidor simples com Python:
➤ python3 -m http.server --directory ~/Imagens/
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
E rodei esse comando:
locust -f localhost --host=http://localhost:8000 --no-web --run-time 1m -c 20 -r 1
Que me deu uma longa saída, mas a importante mesmo está no final:
[2018-11-22 20:51:03,054] phrp/INFO/locust.main: Time limit reached. Stopping Locust.
[2018-11-22 20:51:03,054] phrp/INFO/locust.main: Shutting down (exit code 0), bye.
[2018-11-22 20:51:03,055] phrp/INFO/locust.main: Cleaning up runner...
[2018-11-22 20:51:03,056] phrp/INFO/locust.main: Running teardowns...
Name # reqs # fails Avg Min Max | Median req/s
--------------------------------------------------------------------------------------------------------------------------------------------
GET /antifascist.png 203 0(0.00%) 9 5 21 | 9 4.00
GET /chupacabra.jpg 187 0(0.00%) 10 5 20 | 9 3.80
GET /democracia.jpg 191 0(0.00%) 10 5 27 | 9 3.30
--------------------------------------------------------------------------------------------------------------------------------------------
Total 581 0(0.00%) 11.10
Percentage of the requests completed within given times
Name # reqs 50% 66% 75% 80% 90% 95% 98% 99% 100%
--------------------------------------------------------------------------------------------------------------------------------------------
GET /antifascist.png 203 9 10 11 11 13 15 17 18 21
GET /chupacabra.jpg 187 9 10 11 12 13 16 18 19 20
GET /democracia.jpg 191 9 10 11 12 14 16 19 21 27
--------------------------------------------------------------------------------------------------------------------------------------------
Total 581 9 10 11 12 13 15 18 19 27
Por padrão, o locust
disponibiliza uma interface gráfica na porta 8080, e para não ter isso é que passei a opção --no-web
.
As outras opções que usei foram:
-f localhost
: o arquivo a executar, nesse caso salvei meu arquivo comolocalhost.py
.--host=http://localhost:8000
: em qual host o locust vai pegar os endpoints indicados.--run-time 1m
: por quanto tempo o script vai ficar trabalhando; caso não especificado fica executando até ser interrompido(CTRL-C).-c 20
: quantas tarefas concorrentes teremos.-r 1
: define a taxa de novas tarefas por segundo, até chegar ao limite acima definido.
Algumas breves explicações
O decorator @task
define, dentro da classe, que dada função é uma tarefa a ser executada. Opcionalmente, pode-se informar um número inteiro que será o peso com que essa tarefa será executada em relação às outras. Por exemplo, se eu precisar que um endpoint seja executa mais vezes que os outros, é aí que defino esse comportamento, algo como @task(10)
.
A classe TaskSet
define um conjunto de tarefas a serem executadas, enquanto a classe HttpLocust
define o controlador de todas as tarefas.
O que realmente importa
Mas no final das contas, eu tenho um certo caminho a seguir em meus testes: autenticar, escolher algumas coisas, simular, verificar. Para isso existe a classe TaskSequence
, para usar ao invés de TaskSet
. E como decorator, deve-se usar @seq_task(1)
, com um número (nesse exemplo 1
) indicando a sequência dos endpoints para cada conjunto de tarefas.
Há ainda o problema de autenticar, e seguir com as demais requisições com um token. Mas isso é conversa pra um próximo texto!