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

A análise e o ajuste de desempenho são uma ferramenta poderosa para verificar a conformidade do desempenho dos clientes.

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

Go é especialmente bom aqui porque possui ferramentas de criação de perfil pprof na biblioteca padrão.

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

estratégia

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

  • Determinamos os limites de otimização (requisitos);
  • Calculamos a carga de transação do sistema;
  • Realizamos o teste (criamos dados);
  • Nós observamos;
  • Analisamos - todos os requisitos foram atendidos?
  • Nós estabelecemos isso cientificamente, fazemos uma hipótese;
  • Realizamos um experimento para testar essa hipótese.

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

Arquitetura simples de servidor HTTP

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

A aplicação que está sendo analisada é um servidor HTTP que pesquisa o Postgresql para cada solicitação. Além disso, há Prometheus, node_exporter e Grafana para coletar e exibir métricas de aplicativos e sistemas.

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

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

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

Definindo metas

Nesta etapa, decidimos o objetivo. O que estamos tentando analisar? Como sabemos quando é hora de terminar? Neste artigo imaginaremos que temos clientes e que nosso serviço processará 10 solicitações por segundo.

В Livro SRE do Google Métodos de seleção e modelagem são discutidos em detalhes. Vamos fazer o mesmo e construir modelos:

  • Latência: 99% das solicitações devem ser concluídas em menos de 60ms;
  • Custo: O serviço deve consumir a quantia mínima de dinheiro que consideramos razoavelmente possível. Para fazer isso, maximizamos o rendimento;
  • Planejamento de capacidade: requer compreensão e documentação de quantas instâncias do aplicativo precisarão ser executadas, incluindo a funcionalidade geral de escalabilidade, e quantas instâncias serão necessárias para atender aos requisitos iniciais de carga e provisionamento redundância n+1.

A latência pode exigir otimização além da análise, mas o rendimento claramente precisa ser analisado. Ao utilizar o processo SRE SLO, a solicitação de atraso vem do cliente ou empresa, representada pelo proprietário do produto. E o nosso serviço cumprirá esta obrigação desde o início, sem quaisquer configurações!

Configurando um ambiente de teste

Com a ajuda de um ambiente de teste, poderemos colocar uma carga medida em nosso sistema. Para análise, serão gerados dados sobre o desempenho do web service.

Carga de transação

Este ambiente utiliza Vegetar para criar uma taxa de solicitação HTTP personalizada até ser interrompida:

$ 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

Observação

A carga transacional será aplicada em tempo de execução. Além das métricas da aplicação (número de solicitações, latência de resposta) e do sistema operacional (memória, CPU, IOPS), será executado o perfil da aplicação para entender onde há problemas e como o tempo de CPU está sendo consumido.

Perfil

A criação de perfil é um tipo de medida que permite ver para onde está indo o tempo de CPU quando um aplicativo está em execução. Ele permite determinar exatamente onde e quanto tempo do processador é gasto:

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

Esses dados podem ser usados ​​durante a análise para obter informações sobre o tempo de CPU desperdiçado e o trabalho desnecessário sendo executado. Go (pprof) pode gerar perfis e visualizá-los como gráficos em degradê usando um conjunto padrão de ferramentas. Falarei sobre seu uso e guia de configuração posteriormente neste artigo.

Execução, observação, análise.

Vamos fazer uma experiência. Iremos executar, observar e analisar até estarmos satisfeitos com o desempenho. Escolhamos um valor de carga arbitrariamente baixo para aplicá-lo na obtenção dos resultados das primeiras observações. A cada passo subsequente aumentaremos a carga com um determinado fator de escala, escolhido com alguma variação. Cada execução de teste de carga é realizada com o número de solicitações ajustado: make load-test LOAD_TEST_RATE=X.

50 solicitações por segundo

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

Preste atenção aos dois gráficos superiores. O canto superior esquerdo mostra que nosso aplicativo processa 50 solicitações por segundo (ele pensa) e o canto superior direito mostra a duração de cada solicitação. Ambos os parâmetros nos ajudam a observar e analisar se estamos ou não dentro dos nossos limites de desempenho. Linha vermelha no gráfico Latência de solicitação HTTP mostra SLO em 60ms. A linha mostra que estamos bem abaixo do nosso tempo máximo de resposta.

Vejamos o lado dos custos:

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

Ainda podemos melhorar esse número.

500 solicitações por segundo

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

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

Novamente, no gráfico superior esquerdo você pode ver que o aplicativo está registrando carga normal. Se não for esse o caso, há um problema no servidor em que o aplicativo está sendo executado. O gráfico de latência de resposta está localizado no canto superior direito, mostrando que 500 solicitações por segundo resultaram em um atraso de resposta de 25 a 40 ms. O percentil 99 ainda se encaixa perfeitamente no SLO de 60 ms escolhido acima.

Em termos de custo:

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

Tudo ainda pode ser melhorado.

1000 solicitações por segundo

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

Ótimo lançamento! O aplicativo mostra que processou 1000 solicitações por segundo, mas o limite de latência foi violado pelo SLO. Isso pode ser visto na linha p99 no gráfico superior direito. Apesar da linha p100 ser muito maior, os atrasos reais são superiores ao máximo de 60ms. Vamos mergulhar na criação de perfil para descobrir o que o aplicativo realmente faz.

Perfil

Para criação de perfil, definimos a carga para 1000 solicitações por segundo e, em seguida, usamos pprof para capturar dados para descobrir onde o aplicativo está gastando tempo de CPU. Isso pode ser feito ativando o endpoint HTTP pprofe, em seguida, 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 Desempenho. Método de configuração usando um servidor web simples em Go

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

O eixo X é a população do perfil da pilha, classificada em ordem alfabética (isto não é o tempo), o eixo Y mostra a profundidade da pilha, contando de zero em [topo]. Cada retângulo é um quadro de pilha. Quanto mais larga a moldura, mais frequentemente ela está presente nas pilhas. O que está em cima é executado na CPU e o que está abaixo são os elementos filhos. As cores geralmente não significam nada, mas são simplesmente escolhidas aleatoriamente para diferenciar as molduras.

Análise - hipótese

Para ajuste, vamos nos concentrar em tentar encontrar tempo desperdiçado de CPU. Procuraremos as maiores fontes de gastos inúteis e as eliminaremos. Bem, dado que a criação de perfil revela com muita precisão onde exatamente o aplicativo está gastando seu tempo de processador, pode ser necessário fazer isso várias vezes e também alterar o código-fonte do aplicativo, executar novamente os testes e verificar se o desempenho se aproxima do alvo.

Seguindo as recomendações de Brendan Gregg, leremos o gráfico de cima para baixo. Cada linha exibe um quadro de 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 a terão em sua pilha). A próxima linha já é diferente:

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

Se você passar o cursor sobre o nome de uma função no gráfico, será exibido o tempo total que ela esteve na pilha durante a depuração. A função HTTPServe estava presente 65% do tempo, outras funções de tempo de execução runtime.mcall, mstart и gc, ocupou o resto do tempo. Curiosidade: 5% do tempo total é gasto em consultas DNS:

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

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

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

Curiosamente, o programa mostra que, em princípio, existem três fontes principais que acrescentam atrasos: abertura e encerramento de conexões, solicitação de dados e conexão ao banco de dados. O gráfico mostra que as solicitações de DNS, abertura e fechamento de conexões ocupam cerca de 13% do tempo total de execução.

Hipótese: A reutilização de conexões usando pooling deve reduzir o tempo de uma única solicitação HTTP, permitindo maior rendimento e menor latência.

Configurando o aplicativo - experimente

Atualizamos o código fonte, tentamos remover a conexão com o Postgresql para cada solicitação. A primeira opção é usar conjunto de conexões no nível do aplicativo. Neste experimento nós vamos configurar pool de conexões usando driver sql para go:

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

if err != nil {
   return nil, err
}

Execução, observação, análise

Após reiniciar o teste com 1000 solicitações por segundo, fica claro que os níveis de latência do p99 voltaram ao normal com um SLO de 60ms!

Qual é o custo?

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

Vamos fazer ainda melhor!

2000 solicitações por segundo

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

Dobrar a carga mostra a mesma coisa, o gráfico superior esquerdo mostra que a aplicação consegue processar 2000 solicitações por segundo, p100 é inferior a 60ms, p99 satisfaz o SLO.

Em termos de custo:

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

3000 solicitações por segundo

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

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

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

Vamos tentar outra rodada de análise.

Análise - hipótese

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

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

Ainda 6% do tempo é gasto no estabelecimento de conexões. A configuração do pool melhorou o desempenho, mas você ainda pode ver que o aplicativo continua trabalhando na criação de novas conexões com o banco de dados.

Hipótese: As conexões, apesar da presença de um pool, ainda estão sendo descartadas e limpas, portanto o aplicativo precisa redefini-las. Definir o número de conexões pendentes para o tamanho do pool deve ajudar na latência, minimizando o tempo que o aplicativo gasta criando uma conexão.

Configurando o aplicativo - experimente

Tentando instalar MaxIdleConns igual ao tamanho da piscina (também descrito aqui):

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

Execução, observação, análise

3000 solicitações por segundo

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

p99 é inferior a 60ms com significativamente menos p100!

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

A verificação do gráfico em chama mostra que a conexão não é mais perceptível! Vamos verificar com mais detalhes pg(*conn).query – também não percebemos a conexão sendo estabelecida aqui.

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

Conclusão

A análise de desempenho é fundamental para compreender se as expectativas do cliente e os requisitos não funcionais estão sendo atendidos. A análise comparando as observações com as expectativas do cliente pode ajudar a determinar o que é aceitável e o que não é. Go fornece ferramentas poderosas integradas à biblioteca padrão que tornam a análise simples e acessível.

Fonte: habr.com

Adicionar um comentário