SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Análise de desempenho e ajuste - uma poderosa ferramenta de verificação de conformidade de desempenho para os clientes.

A análise de desempenho pode ser usada para verificar gargalos em programas, aplicando uma abordagem científica na verificação de experimentos de ajuste. Este artigo define uma abordagem geral para análise de desempenho e ajuste, usando como exemplo um servidor web em Go.

Go se destaca especialmente aqui, pois possui ferramentas de profiling pprof na biblioteca padrão.

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Estratégia

Vamos criar uma lista resumida para nossa análise estrutural. Tentaremos usar alguns dados para tomada de decisões em vez de fazer alterações baseadas em intuição ou suposições. Para isso, faremos o seguinte:

  • Definimos os limites da otimização (requisitos);
  • Calculamos a carga transacional para o sistema;
  • Executamos o teste (criamos dados);
  • Observamos;
  • Analisamos - todos os requisitos estão sendo atendidos?
  • Ajustamos cientificamente, fazemos uma hipótese;
  • Executamos um experimento para verificar essa hipótese.

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Arquitetura de um servidor HTTP simples

Para este artigo, usaremos um pequeno servidor HTTP em Golang. Todo o código deste artigo pode ser encontrado aqui.

O aplicativo analisado é um servidor HTTP que consulta o Postgresql a cada solicitação. Adicionalmente, há Prometheus, node_exporter e Grafana para coletar e exibir métricas do aplicativo e do sistema.

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Para simplificar, consideramos que para a escalabilidade horizontal (e simplificação dos cálculos) cada serviço e banco de dados são implementados juntos:

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Definimos os objetivos

Nesta etapa, definimos o objetivo. O que estamos tentando analisar? Como saberemos quando é hora de parar? Neste artigo, vamos supor que temos clientes e que nosso serviço processará 10.000 solicitações por segundo.

Em Google SRE Book examina detalhadamente as maneiras de selecionar e modelar. Faremos o mesmo, construindo modelos:

  • Latência: 99% das solicitações devem ser concluídas em menos de 60ms;
  • Custo: o serviço deve consumir o mínimo possível de dinheiro, que consideramos razoável. Para isso, maximizamos a taxa de transferência;
  • Planejamento de capacidade: é necessário compreender e documentar quantas instâncias do aplicativo serão necessárias para executar, incluindo a funcionalidade geral de escalabilidade, bem como quantas instâncias serão necessárias para atender aos requisitos iniciais de carga e garantir redundância n+1.

A latência pode exigir otimização além da análise, mas a largura de banda precisa ser claramente analisada. Ao usar o processo SRE, o requisito de latência vem do cliente ou do negócio, representado pelo proprietário do produto. E nosso serviço cumprirá esse compromisso desde o início, sem qualquer configuração!

Configurar ambiente de teste

Com o ambiente de teste, poderemos impor uma carga controlada ao nosso sistema. Dados de desempenho do serviço da web serão gerados para análise.

Carga transacional

Este ambiente utiliza Vegeta para criar uma frequência de solicitações HTTP personalizada, até que seja interrompido:

$ make load-test LOAD_TEST_RATE=50
echo "POST http://localhost:8080" | vegeta attack -body tests/fixtures/age_no_match.json -rate=50 -duration=0 | tee results.bin | vegeta report

Monitoramento

Durante a execução, será aplicada a carga transacional. Além das métricas do aplicativo (número de solicitações, latências de resposta) e do sistema operacional (memória, CPU, IOPS), o perfilamento do aplicativo será iniciado para entender onde estão os problemas e como o tempo de CPU está sendo consumido.

Perfilamento

O perfilamento é um tipo de medição que permite ver para onde vai o tempo de CPU durante a execução do aplicativo. Ele permite determinar onde exatamente e quanto tempo de CPU está sendo gasto:

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Esses dados podem ser usados durante a análise para obter uma visão sobre o tempo de CPU desperdiçado e trabalhos desnecessários realizados. Go (pprof) pode gerar perfis e visualizá-los em forma de gráfico de chamas, usando um conjunto padrão de ferramentas. Falarei sobre como utilizá-los e a orientação para configuração um pouco mais abaixo no artigo.

Execução, monitoramento, análise.

Vamos realizar um experimento. Vamos executar, observar e analisar até que o desempenho nos satisfaça. Escolheremos um valor de carga baixo aleatoriamente para utilizá-lo na obtenção dos resultados das primeiras observações. Em cada etapa subsequente, aumentaremos a carga com algum coeficiente de escalonamento escolhido com uma certa variação. Cada execução do teste de carga é realizada com ajuste no número de solicitações: make load-test LOAD_TEST_RATE=X.

50 solicitações por segundo

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Observe os dois gráficos superiores. O superior à esquerda mostra que nosso aplicativo processa 50 solicitações por segundo (em sua opinião), enquanto o superior à direita mostra a duração de cada solicitação. Ambos os parâmetros nos ajudam a observar e analisar: estamos dentro dos nossos limites de desempenho ou não. A linha vermelha no gráfico Latência da Solicitação HTTP mostra o SLO em 60ms. Pela linha, é evidente que estamos bem abaixo do nosso tempo máximo de resposta.

Vamos considerar do ponto de vista de custo:

10000 solicitações por segundo / 50 solicitações por servidor = 200 servidores + 1

Ainda podemos melhorar essa métrica.

500 solicitações por segundo

Coisas mais interessantes começam a acontecer quando a carga atinge 500 solicitações por segundo:

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Novamente, no gráfico superior à esquerda é visível que o aplicativo registra uma carga normal. Se não for assim, há um problema no servidor onde o aplicativo está em execução. O gráfico de latência de resposta, localizado no canto superior direito, mostra que 500 solicitações por segundo resultaram em uma latência de resposta de 25-40ms. O percentil 99 ainda se encaixa confortavelmente no SLO de 60ms definido acima.

Do ponto de vista de custo:

10000 solicitações por segundo / 500 solicitações por servidor = 20 servidores + 1

Ainda dá para melhorar.

1000 solicitações por segundo

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Ótimo lançamento! O aplicativo mostra que processou 1000 solicitações por segundo, no entanto, a margem de latência foi ultrapassada em relação ao SLO. Isso é visível pela linha p99 no gráfico superior direito. Embora a linha p100 esteja muito acima, as latências reais estão acima do máximo de 60ms. Vamos mergulhar na profilação para descobrir o que o aplicativo está realmente fazendo.

Perfilamento

Para a profilação, configuramos uma carga de 1000 solicitações por segundo e então usamos pprof para capturar dados e descobrir onde o aplicativo está gastando o tempo do processador. Isso pode ser feito ativando o endpoint HTTP pprof, após o que, sob carga, salve os resultados usando curl:

$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.prof

Os resultados podem ser exibidos assim:

$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

O gráfico mostra onde e quanto tempo de CPU o aplicativo está gastando. Da descrição de Brendan Gregg:

No eixo X está o preenchimento do perfil da pilha, classificado em ordem alfabética (isto não é tempo); o eixo Y mostra a profundidade da pilha, contando a partir de zero em [top]. Cada retângulo é um quadro da pilha. Quanto mais largo o quadro, mais frequentemente ele aparece nas pilhas. O que está no topo trabalha na CPU, e abaixo estão os elementos filhos. As cores, geralmente, não têm significado e são escolhidas aleatoriamente para diferenciar os quadros.

Análise — hipótese

Para a configuração, vamos nos concentrar em tentar encontrar desperdícios desnecessários de tempo de CPU. Vamos procurar as maiores fontes de desperdiço e removê-las. Bem, considerando que a profilagem revela com precisão onde o aplicativo gasta seu tempo de CPU, pode ser necessário fazer isso várias vezes, e também será necessário modificar o código-fonte do aplicativo, reiniciar os testes e observar como o desempenho se aproxima do esperado.

Seguindo as recomendações de Brendan Gregg, leremos o gráfico de cima para baixo. Cada linha exibe um quadro da pilha (chamada de função). A primeira linha é o ponto de entrada no programa, o pai de todas as outras chamadas (em outras palavras, todas as outras chamadas terão isso em sua pilha). A próxima linha já é diferente:

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Se você passar o mouse sobre o nome da função no gráfico, será exibido o tempo total que ela esteve na pilha durante a depuração. A função HTTPServe esteve lá 65% do tempo, outras funções de runtime, runtime.mcall, mstart e gc, ocuparam o restante do tempo. Um fato interessante: 5% do tempo total foi gasto em consultas DNS:

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Os endereços que o programa procura pertencem ao Postgresql. Clicamos em FindByAge:

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Curiosamente, o programa mostra que, em princípio, existem três principais fontes que adicionam latências: abertura e fechamento de conexões, consulta de dados e conexão com o banco de dados. No gráfico, é visível que as consultas DNS, abertura e fechamento de conexões consomem cerca de 13% do tempo total de execução.

Hipótese: A reutilização de conexões através de um pool deve reduzir o tempo de uma solicitação HTTP, permitindo maior largura de banda e menores latências..

Configuração do aplicativo — experimento.

Estamos atualizando o código-fonte, tentando remover a conexão com o Postgresql a cada solicitação. A primeira opção é o uso de pool de conexões no nível do aplicativo. Neste experimento, nós configuraremos o pool de conexões usando o driver sql para Go:

db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)

if err != nil {
   return nil, err
}

Execução, monitoramento, análise.

Após reiniciar o teste com 1000 solicitações por segundo, é visível que o p99 de latências voltou ao normal com SLO de 60ms!

E quanto custa?

10000 solicitações por segundo / 1000 solicitações por servidor = 10 servidores + 1.

Vamos fazer ainda melhor!

2000 solicitações por segundo.

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

A duplicação da carga mostra o mesmo, o gráfico superior esquerdo demonstra que o aplicativo consegue processar 2000 solicitações por segundo, p100 abaixo de 60ms, p99 atende ao SLO.

Do ponto de vista de custo:

10000 solicitações por segundo / 2000 solicitações por servidor = 5 servidores + 1.

3000 solicitações por segundo.

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Aqui o aplicativo pode processar 3000 solicitações com latência p99 menor que 60ms. O SLO não é violado, e o custo é considerado da seguinte forma:

10000 solicitações por segundo / 3000 solicitações por servidor = 4 servidores + 1. (o autor arredondou para cima, Nota do tradutor)

Vamos tentar mais uma rodada de análise.

Análise — hipótese

Coletamos e exibimos os resultados de depuração do aplicativo a 3000 solicitações por segundo:

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Ainda assim, 6% do tempo está sendo gasto na criação de conexões. A configuração do pool melhorou o desempenho, mas ainda se nota que o aplicativo continua a criar novas conexões com o banco de dados.

Hipótese: As conexões, apesar da presença do pool, ainda estão sendo descartadas e limpas, então o aplicativo precisa reconectar. Definir o número de conexões pendentes igual ao tamanho do pool deve ajudar com a latência ao minimizar o tempo que o aplicativo gasta criando uma conexão..

Configuração do aplicativo — experimento.

Vamos tentar definir MaxIdleConns igual ao tamanho do pool (também descrito aqui):

db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
   return nil, err
}

Execução, monitoramento, análise.

3000 solicitações por segundo.

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

p99 abaixo de 60ms com p100 significativamente menor!

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

A verificação do flame graph mostra que a criação de conexão já não é perceptível! Verificamos mais detalhadamente. pg(*conn).query — também não notamos a criação de conexão aqui.

SRE: Análise de performance. Método de configuração usando um simples servidor web em Go

Conclusão

A análise de desempenho é crítica para entender se as expectativas do cliente e os requisitos não funcionais foram atendidos. A análise, comparando observações com as expectativas dos clientes, pode ajudar a identificar o que é aceitável e o que não é. Go oferece ferramentas eficazes, incorporadas na biblioteca padrão, que tornam a análise simples e acessível.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster