De alguma forma, tive que lidar com as métricas da nossa API, como sempre (sem tempo?!) Para adicionar mais tarde - é muito difícil e ainda não foi implementado - significa que é hora de implementá-lo. Depois de algumas andanças na rede, o sistema de monitoramento mais popular, me pareceu, foi o Prometeu.
Usando o Prometheus, podemos rastrear vários recursos do computador, como: memória, processador, disco, carga de rede. Também pode ser importante calcular o número de chamadas para os métodos de nossa API ou medir o tempo de sua execução, porque quanto maior a carga no sistema, mais caro é o tempo de inatividade. E aqui Prometeu vem em nosso auxílio. Parece-me que este artigo fornece os principais pontos para entender o trabalho do Prometheus e para adicionar uma coleção de métricas à API. Portanto, começamos com o mais banal, com uma pequena descrição.
Prometheus é um sistema de código aberto e DBMS de séries temporais escrito em Go e desenvolvido pela SoundCloud. Possui documentação oficial e suporte para idiomas como: Go, Java ou Scala, Python, Ruby. Há suporte não oficial para outros idiomas, como: C #, C ++, C, Bash, Lua para Nginx, Lua para Tarantool e outros; toda a lista está no site oficial do Prometheus.
Todos os serviços do Prometheus estão disponíveis como imagens do Docker no Docker Hub ou no Quay.io.
O Prometheus é iniciado pelo docker run -p 9090:9090 prom/prometheus
, que inicia com a configuração padrão e define a porta localhost:9090
Depois disso, a interface do Prometheus estará disponível no localhost:9090
.
O Prometheus é um sistema de monitoramento que inclui várias ferramentas para configurar o monitoramento de aplicativos (pontos de extremidade) usando o protocolo HTTP. Ao se conectar ao Prometheus, a API HTTP não suporta "autenticação básica". Se você deseja usar a autenticação básica para se conectar ao Prometheus, recomendamos que você use o Prometheus em conjunto com um servidor proxy reverso e use a autenticação no nível do proxy. Você pode usar qualquer proxy reverso com o Prometheus.
Os principais componentes do Prometheus:
- um servidor que coleta métricas, as salva no banco de dados e as limpa;
- pacotes para coletar métricas na API;
- Pushgateway - componente para receber métricas de aplicativos para os quais uma solicitação Pull não pode ser usada;
- Exportadores - ferramentas para exportar métricas de aplicativos e serviços de terceiros, instaladas nas máquinas de destino;
- AlertManager - gerenciador de notificações (alertas), os alertas são definidos no arquivo de configuração e definidos por um conjunto de regras para métricas.
Se durante a operação houver conformidade com a regra, um alerta será acionado e enviado aos destinatários especificados por e-mail, Slack ou outros.
Os objetos com os quais o Prometheus trabalha são chamados de métricas recebidas dos destinos pelo Pushgateway ou pelos Exportadores.
Ao coletar métricas, vários métodos de transmissão são usados:
- O Prometheus solicita métricas do destino por meio de uma solicitação Pull, cujas configurações são especificadas no arquivo de configuração na seção scrape_config para cada tarefa.
Quando o sistema coleta dados, você pode controlar a frequência da coleta e criar várias configurações de coleta de dados para selecionar uma frequência diferente para objetos diferentes; - Os exportadores permitem coletar métricas de vários objetos, por exemplo: bancos de dados (MongoDB, SQL, etc.), intermediários de mensagens (RabbitMQ, EMQ, NSQ, etc.), balanceadores de carga HTTP, etc;
- Pushgateway. Pode ser usado, se necessário, quando o aplicativo não puder fornecer a métrica diretamente ao Prometheus; ou ao usar tarefas em lote que não têm a capacidade de usar a solicitação pull do Prometheus.
Assim, todas as métricas recebidas serão armazenadas pelo Prometheus em um banco de dados com registro de data e hora.
Configuração
O Prometheus é configurado usando os sinalizadores de linha de comando e os arquivos de configuração fornecidos no formato YAML. Os sinalizadores da linha de comando permitem configurar parâmetros imutáveis, como: caminhos, volumes de dados armazenados no disco e na memória, etc. O arquivo de configuração permite configurar tudo relacionado a tarefas e configurar arquivos yaml de regra carregados. Tudo está escrito no arquivo de configuração global, permite definir configurações gerais para todos e destacar configurações para diferentes seções de configuração separadamente. As configurações pesquisadas pelo Prometheus são definidas no arquivo de configuração na seção scrape_configs.
O Prometheus pode recarregar os arquivos de configuração durante a operação, se a nova configuração for inválida, ela não será aplicada. A reinicialização do arquivo de configuração é acionada enviando o comando SIGHUP Prometheus ou enviando uma solicitação HTTP POST para /-/reload
, desde que o --web.enable-lifecycle
esteja --web.enable-lifecycle
. Também recarregará todos os arquivos de regras configurados.
Que tipos de dados são usados
O Prometheus armazena um modelo de dados multidimensionais personalizado e usa uma linguagem de consulta para dados multidimensionais chamada PromQL. O Prometheus armazena dados na forma de séries temporais; suporta várias opções de armazenamento:
- armazenamento em disco local: a cada 2 horas, os dados armazenados em buffer na memória são compactados e armazenados em disco. Por padrão, o diretório ./data é usado no diretório de trabalho para salvar arquivos compactados;
- Repositório remoto: O Prometheus suporta a integração com repositórios de terceiros (por exemplo: Kafka, PostgreSQL, Amazon S3, etc.) através do adaptador Protocol Buffer.
A série temporal armazenada é determinada pelas métricas e metadados na forma de pares de valores-chave, embora, se necessário, o nome da métrica não possa ser usado e a própria métrica consistirá apenas de metadados. Uma série temporal pode ser formalmente definida como <nome da métrica> {<metadados>}. A chave é <nome da métrica> {<metadata>} - o que estamos medindo, e o valor é o valor real como um número com o tipo float64 (o Prometheus suporta apenas esse tipo). A descrição da chave contém metadados (rótulos), também descritos por pares de valores-chave: <nome do rótulo> = "<valor do rótulo>", <nome do rótulo> = "<valor do rótulo>", ...
Ao armazenar métricas, os seguintes tipos de dados são usados:
- Contador - conta a quantia durante um período de tempo. Esse tipo de métrica pode aumentar apenas (você não pode usar valores negativos) ou redefinir o valor.
Pode ser adequado, por exemplo, para contar o número de solicitações por minuto ou o número de erros por dia, o número de pacotes de rede enviados / recebidos etc. - Medidor - armazena valores que podem diminuir ou aumentar ao longo do tempo.
O medidor não mostra o desenvolvimento de métricas durante um período de tempo. Usando o Gauge, você pode perder alterações irregulares de métrica ao longo do tempo. - Histograma - salva várias séries temporais: a soma total de todos os valores observados; o número de eventos que foram observados;
contadores acumulativos (baldes) - são indicados no rótulo como le="<upper inclusive bound>"
.
Os valores são coletados em áreas com limites superiores personalizados (buckets). - Resumo - salva várias séries temporais: a soma total de todos os valores observados; o número de eventos que foram observados;
flow-quantis do fluxo (0 ≤ φ ≤ 1) dos eventos observados - são indicados no rótulo como quantile="<φ>"
.
Como os dados são salvos?
Prometheus recomenda "dar" 2/3 da RAM a um aplicativo em execução.
Para armazenar dados na memória, o Prometheus usa arquivos chamados chunk; cada métrica possui seu próprio arquivo. Todos os arquivos de partes são imutáveis, exceto o último no qual os dados são gravados. Novos dados são salvos em partes e a cada 2 horas o fluxo em segundo plano combina os dados e os grava no disco. Cada bloco de duas horas consiste em um diretório que contém um ou mais arquivos de partes que contêm todas as amostras de séries temporais para esse período, bem como um arquivo de metadados e um arquivo de índice (que indexa os nomes das métricas e rótulos das séries temporais nos arquivos de partes). Se em uma hora o Prometheus não gravar dados no chunck, ele será salvo no disco e um novo chunck será criado para gravar os dados. O período máximo de retenção de dados no Prometheus é de ~ 21 dias.
Porque Como o tamanho da memória é fixo, o desempenho de gravação e leitura do sistema será limitado por essa quantidade de memória. A quantidade de memória PTSDB é determinada pelo período mínimo, pelo período de coleta e pelo número de métricas de tempo.
O Prometheus também possui um mecanismo WAL para evitar a perda de dados.
O write write log (WAL) serializa operações memorizadas em uma mídia permanente na forma de arquivos de log. No caso de uma falha, os arquivos WAL podem ser usados para restaurar o banco de dados para seu estado consistente, restaurando a partir dos logs.
Os arquivos de log são armazenados em um diretório wal em segmentos de 128 MB. Esses arquivos contêm dados brutos que ainda não foram compactados, portanto, são significativamente maiores que os arquivos de fragmento regulares.
O Prometheus armazenará pelo menos três arquivos de log, mas os servidores com alto tráfego podem ver mais de três arquivos WAL, pois precisam armazenar pelo menos duas horas de dados brutos.
O resultado do uso do WAL é uma redução significativa no número de solicitações de gravação no disco, conforme apenas um arquivo de log precisa ser gravado no disco e nem todos os dados que foram alterados como resultado da operação. O arquivo de log é gravado sequencialmente e, portanto, o custo de sincronização do log é muito menor que o custo de gravação de fragmentos com dados.
O Prometheus salva pontos de interrupção periódicos, que por padrão são adicionados a cada 2 horas, compactando os logs do período anterior e salvando-os em disco.
Todos os pontos de interrupção são armazenados no mesmo diretório que checkpoint.ddd, em que ddd é um número crescente monotonicamente. Portanto, ao se recuperar de uma falha, ele pode restaurar pontos de interrupção do catálogo de pontos de interrupção com uma indicação da ordem (.ddd).
Ao escrever logs WAL, você pode retornar a qualquer ponto de verificação para o qual o log de dados está disponível.
O que aconteceu na prática?
Ao adicionar ao projeto (.Net Framework), usamos o pacote Prometheus.Client.3.0.2 para coletar métricas. Para coletar métricas, os métodos e classes necessários foram adicionados ao projeto para armazenar métricas até que sejam recebidos pelo Prometheus.
Uma interface IMetricsService foi originalmente definida que continha métodos de timer para medir por quanto tempo os métodos funcionaram:
public interface IMetricsService { Stopwatch StartTimer(); void StopTimer(Stopwatch timer, string controllerName, string actionName, string methodName = "POST"); }
Adicionamos a classe MetricsService, que implementa a interface IMetricsService e armazena temporariamente as métricas.
public class MetricsService : IMetricsService { private static Histogram _histogram; static MetricsService() { _histogram = CreateHistogram(); } public Stopwatch StartTimer() { try { var timer = new Stopwatch(); timer.Start(); return timer; } catch (Exception exception) { Logger.Error(exception); } return null; } public void StopTimer(Stopwatch timer, string controllerName, string actionName, string methodName = "POST") { try { if (timer == null) { throw new ArgumentException($"{nameof(timer)} can't be null."); } timer.Stop(); _histogram .WithLabels(controllerName, actionName, methodName) .Observe(timer.ElapsedMilliseconds, DateTimeOffset.UtcNow); } catch (Exception exception) { Logger.Error(exception); } } public static List<string> GetAllLabels() { var metricsList = new List<string>(); try { foreach (var keyValuePair in _histogram.Labelled) { var controllerName = keyValuePair.Key.Labels[0].Value; var actionName = keyValuePair.Key.Labels[1].Value; var methodName = keyValuePair.Key.Labels[2].Value; var requestDurationSum = keyValuePair.Value.Value.Sum; var requestCount = keyValuePair.Value.Value.Count; metricsList.Add($"http_request_duration_widget_sum{{controller={controllerName},action={actionName},method={methodName}}} {requestDurationSum}"); metricsList.Add($"http_request_duration_widget_count{{controller={controllerName},action={actionName},method={methodName}}} {requestCount}"); } _histogram = CreateHistogram(); } catch (Exception exception) { Logger.Error(exception); } return metricsList; } private static Histogram CreateHistogram() { var newMetrics = Metrics .WithCustomRegistry(new CollectorRegistry()) .CreateHistogram(name: "http_request_duration_web_api", help: "Histogram metrics of Web.Api", includeTimestamp: true, labelNames: new[] { "controller", "action", "method" }); var oldValue = _histogram; for (var i = 0; i < 10; i++) { var oldValue = Interlocked.Exchange<Histogram>(ref oldValue, newMetrics); if (oldValue != null) { return oldValue; } } return null; } }
Agora podemos usar nossa classe para salvar as métricas que planejamos coletar nos métodos Application_BeginRequest, Application_Error, Application_EndRequest. Na classe Global.cs, adicionamos uma coleção de métricas aos métodos acima.
private IMetricsService _metricsService; protected virtual void Application_BeginRequest(object sender, EventArgs e) { var context = new HttpContextWrapper(HttpContext.Current); var metricServiceTimer = _metricsService.StartTimer(); context.Items.Add("metricsService", _metricsService); context.Items.Add("metricServiceTimer", metricServiceTimer); } protected virtual void Application_EndRequest(object sender, EventArgs e) { WriteMetrics(new HttpContextWrapper(HttpContext.Current)); } protected void Application_Error(object sender, EventArgs e) { WriteMetrics(new HttpContextWrapper(HttpContext.Current)); } private void WriteMetrics(HttpContextBase context) { try { _metricsService = context.Items["metricsService"] as IMetricsService; if (_metricsService != null) { var timer = context.Items["metricServiceTimer"] as Stopwatch; string controllerName = null; string actionName = null; var rd = RouteTable.Routes.GetRouteData(context); if (rd != null) { controllerName = rd.GetRequiredString("controller"); actionName = rd.GetRequiredString("action"); } _metricsService.StopTimer(timer, controllerName, actionName, context.Request.HttpMethod); } } catch (Exception exception) { Logger.Error("Can't write metrics.", exception); } }
Adicione um novo controlador, que será um ponto de referência para o envio das métricas da nossa API para o Prometheus:
public class MetricsController : Controller { [HttpGet] public string[] GetAllMetrics() { try { var metrics = MetricsService.GetAllLabels(); return metrics.ToArray(); } catch (Exception exception) { Logger.Error(exception); } return new string[] { }; } }
A última etapa será configurar a configuração do Prometheus para coletar métricas na seção scrape_configs, após a qual podemos ver as métricas coletadas já na interface do usuário do Prometheus ou Grafana.
Principais recursos que nos interessavam no Prometheus:
Modelo de dados multidimensional: métricas e rótulos.
Linguagem de consulta flexível do PromQL. No mesmo operador de consulta, podemos usar operações como multiplicação, adição, concatenação, etc; pode ser executado com várias métricas.
Reúne dados baseados em HTTP usando o método pull.
Compatível com o método push via Pushgateway.
É possível coletar métricas de outros aplicativos através dos Exportadores.
Fornece um mecanismo para impedir a perda de dados.
Suporta várias representações gráficas de dados.