Tipos complexos de métricas no Prometheus

Publicado em 30 jun 2020. Uns 7 minutos de leitura.

O Prometheus, além dos tipos primitivos de métricas, possui dois tipos mais complexos para monitorar métricas com mais nuances do que apenas contadores e medidas. Esses tipos são o histograma e o sumário. Eles são parecidos no sentido de que dá para fazer certas análises em cima de ambos os tipos, mas possuem algumas diferenças sutís em que um pode ser melhor que o outro, a depender da situação.

📊 Histograma (histogram)

Quem cursou alguma matéria de estatística deve lembrar (ou não) do que é um histograma. Ele é um diagrama cuja área de cada barra e proporcional a frequência de uma variável (a definição é maior do que isso, mas para o que vamos falar aqui é suficiente).

No contexto do Prometheus, um histograma é uma maneira de monitorar observações de um evento. Mas um contador também é, e aí? A diferença é que o histograma monitora a distribuição de valores observados. Para isso, ele recebe uma definição de intervalos (chamados buckets). Cada intervalo vai gerar um contador separado, com uma dimensão le (menor ou igual, em inglês less than or equal).

Supondo que você tem uma métrica de latência de requisições HTTP, e você quer monitorar monitorar qual o percentual destas requisições duram até 200 e até 500 milissegundos. Um histograma com com dois intervalos (até 200 ms e até 500 ms) vai te dar essa resposta. Note que, na verdade, há três intervalos pois automaticamente você vai ter um intervalo de até infinito, para registrar as ocorrências que não estão dentro de nenhum dos intervalos que você definiu.

Dada uma distribuição de latências com essas durações, em milissegundos: 20, 30, 300, 200, 900, 500, 1200, teríamos as seguintes séries temporais:

MétricaValor
http_request_duration_seconds_bucket{le="0.2"}3
http_request_duration_seconds_bucket{le="0.5"}5
http_request_duration_seconds_bucket{le="+Inf"}7
http_request_duration_seconds_sum3150
http_request_duration_seconds_count7

Note que os intervalos são cumulativos. Assim, caso queiramos saber o percentual de requisições que está abaixo de 500 ms, podemos pensar em fazer a query:

http_request_duration_seconds_bucket{le="0.5"}
/
http_request_duration_seconds_bucket{le="+Inf"}

Note que, na prática, podemos ter várias outras dimensões que podem ser agrupadas. Por exemplo, se uma dimensão for o caminho da requisição, a consulta acima vai retornar uma série temporal para cada combinação de le e path.

MétricaValor
http_request_duration_seconds_bucket{path="/", le="0.2"}3
http_request_duration_seconds_bucket{path="/", le="0.5"}5
http_request_duration_seconds_bucket{path="/", le="+Inf"}7
http_request_duration_seconds_sum{path="/"}3150
http_request_duration_seconds_count{path="/"}7
http_request_duration_seconds_bucket{path="/help", le="0.2"}2
http_request_duration_seconds_bucket{path="/help", le="0.5"}10
http_request_duration_seconds_bucket{path="/help", le="+Inf"}18
http_request_duration_seconds_sum{path="/help"}4125
http_request_duration_seconds_count{path="/help"}18

Se adicionarmos o código de retorno, é mais uma dimensão para multiplicar a quantidade de séries temporais retornadas. Além disso, faz sentido olhar para essa métrica numa janela deslizante (por exemplo, os últimos 5 minutos). Do contrário, vamos analisar valores desde que a aplicação começou a rodar. Podemos agrupar com sum e analisar ao longo do tempo com rate.

sum(rate(http_request_duration_seconds_bucket{le="0.5"}[10m]))
/
sum(rate(http_request_duration_seconds_count[10m]))

Como estamos agregando, podemos dividir tanto pelo bucket de limite infinito como pela métrica _count, pois o sum vai tirar as dimensões. Se tentássemos usar o _count no primeiro exemplo, não iria funcionar pois essa métrica não tem a dimensão le, então a divisão não iria acontecer. O intervalo com limite infinito e a métrica _count vão ter sempre o mesmo valor.

📉 Sumário (summary)

No Prometheus, um sumário é usado principalmente para caulcular percentis (voltando à aula de estatística). Um percentil é um valor que divide a população de acordo com a distribuição de valores de uma variável. Em outras palavras, usamos percentis quando queremos saber qual o maior valor que um percentual dos eventos tem.

Um percentil muito conhecido é o percentil 50 (ou p50). Ele é a mediana de um conjunto de valores. Dizer que a mediana, ou p50, de um conjunto é 10 significa que 50% dos valores é menor ou igual a 10. O uso de percentis é interessante pois, diferentemente da média aritimética, os percentis descartam valores excepcionalmente discrepantes mais facilmente.

Estes percentis são calculados do cliente (ou seja, na aplicação que está gerando as métricas) e são pré-definidos por você. Para cada percentil escolhido, o cliente do Prometheus vai gerar uma métrica com uma dimensão quantile (variando de 0 a 1, inclusive). Usando poucas observações é mais chato dar um exemplo numérico, mas as séries temporais geradas a partir de um sumário vão parecer com essas, dada a escolha dos percentis 50, 90 e 95:

MétricaValor
http_request_duration_seconds{quantile="0.5"}50
http_request_duration_seconds{quantile="0.9"}135
http_request_duration_seconds{quantile="0.95"}450
http_request_duration_seconds_sum72843
http_request_duration_seconds_count40

Note que o sumário também expõe duas métricas adicionais, _sum e _count. Com elas, assim como nas do histograma, podemos calcular uma média aritimética simples. Já os percentis já estão calculados, e não precisamos fazer nenhuma agregação. Aliás, não só não precisamos como não podemos.

Deixa eu dizer mais alto pra galera lá atrás: não podemos agregar percentis. Agregações de percentis não têm significado. Isso quer dizer que, ao utilizar um sumário (que nos dá percentis), nós não podemos somar, tirar alguma outra média (aritimética ou até outro percentil), e por aí vai. Não sucumba à tentação.

Isso também implica nossas métricas precisarem ser geradas apenas com as dimensões necessárias desde o início, pois na hora da consulta não podemos juntar os percentis, por exemplo, de todos os códigos de resposta numa métrica de percentil só. Se queremos analisar essa métrica deste jeito, o cliente (nossa aplicação) deve reportá-la sem separar por esta dimensão.

E se você quiser ter mais flexibilidade? Podemos calcular percentis com um outro tipo de métrica no Prometheus. E você já conhece ela.

📊 Histograma 2: O Inimigo Agora é Outro

Os histogramas podem ser usados para estimar, de maneira razoavel, percentis em tempo de consulta. Ou seja, podemos usar as séries temporais de uma métrica do tipo histograma para calcular uma série temporal que representa um percentil.

O Prometheus tem uma função para isso, a histogram_quantile. Ela recebe um histograma (ou seja, uma lista de séries temporais com a dimensão le) e um quantil (um número de 0 a 1, inclusive, como no sumário). Usando as métricas do primeiro exemplo com histograma, podemos fazer uma consulta do p90 considerando uma janela de 10 minutos assim:

histogram_quantile(0.9, rate(http_request_duration_seconds_bucket[10m])

O exemplo acima não tem uma agregação, como somar os intervalos. Para agrupar todas as dimensões, podemos usar o sum novamente. Porém, dessa vez, precisamos manter o agrupamento por intervalo (le). Do contrário, a função histogram_quantile não vai funcionar.

histogram_quantile(0.9, sum(rate(http_request_duration_seconds_bucket[10m]) by (le))

Caso queiramos ter mais de um percentil com mesmo valor retornado, separado por uma dimensão específica (por exemplo, status_code), podemos adicioná-lo no agrupamento com o le.

histogram_quantile(0.9, sum(rate(http_request_duration_seconds_bucket[10m]) by (status_code, le))

É importante ressaltar que os percentis que temos aqui são aproximados. No caso dos calculados a partir do histograma, a qualidade da aproximação depende da escolha dos intervalos. O ideal é que os intervalos consigam representar bem a distribuição dos valores. Se todas as observações caírem em um bucket só, o cálculo do percentil não vai ser realista.

Dito isso, faz se você quer agregar os dados e calcular percentis diferentes em tempo de consulta, histogramas são a única maneira válida. Do contrário, histograma e sumário funcionam. Se você consegue estimar valores para os intervalos do histograma que representam bem a possível distribuição dos valores observados, use um histograma pois você tem bem mais flexibilidade para criar consultas e alertas sem mexer na aplicação. Se você precisa de percentis mais precisos, e pode definir em quais percentis tem interesse a nivel da aplicação, o sumário vai te servir bem.

A documentação do Prometheus tem uma página dedicada a histogramas e sumários que fala bastante sobre o que a gente viu aqui. Caso queira ler mais sobre as diferenças entre essas métricas, ela é um bom recurso!