10 anos atrás, o Google estava enfrentando um gargalo crítico causado por tempos de compilação C++ extremamente prolongados e precisava de uma forma totalmente nova para resolvê-lo. Os engenheiros do Google enfrentaram o desafio criando uma nova linguagem chamada Go (aka Golang). A nova linguagem de Go empresta as melhores partes do C++, (mais notadamente sua performance e recursos de segurança) e combina com a velocidade do Python para permitir que Go use rapidamente vários núcleos enquanto é capaz de implementar concorrência.

Aqui, na Coralogix, estamos analisando os logs de nossos clientes a fim de dar-lhes insights em tempo real, alertas e meta-dados em seus logs. Para fazer isso, a fase de análise, que é muito complexa e carregada com toneladas de regras para cada serviço de linha de log, deve ser extremamente rápida. Essa é uma das razões porque decidimos usar o Go lang.

O novo serviço agora está rodando em tempo integral em produção e, embora estejamos vendo ótimos resultados, ele precisa rodar em máquinas de alto desempenho. Mais de dezenas de bilhões de logs são analisados diariamente por este serviço Go que roda em uma instância AWS m4.2xlarge com 8 CPUs e 36 GB de Memória.

Neste estágio, poderíamos ter chamado de um dia ótimo sentindo que tudo estava funcionando bem, mas não é assim que rolamos aqui na Coralogix. Queríamos mais funcionalidades (performance, etc) usando menos (instâncias AWS). Para melhorar, primeiro precisamos entender a natureza dos nossos gargalos e como podemos reduzi-los ou eliminá-los completamente.

Decidimos rodar alguns perfis de Golang no nosso serviço e verificar o que exatamente causou o alto consumo de CPU para ver se podemos otimizar.

Primeiro, atualizamos para a última versão estável do Go (uma parte chave do ciclo de vida do software). Estávamos na versão Go v1.12.4, e a mais recente foi a 1.13.8. A versão 1.13, de acordo com a documentação, teve grandes melhorias na biblioteca de tempo de execução e alguns outros componentes que utilizavam principalmente memória. Resumindo, trabalhar com a última versão estável foi útil e nos salvou bastante trabalho →

Assim, o consumo de memória melhorou de cerca de ~800MB para ~180MB.

Segundo, para entender melhor nosso processo e entender onde estamos gastando tempo e recursos, começamos a fazer o perfil.

Perfilar diferentes serviços e linguagens de programação pode parecer complexo e intimidador, mas na verdade é muito fácil em Go e pode ser descrito em poucos comandos. Go tem uma ferramenta dedicada chamada ‘pprof’ que deve ser habilitada em sua aplicação ouvindo uma rota (porta padrão – 6060) e usar o pacote Go para gerenciar conexões http:

import _ "net/http/pprof"

Então inicialize o seguinte em sua função principal ou sob seu pacote de rota:

go func() {log.Println(http.ListenAndServe("localhost:6060", nil))}()

Agora você pode iniciar seu serviço e conectar-se a

http://localhost:6060/debug/pprof

Documentação completa por Go pode ser encontrada aqui.

O perfil padrão para o pprof será uma amostra de 30 segundos de uso da CPU. Existem alguns caminhos diferentes que permitem a amostragem para o uso da CPU, uso da pilha e muito mais.

Concentrámo-nos no uso da nossa CPU, por isso fizemos um perfil de 30 segundos em produção e descobrimos o que se vê na imagem abaixo (lembrete: isto é depois de actualizar a nossa versão Go e diminuir as partes internas de Go para o mínimo):

Go profiling – Coralogix

Como você pode ver, encontramos muita atividade de pacote de tempo de execução, o que indicava especificamente a atividade de GC → Quase 29% da nossa CPU (apenas os 20 objetos mais consumidos) é usada por GC. Como Go GC é bastante rápido e bastante otimizado, a melhor prática é não alterá-lo ou modificá-lo e como nosso consumo de memória era muito baixo (comparado com nossa versão anterior Go) o principal suspeito era uma alta taxa de alocação de objetos.

Se esse for o caso, há duas coisas que podemos fazer:

  • Tune Go GC activity para se adaptar ao nosso comportamento de serviço, ou seja – retardar seu acionamento para ativar o GC com menos freqüência. Isto nos forçará a compensar com mais memória.
  • Localizar a função, área ou linha em nosso código que aloca muitos objetos.

Locando em nosso tipo de instância, ficou claro que tínhamos muita memória de sobra e estamos atualmente vinculados à CPU da máquina. Então nós apenas trocamos essa proporção. Golang, desde os seus primeiros dias, tem uma bandeira que a maioria dos desenvolvedores não conhecem, chamada GOGC. Esta bandeira, com um padrão de 100, simplesmente diz ao sistema quando acionar o GC. O padrão irá acionar o processo de GC sempre que a pilha atingir 100% do seu tamanho inicial. Mudar esse valor para um número maior atrasará o disparo do GC e baixá-lo acionará o GC mais cedo. Começamos a comparar alguns valores diferentes e o melhor desempenho para o nosso propósito foi alcançado quando usamos: GOGC=2000.

Isso imediatamente aumentou nosso uso de memória de ~200MB para ~2.7GB (Isso é depois que o consumo de memória diminuiu devido à nossa atualização da versão Go) e diminuiu nosso uso de CPU em ~10%.
A seguinte captura de tela demonstra os resultados do benchmark:

GOGC = resultados de 2000 – benchmark Coralogix

As 4 principais funções consumidoras de CPU são as funções do nosso serviço, o que faz sentido. O uso total de GC é agora ~13%, menos da metade do seu consumo anterior(!)

Podíamos ter parado por aí, mas decidimos descobrir onde e porque alocamos tantos objectos. Muitas vezes, há uma boa razão para isso (por exemplo, no caso do processamento stream onde criamos muitos objetos novos para cada mensagem que recebemos e precisamos nos livrar dela porque é irrelevante para a próxima mensagem), mas há casos em que há uma maneira fácil de otimizar e diminuir dramaticamente a criação de objetos.

Para começar, vamos executar o mesmo comando de antes com uma pequena alteração para pegar o heap dump:

http://localhost:6060/debug/pprof/heap

Para consultar o arquivo de resultados você pode executar o seguinte comando dentro da sua pasta de código para analisar o dump:

go tool pprof -alloc_objects <HEAP.PROFILE.FILE>

A nossa fotografia ficou assim:

>

Todos pareciam razoáveis, excepto a terceira linha, que é uma função de monitorização que reporta ao nosso exportador de prometheus no final de cada fase de análise de regras do Coralogix. Para aprofundar, executamos o seguinte comando:

list <FunctionName>

Por exemplo:

list reportRuleExecution

E depois obtivemos o seguinte:

Leave a comment

O seu endereço de email não será publicado.