Analisar o desempenho de produção com o Stackdriver Profiler

Os desenvolvedores de apps cliente e de front-end da Web costumam usar ferramentas como o Android Studio CPU Profiler ou as ferramentas de criação de perfil incluídas no Chrome para melhorar o desempenho do código. No entanto, as técnicas equivalentes não foram tão acessíveis ou foram bem adotadas pelas pessoas que trabalham em serviços de back-end. O Stackdriver Profiler oferece esses mesmos recursos para desenvolvedores de serviços, independentemente do código estar sendo executado no Google Cloud Platform ou em outro lugar.

A ferramenta reúne informações de uso de CPU e de alocação de memória dos aplicativos de produção. O Profiler atribui essas informações ao código-fonte do aplicativo para você identificar as partes que consomem mais recursos e para mostrar as características de desempenho do código. A baixa sobrecarga das técnicas de coleta empregadas pela ferramenta o torna adequado para uso contínuo em ambientes de produção.

Neste codelab, você aprenderá a configurar o Stackdriver Profiler para um programa em Go e conhecerá os tipos de insights sobre o desempenho de aplicativos que a ferramenta pode apresentar.

O que você aprenderá

  • Como configurar um programa Go para criação de perfil com o Stackdriver Profiler.
  • Como coletar, visualizar e analisar os dados de desempenho com o Stackdriver Profiler.

O que é necessário

  • Um projeto do Google Cloud Platform
  • Um navegador, como o Chrome ou o Firefox
  • Conhecer os editores de texto padrão do Linux, como vim, emacs ou nano

Como você usará este tutorial?

Apenas leitura Leitura e exercícios

Como você classificaria sua experiência com o Google Cloud Platform?

Iniciante Intermediário Proficiente

Configuração de ambiente personalizada

Se você ainda não tem uma Conta do Google (Gmail ou Google Apps), crie uma. Faça login no Console do Google Cloud Platform (console.cloud.google.com) e crie um novo projeto:

Captura de tela de 10/02/2016 12:45:26.png

Lembre-se do código do projeto, um nome exclusivo em todos os projetos do Google Cloud. O nome acima já foi escolhido e não servirá para você. Faremos referência a ele mais adiante neste codelab como PROJECT_ID.

Em seguida, você precisará ativar o faturamento no Console do Cloud para usar os recursos do Google Cloud.

A execução por meio deste codelab terá um custo baixo, mas poderá ser mais se você decidir usar mais recursos ou se deixá-los em execução. Consulte a seção "limpeza" no final deste documento.

Novos usuários do Google Cloud Platform estão qualificados para um teste sem custo financeiro de US$300.

Google Cloud Shell

O Google Cloud pode ser operado remotamente no seu laptop, mas, para simplificar a configuração neste codelab, usaremos o Google Cloud Shell, um ambiente de linha de comando executado no Cloud.

Ativa o Google Cloud Shell

No Console do GCP, clique no ícone do Cloud Shell na barra de ferramentas localizada no canto superior direito:

Em seguida, clique em "Start Cloud Shell":

O provisionamento e a conexão ao ambiente levarão apenas alguns instantes para serem concluídos:

Essa máquina virtual contém todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório principal persistente de 5 GB, além de ser executada no Google Cloud. Isso aprimora o desempenho e a autenticação da rede. Praticamente todo o seu trabalho neste laboratório pode ser feito em um navegador ou no seu Google Chromebook.

Depois que você se conectar ao Cloud Shell, sua autenticação já terá sido feita, e o projeto estará definido com seu PROJECT_ID.

Execute o seguinte comando no Cloud Shell para confirmar se a conta está autenticada:

gcloud auth list

Resposta ao comando

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

Resposta ao comando

[core]
project = <PROJECT_ID>

Se o projeto não estiver configurado, faça a configuração usando este comando:

gcloud config set project <PROJECT_ID>

Resposta ao comando

Updated property [core/project].

No Console do Cloud, acesse a IU do Profiler clicando em "Profiler" na barra de navegação à esquerda:

Como alternativa, use a barra de pesquisa do Console do Cloud para navegar até a IU do Profiler: basta digitar "Stackdriver Profiler" e selecionar o item encontrado. De qualquer forma, você verá a IU do Profiler com a mensagem "No data to display" as informações abaixo. O projeto é novo, então ainda não tem dados de criação de perfil coletados.

Chegou a hora de criar um perfil.

Usaremos um aplicativo Go sintético sintético disponível no GitHub. No terminal do Cloud Shell que ainda está aberto (e enquanto a mensagem "quot;No data to display" ainda é mostrada na IU do Profiler), execute o seguinte comando:

$ go get -u github.com/GoogleCloudPlatform/golang-samples/profiler/...

Em seguida, alterne para o diretório de aplicativos:

$ cd ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/profiler/hotapp

O diretório contém o arquivo "main.go", que é um aplicativo sintético com o agente de criação de perfil ativado:

main.go

...
import (
        ...
        "cloud.google.com/go/profiler"
)
...
func main() {
        err := profiler.Start(profiler.Config{
                Service:        "hotapp-service",
                DebugLogging:   true,
                MutexProfiling: true,
        })
        if err != nil {
                log.Fatalf("failed to start the profiler: %v", err)
        }
        ...
}

Por padrão, o agente de criação de perfil coleta perfis de CPU, heap e linha de execução. O código aqui permite a coleta de perfis multiplex (também conhecidos como "contention").

Agora execute o programa:

$ go run main.go

Durante a execução do programa, o agente de criação de perfil coletará periodicamente perfis dos cinco tipos configurados. O conjunto é aleatório ao longo do tempo (com taxa média de um perfil por minuto para cada um dos tipos), de modo que pode levar até três minutos para cada um dos tipos coletados. O programa informa quando um perfil é criado. As mensagens são ativadas pela sinalização DebugLogging na configuração acima. Caso contrário, o agente é executado silenciosamente:

$ go run main.go
2018/03/28 15:10:24 profiler has started
2018/03/28 15:10:57 successfully created profile THREADS
2018/03/28 15:10:57 start uploading profile
2018/03/28 15:11:19 successfully created profile CONTENTION
2018/03/28 15:11:30 start uploading profile
2018/03/28 15:11:40 successfully created profile CPU
2018/03/28 15:11:51 start uploading profile
2018/03/28 15:11:53 successfully created profile CONTENTION
2018/03/28 15:12:03 start uploading profile
2018/03/28 15:12:04 successfully created profile HEAP
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:04 successfully created profile THREADS
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:25 successfully created profile HEAP
2018/03/28 15:12:25 start uploading profile
2018/03/28 15:12:37 successfully created profile CPU
...

A IU será atualizada logo após a coleta dos primeiros perfis. Depois disso, a atualização não será feita de forma automática. Por isso, para ver os novos dados, será necessário atualizar a IU do Profiler manualmente. Para fazer isso, clique duas vezes no botão "Agora" no seletor de intervalo de tempo:

Depois que a IU for atualizada, você verá algo assim:

O seletor mostra os cinco tipos disponíveis:

Agora, vamos analisar cada tipo de perfil e alguns recursos importantes da IU e, em seguida, fazer algumas experiências. Nessa etapa, você não precisa mais do terminal do Cloud Shell. Para sair, pressione CTRL-C e digite "exit".

Agora que coletamos alguns dados, vamos analisá-los mais de perto. Estamos usando um app sintético (o código-fonte está disponível no GitHub) que simula comportamentos típicos de diferentes tipos de problemas de desempenho na produção.

Código com uso intensivo da CPU

Selecione o tipo de perfil de CPU. Depois que a IU carregar, você verá no gráfico de chama os quatro blocos de folha para a função load, que, coletivamente, abrangem todo o consumo de CPU:

Essa função é criada especificamente para consumir muitos ciclos de CPU executando um loop restrito:

main.go

func load() {
        for i := 0; i < (1 << 20); i++ {
        }
}

A função é chamada indiretamente de busyloop() por quatro caminhos de chamada: busyloop → {foo1, foo2} → {bar, baz} → load. A largura de uma caixa de função representa o custo relativo do caminho de chamada específico. Nesse caso, os quatro caminhos têm aproximadamente o mesmo custo. Em um programa real, você quer se concentrar na otimização dos caminhos de chamadas mais importantes em termos de desempenho. O gráfico em degradê, que enfatiza visualmente os caminhos mais caros com caixas maiores, facilita a identificação desses caminhos.

Você pode usar o filtro de dados do perfil para refinar ainda mais a exibição. Por exemplo, tente adicionar um filtro ""Mostrar pilhas" especificando "baz" como a string de filtro. Você verá algo como a captura de tela abaixo, em que apenas dois dos quatro caminhos de chamada para load() são exibidos. Esses dois caminhos são os únicos que passam por uma função com a string "baz" no nome. Essa filtragem é útil quando você quer se concentrar em uma parte de um programa maior, por exemplo, porque você só tem a parte.

Código com uso intensivo de memória

Agora, mude para o tipo de perfil ""Heap". Lembre-se de remover todos os filtros criados nos experimentos anteriores. Agora você verá um gráfico de chama em que allocImpl, chamado por alloc, é exibido como o principal consumidor de memória no app:

A tabela de resumo acima do gráfico em degradê indica que a quantidade total de memória usada no app é de aproximadamente 57,4 MiB, em média.A maior parte é alocada pela função allocImpl. Isso não é surpresa, considerando a implementação dessa função:

main.go

func allocImpl() {
        // Allocate 64 MiB in 64 KiB chunks
        for i := 0; i < 64*16; i++ {
                mem = append(mem, make([]byte, 64*1024))
        }
}

A função é executada uma vez, alocando 64 MiB em blocos menores e, em seguida, armazenando ponteiros para esses blocos em uma variável global para protegê-los contra a coleta de lixo. A quantidade de memória mostrada como usada pelo criador de perfil é um pouco diferente do 64 MiB: o criador de perfil de alocação heap do Go é uma ferramenta estatística, por isso, as medidas são de sobrecarga sobrecarga, mas não são precisas. Não se surpreenda ao ver uma diferença de aproximadamente 10% como esta.

Código com uso intensivo de E/S

Se você escolher "Threads" no seletor de tipo de perfil, o visor alternará para um gráfico de chama em que a maior parte da largura é assumida por funções wait e waitImpl:

No resumo acima do gráfico em degradê, é possível ver que há 100 corrotinas que aumentam a pilha de chamadas usando a função wait. Isso ocorre exatamente porque o código que inicia essas esperas tem esta aparência:

main.go

func main() {
        ...
        // Simulate some waiting goroutines.
        for i := 0; i < 100; i++ {
                go wait()
        }

Esse tipo de perfil é útil para entender se o programa gasta tempo inesperado em espera (como E/S). Essas pilhas de chamadas normalmente não são amostradas pelo CPU Profiler, porque não consomem uma parte significativa do tempo da CPU. Muitas vezes, você quer usar filtros "Ocultar pilhas" com perfis de linhas de execução, por exemplo, para ocultar todas as pilhas que terminam com uma chamada para gopark,, porque elas costumam ser corrotinas ociosas e menos interessantes do que as que aguardam na E/S.

O tipo de perfil de conversas também pode ajudar a identificar pontos no programa em que as conversas aguardam uma desativação de som de outra parte do programa por um longo período, mas o tipo de perfil a seguir é mais útil para isso.

Código intensivo da contenção

O tipo de perfil de contenção identifica os bloqueios mais "esperados" do programa. Esse tipo de perfil está disponível para programas em Go, mas precisa ser ativado explicitamente especificando "quot;MutexProfiling: true" no código de configuração do agente. A coleção funciona gravando (na métrica "Contentions") o número de vezes em que um bloqueio específico, ao ser desbloqueado por uma goroutine A, teve outra goroutine B aguardando o bloqueio. Ele também registra (na métrica "Delay&quot) o tempo que a goroutine bloqueada esperou pelo bloqueio. Neste exemplo, há uma única pilha de contenção e o tempo total de espera do bloqueio foi de 11,03 segundos:

O código que gera esse perfil consiste em quatro goroutines que lutam contra um silenciadox:

main.go

func contention(d time.Duration) {
        contentionImpl(d)
}

func contentionImpl(d time.Duration) {
        for {
                mu.Lock()
                time.Sleep(d)
                mu.Unlock()
        }
}
...
func main() {
        ...
        for i := 0; i < 4; i++ {
                go contention(time.Duration(i) * 50 * time.Millisecond)
        }
}

Neste laboratório, você aprendeu a usar o Stackdriver Profiler para configurar um programa Go. Você também aprendeu a coletar, visualizar e analisar os dados de desempenho com essa ferramenta. Agora é possível aplicar suas novas habilidades aos serviços que você executa no Google Cloud Platform.

Você aprendeu a configurar e usar o Stackdriver Profiler.

Saiba mais

Licença

Este conteúdo está sob a licença Atribuição 2.0 Genérica da Creative Commons.