Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Sugiro que você leia a transcrição do relatório do final de 2019 de Alexander Valyalkin “Otimizações Go em VictoriaMetrics”

VictoriaMetrics — um SGBD rápido e escalável para armazenamento e processamento de dados na forma de uma série temporal (o registro forma o tempo e um conjunto de valores correspondentes a esse tempo, por exemplo, obtidos por meio de sondagem periódica do status dos sensores ou coleta de Métricas).

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Aqui está um link para o vídeo deste relatório - https://youtu.be/MZ5P21j_HLE

Slides

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Conte-nos sobre você. Eu sou Alexander Valyalkin. Aqui minha conta GitHub. Sou apaixonado por Go e otimização de desempenho. Escrevi muitas bibliotecas úteis e não tão úteis. Eles começam com fast, ou com quick prefixo.

Atualmente estou trabalhando no VictoriaMetrics. O que é e o que estou fazendo aí? Falarei sobre isso nesta apresentação.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

O esboço do relatório é o seguinte:

  • Primeiro, vou lhe contar o que é VictoriaMetrics.
  • Então direi o que são séries temporais.
  • Então vou lhe contar como funciona um banco de dados de série temporal.
  • A seguir, falarei sobre a arquitetura do banco de dados: em que ela consiste.
  • E então vamos passar para as otimizações que VictoriaMetrics possui. Esta é uma otimização para o índice invertido e uma otimização para a implementação do bitset em Go.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Alguém na plateia sabe o que é VictoriaMetrics? Nossa, muita gente já sabe. É uma boa notícia. Para quem não sabe, este é um banco de dados de séries temporais. É baseado na arquitetura ClickHouse, em alguns detalhes da implementação do ClickHouse. Por exemplo, em: MergeTree, cálculo paralelo em todos os núcleos de processador disponíveis e otimização de desempenho trabalhando em blocos de dados colocados no cache do processador.

VictoriaMetrics oferece melhor compactação de dados do que outros bancos de dados de série temporal.

Ele é dimensionado verticalmente - ou seja, você pode adicionar mais processadores e mais RAM em um computador. A VictoriaMetrics utilizará com sucesso esses recursos disponíveis e melhorará a produtividade linear.

VictoriaMetrics também é dimensionado horizontalmente - ou seja, você pode adicionar nós adicionais ao cluster VictoriaMetrics e seu desempenho aumentará quase linearmente.

Como você adivinhou, VictoriaMetrics é um banco de dados rápido, porque não consigo escrever outros. E está escrito em Go, então vou falar sobre isso neste encontro.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Quem sabe o que é uma série temporal? Ele também conhece muitas pessoas. Uma série temporal é uma série de pares (timestamp, значение), onde esses pares são classificados por tempo. O valor é um número de ponto flutuante – float64.

Cada série temporal é identificada exclusivamente por uma chave. Em que consiste esta chave? Consiste em um conjunto não vazio de pares de valores-chave.

Aqui está um exemplo de uma série temporal. A chave desta série é uma lista de pares: __name__="cpu_usage" é o nome da métrica, instance="my-server" - este é o computador no qual esta métrica é coletada, datacenter="us-east" - este é o data center onde este computador está localizado.

Acabamos com um nome de série temporal que consiste em três pares de valores-chave. Esta chave corresponde a uma lista de pares (timestamp, value). t1, t3, t3, ..., tN - estes são carimbos de data e hora, 10, 20, 12, ..., 15 — os valores correspondentes. Este é o uso da CPU em um determinado momento para uma determinada série.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Onde as séries temporais podem ser usadas? Alguém tem alguma ideia?

  • No DevOps, você pode medir CPU, RAM, rede, rps, número de erros, etc.
  • IoT - podemos medir temperatura, pressão, coordenadas geográficas e algo mais.
  • Também finanças – podemos monitorar os preços de todos os tipos de ações e moedas.
  • Além disso, séries temporais podem ser utilizadas no monitoramento de processos produtivos em fábricas. Temos usuários que usam VictoriaMetrics para monitorar turbinas eólicas, para robôs.
  • As séries temporais também são úteis para coletar informações de sensores de vários dispositivos. Por exemplo, para um motor; para medir a pressão dos pneus; para medir velocidade, distância; para medir o consumo de gasolina, etc.
  • Séries temporais também podem ser usadas para monitorar aeronaves. Cada aeronave possui uma caixa preta que coleta séries temporais para vários parâmetros de saúde da aeronave. As séries temporais também são usadas na indústria aeroespacial.
  • Cuidados de saúde são pressão arterial, pulso, etc.

Pode haver mais aplicativos dos quais esqueci, mas espero que você entenda que as séries temporais são usadas ativamente no mundo moderno. E o volume de seu uso cresce a cada ano.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Por que você precisa de um banco de dados de série temporal? Por que você não pode usar um banco de dados relacional regular para armazenar séries temporais?

Porque as séries temporais geralmente contêm uma grande quantidade de informações, que são difíceis de armazenar e processar em bancos de dados convencionais. Assim, surgiram bancos de dados especializados para séries temporais. Essas bases armazenam efetivamente pontos (timestamp, value) com a chave fornecida. Eles fornecem uma API para leitura de dados armazenados por chave, por um único par de valores-chave ou por vários pares de valores-chave ou por regexp. Por exemplo, você deseja encontrar a carga da CPU de todos os seus serviços em um data center na América, então você precisa usar esta pseudoconsulta.

Normalmente, os bancos de dados de série temporal fornecem linguagens de consulta especializadas porque o SQL de série temporal não é muito adequado. Embora existam bancos de dados que suportam SQL, não é muito adequado. Linguagens de consulta como PromQL, InfluxoQL, Fluxo, Q. Espero que alguém tenha ouvido pelo menos uma dessas línguas. Muitas pessoas provavelmente já ouviram falar do PromQL. Esta é a linguagem de consulta do Prometheus.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Esta é a aparência de uma arquitetura moderna de banco de dados de série temporal usando VictoriaMetrics como exemplo.

Consiste em duas partes. Este é o armazenamento para o índice invertido e o armazenamento para valores de série temporal. Esses repositórios são separados.

Quando um novo registro chega ao banco de dados, primeiro acessamos o índice invertido para encontrar o identificador da série temporal para um determinado conjunto label=value para uma determinada métrica. Encontramos esse identificador e salvamos o valor no armazenamento de dados.

Quando chega uma solicitação para recuperar dados do TSDB, primeiro vamos para o índice invertido. Vamos pegar tudo timeseries_ids registros que correspondem a este conjunto label=value. E então obtemos todos os dados necessários do data warehouse, indexados por timeseries_ids.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Vejamos um exemplo de como um banco de dados de série temporal processa uma consulta de seleção recebida.

  • Primeiro de tudo ela consegue tudo timeseries_ids de um índice invertido que contém os pares fornecidos label=valueou satisfazer uma determinada expressão regular.
  • Em seguida, ele recupera todos os pontos de dados do armazenamento de dados em um determinado intervalo de tempo para os encontrados timeseries_ids.
  • Depois disso, o banco de dados realiza alguns cálculos sobre esses pontos de dados, conforme solicitação do usuário. E depois disso ele retorna a resposta.

Nesta apresentação contarei a vocês a primeira parte. Esta é uma pesquisa timeseries_ids por índice invertido. Você pode assistir sobre a segunda parte e a terceira parte mais tarde Fontes VictoriaMetrics, ou espere até eu preparar outros relatórios :)

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Vamos passar para o índice invertido. Muitos podem pensar que isso é simples. Quem sabe o que é um índice invertido e como funciona? Oh, não há mais tantas pessoas. Vamos tentar entender o que é.

Na verdade é simples. É simplesmente um dicionário que mapeia uma chave para um valor. O que é uma chave? Este casal label=valueOnde label и value - estas são linhas. E os valores são um conjunto timeseries_ids, que inclui o par fornecido label=value.

O índice invertido permite que você encontre tudo rapidamente timeseries_ids, que deram label=value.

Também permite que você encontre rapidamente timeseries_ids série temporal para vários pares label=value, ou para casais label=regexp. Como isso acontece? Encontrando a intersecção do conjunto timeseries_ids para cada par label=value.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Vejamos várias implementações do índice invertido. Vamos começar com a implementação ingênua mais simples. Ela se parece com isso.

Função getMetricIDs obtém uma lista de strings. Cada linha contém label=value. Esta função retorna uma lista metricIDs.

Como funciona? Aqui temos uma variável global chamada invertedIndex. Este é um dicionário normal (map), que mapeará a string para fatiar ints. A linha contém label=value.

Implementação da função: get metricIDs pela primeira vez label=value, então passamos por todo o resto label=value, entendemos metricIDs para eles. E chame a função intersectInts, que será discutido abaixo. E esta função retorna a interseção dessas listas.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Como você pode ver, implementar um índice invertido não é muito complicado. Mas esta é uma implementação ingênua. Que desvantagens isso tem? A principal desvantagem da implementação ingênua é que esse índice invertido é armazenado na RAM. Após reiniciar a aplicação perdemos este índice. Não há salvamento deste índice em disco. É improvável que tal índice invertido seja adequado para um banco de dados.

A segunda desvantagem também está relacionada à memória. O índice invertido deve caber na RAM. Se exceder o tamanho da RAM, obviamente obteremos um erro de falta de memória. E o programa não funcionará.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Este problema pode ser resolvido usando soluções prontas, como NívelDBOu RocksDBName.

Resumindo, precisamos de um banco de dados que nos permita realizar três operações rapidamente.

  • A primeira operação é gravar ключ-значение para este banco de dados. Ela faz isso muito rapidamente, onde ключ-значение são strings arbitrárias.
  • A segunda operação é uma busca rápida por um valor usando uma determinada chave.
  • E a terceira operação é uma busca rápida por todos os valores por um determinado prefixo.

LevelDB e RocksDB – esses bancos de dados foram desenvolvidos pelo Google e Facebook. Primeiro veio o LevelDB. Aí o pessoal do Facebook pegou o LevelDB e começou a melhorar, eles fizeram o RocksDB. Agora, quase todos os bancos de dados internos funcionam no RocksDB dentro do Facebook, incluindo aqueles que foram transferidos para RocksDB e MySQL. Eles o nomearam Minhas rochas.

Um índice invertido pode ser implementado usando LevelDB. Como fazer isso? Nós salvamos como uma chave label=value. E o valor é o identificador da série temporal onde o par está presente label=value.

Se tivermos muitas séries temporais com um determinado par label=value, então haverá muitas linhas neste banco de dados com a mesma chave e diferentes timeseries_ids. Para obter uma lista de todos timeseries_ids, que começa com isso label=prefix, fazemos uma varredura de intervalo para o qual esse banco de dados está otimizado. Ou seja, selecionamos todas as linhas que começam com label=prefix e obtenha o necessário timeseries_ids.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Aqui está um exemplo de implementação de como seria em Go. Temos um índice invertido. Este é o LevelDB.

A função é a mesma da implementação ingênua. Ele repete a implementação ingênua quase linha por linha. A única questão é que, em vez de nos voltarmos para map acessamos o índice invertido. Obtemos todos os valores do primeiro label=value. Então passamos por todos os pares restantes label=value e obtenha os conjuntos correspondentes de metricIDs para eles. Então encontramos a interseção.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Tudo parece estar bem, mas há desvantagens nesta solução. VictoriaMetrics implementou inicialmente um índice invertido baseado em LevelDB. Mas no final tive que desistir.

Por que? Porque o LevelDB é mais lento que a implementação ingênua. Em uma implementação ingênua, dada uma determinada chave, recuperamos imediatamente a fatia inteira metricIDs. Esta é uma operação muito rápida – toda a fatia está pronta para uso.

No LevelDB, toda vez que uma função é chamada GetValues você precisa passar por todas as linhas que começam com label=value. E obtenha o valor de cada linha timeseries_ids. De tal timeseries_ids colete uma fatia destes timeseries_ids. Obviamente, isso é muito mais lento do que simplesmente acessar um mapa normal por chave.

A segunda desvantagem é que o LevelDB é escrito em C. Chamar funções C do Go não é muito rápido. Demora centenas de nanossegundos. Isso não é muito rápido, porque comparado a uma chamada de função regular escrita em go, que leva de 1 a 5 nanossegundos, a diferença no desempenho é dezenas de vezes. Para VictoriaMetrics esta foi uma falha fatal :)

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Então escrevi minha própria implementação do índice invertido. E ele ligou para ela conjunto de mesclagem.

Mergeset é baseado na estrutura de dados MergeTree. Esta estrutura de dados foi emprestada do ClickHouse. Obviamente, o mergeset deve ser otimizado para pesquisa rápida timeseries_ids de acordo com a chave fornecida. Mergeset é escrito inteiramente em Go. Você pode ver Fontes VictoriaMetrics no GitHub. A implementação do mergeset está na pasta /lib/mergeset. Você pode tentar descobrir o que está acontecendo lá.

A API mergeset é muito semelhante ao LevelDB e RocksDB. Ou seja, permite salvar rapidamente novos registros e selecionar rapidamente os registros por um determinado prefixo.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Falaremos sobre as desvantagens do mergeset mais tarde. Agora vamos falar sobre quais problemas surgiram com o VictoriaMetrics na produção ao implementar um índice invertido.

Por que eles surgiram?

A primeira razão é a alta taxa de rotatividade. Traduzido para o russo, esta é uma mudança frequente nas séries temporais. É quando uma série temporal termina e uma nova série começa, ou muitas novas séries temporais começam. E isso acontece com frequência.

A segunda razão é o grande número de séries temporais. No início, quando o monitoramento ganhava popularidade, o número de séries temporais era pequeno. Por exemplo, para cada computador você precisa monitorar CPU, memória, rede e carga de disco. 4 séries temporais por computador. Digamos que você tenha 100 computadores e 400 séries temporais. Isso é muito pouco.

Com o tempo, as pessoas descobriram que poderiam medir informações mais granulares. Por exemplo, meça a carga não de todo o processador, mas separadamente de cada núcleo do processador. Se você tiver 40 núcleos de processador, terá 40 vezes mais séries temporais para medir a carga do processador.

Mas isso não é tudo. Cada núcleo do processador pode ter vários estados, como inativo, quando está ocioso. E também trabalhe no espaço do usuário, trabalhe no espaço do kernel e outros estados. E cada um desses estados também pode ser medido como uma série temporal separada. Além disso, isso aumenta o número de linhas em 7 a 8 vezes.

De uma métrica obtivemos 40 x 8 = 320 métricas para apenas um computador. Multiplicando por 100, obtemos 32 em vez de 000.

Então apareceu o Kubernetes. E piorou porque o Kubernetes pode hospedar muitos serviços diferentes. Cada serviço no Kubernetes consiste em vários pods. E tudo isso precisa ser monitorado. Além disso, temos uma implantação constante de novas versões de seus serviços. Para cada nova versão, novas séries temporais devem ser criadas. Como resultado, o número de séries temporais cresce exponencialmente e nos deparamos com o problema de um grande número de séries temporais, o que é chamado de alta cardinalidade. VictoriaMetrics lida com isso com sucesso em comparação com outros bancos de dados de série temporal.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Vamos dar uma olhada mais de perto na alta taxa de rotatividade. O que causa uma alta taxa de rotatividade na produção? Porque alguns significados de rótulos e tags estão em constante mudança.

Por exemplo, veja o Kubernetes, que tem o conceito deployment, ou seja, quando uma nova versão do seu aplicativo for lançada. Por algum motivo, os desenvolvedores do Kubernetes decidiram adicionar o ID de implantação ao rótulo.

A que isso levou? Além disso, a cada nova implantação, todas as séries temporais antigas são interrompidas e, em vez delas, novas séries temporais começam com um novo valor de rótulo deployment_id. Pode haver centenas de milhares e até milhões dessas linhas.

O importante sobre tudo isso é que o número total de séries temporais cresce, mas o número de séries temporais que estão atualmente ativas e recebendo dados permanece constante. Esse estado é chamado de alta taxa de rotatividade.

O principal problema da alta taxa de rotatividade é garantir uma velocidade de pesquisa constante para todas as séries temporais para um determinado conjunto de rótulos durante um determinado intervalo de tempo. Normalmente, este é o intervalo de tempo da última hora ou do último dia.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Como resolver este problema? Aqui está a primeira opção. Isso serve para dividir o índice invertido em partes independentes ao longo do tempo. Ou seja, passado algum intervalo de tempo, terminamos de trabalhar com o índice invertido atual. E crie um novo índice invertido. Passa mais um intervalo de tempo, criamos outro e outro.

E ao fazer a amostragem desses índices invertidos, encontramos um conjunto de índices invertidos que se enquadram no intervalo determinado. E, consequentemente, selecionamos o id da série temporal a partir daí.

Isso economiza recursos porque não precisamos olhar as peças que não estão dentro do intervalo determinado. Isto é, normalmente, se selecionarmos os dados da última hora, então, para os intervalos de tempo anteriores, ignoramos as solicitações.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Existe outra opção para resolver este problema. Isso armazena para cada dia uma lista separada de IDs de séries temporais que ocorreram naquele dia.

A vantagem desta solução em relação à solução anterior é que não duplicamos informações de séries temporais que não desaparecem com o tempo. Eles estão constantemente presentes e não mudam.

A desvantagem é que tal solução é mais difícil de implementar e mais difícil de depurar. E a VictoriaMetrics escolheu esta solução. Foi assim que aconteceu historicamente. Esta solução também tem um bom desempenho em comparação com a anterior. Porque esta solução não foi implementada devido ao facto de ter que duplicar dados em cada partição para séries temporais que não se alteram, ou seja, que não desaparecem com o tempo. VictoriaMetrics foi otimizado principalmente para consumo de espaço em disco, e a implementação anterior piorou o consumo de espaço em disco. Mas esta implementação é mais adequada para minimizar o consumo de espaço em disco, por isso foi escolhida.

Eu tive que lutar com ela. A dificuldade foi que nesta implementação você ainda precisa escolher um número muito maior timeseries_ids para dados do que quando o índice invertido é particionado por tempo.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Como resolvemos esse problema? Resolvemos isso de uma maneira original - armazenando vários identificadores de série temporal em cada entrada de índice invertida, em vez de um identificador. Ou seja, temos uma chave label=value, que ocorre em todas as séries temporais. E agora salvamos vários timeseries_ids em uma entrada.

Aqui está um exemplo. Anteriormente tínhamos N entradas, mas agora temos uma entrada cujo prefixo é igual a todas as outras. Para a entrada anterior, o valor contém todos os IDs de série temporal.

Isso tornou possível aumentar a velocidade de digitalização desse índice invertido em até 10 vezes. E nos permitiu reduzir o consumo de memória do cache, pois agora armazenamos a string label=value apenas uma vez no cache juntos N vezes. E essa linha pode ser grande se você armazenar linhas longas em suas tags e rótulos, que o Kubernetes gosta de colocar lá.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Outra opção para acelerar a pesquisa em um índice invertido é a fragmentação. Criação de vários índices invertidos em vez de um e fragmentação de dados entre eles por chave. Este é um conjunto key=value vapor. Ou seja, obtemos vários índices invertidos independentes, que podemos consultar em paralelo em vários processadores. As implementações anteriores permitiam apenas a operação no modo de processador único, ou seja, verificando dados em apenas um núcleo. Esta solução permite digitalizar dados em vários núcleos ao mesmo tempo, como o ClickHouse gosta de fazer. É isso que pretendemos implementar.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Agora vamos voltar às nossas ovelhas - à função de interseção timeseries_ids. Vamos considerar quais implementações podem existir. Esta função permite que você encontre timeseries_ids para um determinado conjunto label=value.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

A primeira opção é uma implementação ingênua. Dois loops aninhados. Aqui obtemos a entrada da função intersectInts duas fatias - a и b. Na saída, deve retornar para nós a intersecção dessas fatias.

Uma implementação ingênua se parece com isto. Iteramos todos os valores da fatia a, dentro deste loop percorremos todos os valores da fatia b. E nós os comparamos. Se corresponderem, encontramos uma interseção. E salve-o em result.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Quais são as desvantagens? A complexidade quadrática é sua principal desvantagem. Por exemplo, se suas dimensões forem fatia a и b um milhão de cada vez, esta função nunca retornará uma resposta para você. Porque será necessário fazer um trilhão de iterações, o que é muito mesmo para computadores modernos.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

A segunda implementação é baseada no mapa. Criamos mapa. Colocamos todos os valores da fatia neste mapa a. Então passamos por slice em um loop separado b. E verificamos se esse valor é da fatia b no mapa. Se existir, adicione-o ao resultado.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Quais são os benefícios? A vantagem é que existe apenas complexidade linear. Ou seja, a função será executada muito mais rapidamente para fatias maiores. Para uma fatia do tamanho de um milhão, esta função será executada em 2 milhões de iterações, em oposição aos trilhões de iterações da função anterior.

A desvantagem é que esta função requer mais memória para criar este mapa.

A segunda desvantagem é a grande sobrecarga de hashing. Esta desvantagem não é muito óbvia. E para nós também não era muito óbvio, então a princípio no VictoriaMetrics a implementação da interseção era através de um mapa. Mas então o perfil mostrou que o tempo do processador principal é gasto gravando no mapa e verificando a presença de um valor nesse mapa.

Por que o tempo da CPU é desperdiçado nesses locais? Porque Go executa uma operação de hash nessas linhas. Ou seja, calcula o hash da chave para depois acessá-la em um determinado índice do HashMap. A operação de cálculo de hash é concluída em dezenas de nanossegundos. Isso é lento para VictoriaMetrics.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Decidi implementar um bitset otimizado especificamente para este caso. Esta é a aparência da intersecção de duas fatias agora. Aqui criamos um bitset. Adicionamos elementos da primeira fatia a ela. Em seguida verificamos a presença desses elementos na segunda fatia. E adicione-os ao resultado. Ou seja, quase não difere do exemplo anterior. A única coisa aqui é que substituímos o acesso ao mapa por funções personalizadas add и has.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

À primeira vista, parece que isso deveria funcionar mais devagar, se anteriormente um mapa padrão fosse usado lá, e então algumas outras funções fossem chamadas, mas o perfil mostra que isso funciona 10 vezes mais rápido que o mapa padrão no caso do VictoriaMetrics.

Além disso, utiliza muito menos memória em comparação com a implementação do mapa. Porque estamos armazenando bits aqui em vez de valores de oito bytes.

A desvantagem desta implementação é que ela não é tão óbvia, nem trivial.

Outra desvantagem que muitos podem não notar é que esta implementação pode não funcionar bem em alguns casos. Ou seja, está otimizado para um caso específico, para este caso de intersecção de ids de séries temporais VictoriaMetrics. Isso não significa que seja adequado para todos os casos. Se for usado incorretamente, não obteremos um aumento de desempenho, mas sim um erro de falta de memória e lentidão no desempenho.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Consideremos a implementação desta estrutura. Se quiser dar uma olhada, está localizado nas fontes do VictoriaMetrics, na pasta lib/uint64set. Ele é otimizado especificamente para o caso VictoriaMetrics, onde timeseries_id é um valor de 64 bits, onde os primeiros 32 bits são basicamente constantes e apenas os últimos 32 bits mudam.

Esta estrutura de dados não é armazenada em disco, apenas opera na memória.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Aqui está sua API. Não é muito complicado. A API é adaptada especificamente para um exemplo específico de uso do VictoriaMetrics. Ou seja, não existem funções desnecessárias aqui. Aqui estão as funções que são usadas explicitamente pelo VictoriaMetrics.

Existem funções add, que adiciona novos valores. Existe uma função has, que verifica novos valores. E há uma função del, que remove valores. Existe uma função auxiliar len, que retorna o tamanho do conjunto. Função clone clona muito. E função appendto converte este conjunto em fatia timeseries_ids.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

É assim que se parece a implementação desta estrutura de dados. conjunto tem dois elementos:

  • ItemsCount é um campo auxiliar para retornar rapidamente o número de elementos em um conjunto. Seria possível prescindir deste campo auxiliar, mas ele teve que ser adicionado aqui porque o VictoriaMetrics frequentemente consulta o comprimento do bitset em seus algoritmos.

  • O segundo campo é buckets. Esta é uma fatia da estrutura bucket32. Cada estrutura armazena hi campo. Estes são os 32 bits superiores. E duas fatias - b16his и buckets de bucket16 estruturas.

Os 16 bits principais da segunda parte da estrutura de 64 bits são armazenados aqui. E aqui os bitsets são armazenados para os 16 bits inferiores de cada byte.

Bucket64 consiste em uma matriz uint64. O comprimento é calculado usando essas constantes. Em um bucket16 máximo pode ser armazenado 2^16=65536 pedaço. Se você dividir isso por 8, serão 8 kilobytes. Se você dividir por 8 novamente, dá 1000 uint64 significado. Aquilo é Bucket16 – esta é a nossa estrutura de 8 kilobytes.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Vejamos como é implementado um dos métodos desta estrutura para adicionar um novo valor.

Tudo começa com uint64 significados. Calculamos os 32 bits superiores e calculamos os 32 bits inferiores. Vamos passar por tudo buckets. Comparamos os 32 bits principais em cada intervalo com o valor que está sendo adicionado. E se eles corresponderem, chamamos a função add na estrutura b32 buckets. E adicione os 32 bits inferiores lá. E se voltou true, isso significa que adicionamos esse valor lá e não o tínhamos. Se retornar false, então esse significado já existia. Então aumentamos o número de elementos na estrutura.

Se não encontramos o que você precisa bucket com o valor hi necessário, então chamamos a função addAlloc, que produzirá um novo bucket, adicionando-o à estrutura do bucket.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Esta é a implementação da função b32.add. É semelhante à implementação anterior. Calculamos os 16 bits mais significativos e os 16 bits menos significativos.

Em seguida, passamos por todos os 16 bits superiores. Encontramos correspondências. E se houver uma correspondência, chamamos o método add, que consideraremos na próxima página para bucket16.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

E aqui está o nível mais baixo, que deve ser otimizado tanto quanto possível. Calculamos para uint64 valor de id no bit de fatia e também bitmask. Esta é uma máscara para um determinado valor de 64 bits, que pode ser usada para verificar a presença deste bit ou defini-lo. Verificamos se este bit está definido e definido, e retornamos presença. Esta é a nossa implementação, que nos permitiu acelerar em 10 vezes a operação de intersecção de ids de séries temporais em comparação com mapas convencionais.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Além dessa otimização, VictoriaMetrics possui muitas outras otimizações. A maioria dessas otimizações foi adicionada por um motivo, mas após a criação do perfil do código em produção.

Esta é a regra principal da otimização - não adicione otimização assumindo que haverá um gargalo aqui, porque pode acontecer que não haja um gargalo ali. A otimização geralmente degrada a qualidade do código. Portanto, vale a pena otimizar somente após a criação do perfil e de preferência na produção, para que sejam dados reais. Se alguém estiver interessado, você pode dar uma olhada no código-fonte do VictoriaMetrics e explorar outras otimizações que estão lá.

Vá otimizações em VictoriaMetrics. Alexandre Valalkin

Eu tenho uma pergunta sobre o bitset. Muito semelhante à implementação bool vetorial C++, conjunto de bits otimizado. Você fez a implementação a partir daí?

Não, não é daí. Ao implementar este bitset, fui guiado pelo conhecimento da estrutura dessas séries temporais de ids, que são utilizadas no VictoriaMetrics. E sua estrutura é tal que os 32 bits superiores são basicamente constantes. Os 32 bits inferiores estão sujeitos a alterações. Quanto menor o bit, mais frequentemente ele pode mudar. Portanto, esta implementação é otimizada especificamente para esta estrutura de dados. A implementação do C++, até onde eu sei, é otimizada para o caso geral. Se você otimizar para o caso geral, isso significa que não será o ideal para um caso específico.

Aconselho também que assista à reportagem de Alexey Milovid. Há cerca de um mês, ele falou sobre otimização no ClickHouse para especializações específicas. Ele apenas diz que, no caso geral, uma implementação C++ ou alguma outra implementação é adaptada para funcionar bem, em média, em um hospital. Pode ter um desempenho pior do que uma implementação específica de conhecimento como a nossa, onde sabemos que os 32 bits principais são em sua maioria constantes.

Eu tenho uma segunda pergunta. Qual é a diferença fundamental do InfluxDB?

Existem muitas diferenças fundamentais. Em termos de desempenho e consumo de memória, o InfluxDB em testes mostra 10 vezes mais consumo de memória para séries temporais de alta cardinalidade, quando você tem muitas delas, por exemplo, milhões. Por exemplo, VictoriaMetrics consome 1 GB por milhão de linhas ativas, enquanto o InfluxDB consome 10 GB. E isso é uma grande diferença.

A segunda diferença fundamental é que o InfluxDB possui linguagens de consulta estranhas - Flux e InfluxQL. Eles não são muito convenientes para trabalhar com séries temporais em comparação com PromQL, que é apoiado pela VictoriaMetrics. PromQL é uma linguagem de consulta do Prometheus.

E mais uma diferença é que o InfluxDB possui um modelo de dados um pouco estranho, onde cada linha pode armazenar vários campos com um conjunto diferente de tags. Essas linhas são divididas em várias tabelas. Estas complicações adicionais complicam o trabalho subsequente com esta base de dados. É difícil apoiar e compreender.

No VictoriaMetrics tudo é muito mais simples. Lá, cada série temporal é um valor-chave. O valor é um conjunto de pontos - (timestamp, value), e a chave é o conjunto label=value. Não há separação entre campos e medições. Ele permite que você selecione qualquer dado e depois combine, adicione, subtraia, multiplique, divida, ao contrário do InfluxDB, onde os cálculos entre linhas diferentes ainda não foram implementados, até onde eu sei. Mesmo que sejam implementados, é difícil, é preciso escrever muito código.

Tenho uma pergunta esclarecedora. Entendi bem que houve algum tipo de problema que você falou, que esse índice invertido não cabe na memória, então tem particionamento aí?

Primeiro, mostrei uma implementação ingênua de um índice invertido em um mapa Go padrão. Esta implementação não é adequada para bancos de dados porque esse índice invertido não é salvo em disco e o banco de dados deve ser salvo em disco para que esses dados permaneçam disponíveis na reinicialização. Nesta implementação, ao reiniciar a aplicação, seu índice invertido desaparecerá. E você perderá o acesso a todos os dados porque não conseguirá encontrá-los.

Olá! Obrigado pelo relatório! Meu nome é Pavel. Eu sou de Wildberries. Eu tenho algumas perguntas para você. Pergunta um. Você acha que se tivesse escolhido um princípio diferente ao construir a arquitetura do seu aplicativo e particionado os dados ao longo do tempo, talvez você pudesse cruzar os dados durante a pesquisa, com base apenas no fato de que uma partição contém dados para um período de tempo, ou seja, em um intervalo de tempo e você não precisaria se preocupar com o fato de suas peças estarem espalhadas de forma diferente? Pergunta número 2 - já que você está implementando um algoritmo semelhante com bitset e tudo mais, talvez tenha tentado usar instruções do processador? Talvez você tenha tentado essas otimizações?

Responderei a segunda imediatamente. Ainda não chegamos a esse ponto. Mas se for necessário, chegaremos lá. E a primeira, qual foi a pergunta?

Você discutiu dois cenários. E disseram que escolheram o segundo com uma implementação mais complexa. E não preferiram o primeiro, onde os dados são particionados por tempo.

Sim. No primeiro caso, o volume total do índice seria maior, pois em cada partição teríamos que armazenar dados duplicados para aquelas séries temporais que continuam por todas essas partições. E se a taxa de rotatividade da sua série temporal for pequena, ou seja, as mesmas séries são usadas constantemente, então no primeiro caso perderíamos muito mais na quantidade de espaço em disco ocupado em comparação com o segundo caso.

E então - sim, o particionamento de tempo é uma boa opção. Prometeu usa isso. Mas Prometheus tem outra desvantagem. Ao mesclar esses dados, ele precisa manter metainformações na memória para todos os rótulos e séries temporais. Portanto, se os dados mesclados forem grandes, o consumo de memória aumentará muito durante a mesclagem, ao contrário do VictoriaMetrics. Ao mesclar, VictoriaMetrics não consome memória alguma; apenas alguns kilobytes são consumidos, independentemente do tamanho dos dados mesclados.

O algoritmo que você está usando usa memória. Ele marca tags de série temporal que contêm valores. E desta forma você verifica a presença emparelhada em uma matriz de dados e em outra. E você entende se a interseção ocorreu ou não. Normalmente, os bancos de dados implementam cursores e iteradores que armazenam seu conteúdo atual e percorrem os dados classificados devido à simples complexidade dessas operações.

Por que não usamos cursores para percorrer os dados?

Sim.

Armazenamos linhas classificadas em LevelDB ou mergeset. Podemos mover o cursor e encontrar a interseção. Por que não usamos isso? Porque é lento. Porque os cursores significam que você precisa chamar uma função para cada linha. Uma chamada de função dura 5 nanossegundos. E se você tiver 100 milhões de linhas, gastamos meio segundo apenas chamando a função.

Existe tal coisa, sim. E minha última pergunta. A pergunta pode parecer um pouco estranha. Por que não é possível ler todos os agregados necessários no momento da chegada dos dados e salvá-los na forma exigida? Por que economizar grandes volumes em alguns sistemas como VictoriaMetrics, ClickHouse, etc., e depois gastar muito tempo com eles?

Vou dar um exemplo para ficar mais claro. Digamos como funciona um pequeno velocímetro de brinquedo? Ele registra a distância que você percorreu, sempre somando-a a um valor e na segunda vez. E divide. E obtém velocidade média. Você pode fazer a mesma coisa. Some todos os fatos necessários rapidamente.

Ok, entendi a pergunta. Seu exemplo tem seu lugar. Se você sabe de quais agregados precisa, esta é a melhor implementação. Mas o problema é que as pessoas salvam essas métricas, alguns dados no ClickHouse e ainda não sabem como irão agregá-los e filtrá-los no futuro, então têm que salvar todos os dados brutos. Mas se você sabe que precisa calcular algo em média, por que não calculá-lo em vez de armazenar vários valores brutos lá? Mas isso só acontece se você souber exatamente o que precisa.

A propósito, os bancos de dados para armazenamento de séries temporais suportam a contagem de agregados. Por exemplo, o Prometheus suporta regras de gravação. Ou seja, isso pode ser feito se você souber de quais unidades irá precisar. VictoriaMetrics ainda não tem isso, mas geralmente é precedido pelo Prometheus, no qual você pode fazer isso nas regras de recodificação.

Por exemplo, no meu trabalho anterior eu precisava contar o número de eventos em uma janela deslizante durante a última hora. O problema é que tive que fazer uma implementação customizada em Go, ou seja, um serviço para contar essa coisa. Este serviço acabou por não ser trivial, porque é difícil de calcular. A implementação pode ser simples se você precisar contar alguns agregados em intervalos de tempo fixos. Se você deseja contar eventos em uma janela deslizante, não é tão simples quanto parece. Acho que isso ainda não foi implementado no ClickHouse ou em bancos de dados de séries temporais, porque é difícil de implementar.

E mais uma pergunta. Estávamos conversando sobre média e me lembrei que já existiu algo como Grafite com backend de Carbono. E ele sabia como reduzir dados antigos, ou seja, deixar um ponto por minuto, um ponto por hora, etc. Em princípio, isso é bastante conveniente se precisarmos de dados brutos, relativamente falando, por um mês, e todo o resto puder ser diluído. Mas Prometheus e VictoriaMetrics não suportam esta funcionalidade. Está planejado apoiá-lo? Se não, por que não?

Obrigado pela pergunta. Nossos usuários fazem essa pergunta periodicamente. Eles perguntam quando adicionaremos suporte para redução da resolução. Existem vários problemas aqui. Em primeiro lugar, cada usuário entende downsampling algo diferente: alguém quer obter qualquer ponto arbitrário em um determinado intervalo, alguém quer valores máximos, mínimos e médios. Se muitos sistemas gravam dados em seu banco de dados, você não poderá agrupar tudo. Pode ser que cada sistema exija um desbaste diferente. E isso é difícil de implementar.

E a segunda coisa é que o VictoriaMetrics, assim como o ClickHouse, é otimizado para trabalhar com grandes quantidades de dados brutos, de modo que pode extrair um bilhão de linhas em menos de um segundo se você tiver muitos núcleos em seu sistema. Varredura de pontos de série temporal no VictoriaMetrics – 50 milhões de pontos por segundo por núcleo. E esse desempenho é dimensionado para os núcleos existentes. Ou seja, se você tiver 000 núcleos, por exemplo, irá digitalizar um bilhão de pontos por segundo. E esta propriedade da VictoriaMetrics e ClickHouse reduz a necessidade de redução da resolução.

Outra característica é que VictoriaMetrics compacta esses dados de forma eficaz. A compactação em média na produção é de 0,4 a 0,8 bytes por ponto. Cada ponto é um carimbo de data/hora + valor. E é compactado em menos de um byte, em média.

Sergei. Eu tenho uma pergunta. Qual é o quantum mínimo de tempo de gravação?

Um milissegundo. Recentemente conversamos com outros desenvolvedores de bancos de dados de séries temporais. O intervalo de tempo mínimo é de um segundo. E no Grafite, por exemplo, também é um segundo. No OpenTSDB também é um segundo. InfluxDB tem precisão de nanossegundos. No VictoriaMetrics é um milissegundo, porque no Prometheus é um milissegundo. E o VictoriaMetrics foi originalmente desenvolvido como armazenamento remoto para o Prometheus. Mas agora pode salvar dados de outros sistemas.

A pessoa com quem falei disse que eles têm precisão de segundo a segundo - isso é suficiente para eles porque depende do tipo de dados que estão sendo armazenados no banco de dados de série temporal. Se forem dados DevOps ou dados de infraestrutura, onde você os coleta em intervalos de 30 segundos por minuto, então uma segunda precisão é suficiente, você não precisa de nada menos. E se você coletar esses dados de sistemas de negociação de alta frequência, precisará de precisão em nanossegundos.

A precisão de milissegundos no VictoriaMetrics também é adequada para o caso DevOps e pode ser adequada para a maioria dos casos que mencionei no início do relatório. A única coisa para a qual pode não ser adequado são os sistemas de negociação de alta frequência.

Obrigado! E outra pergunta. O que é compatibilidade no PromQL?

Compatibilidade total com versões anteriores. VictoriaMetrics oferece suporte total ao PromQL. Além disso, adiciona funcionalidade avançada adicional no PromQL, chamada MétricasQL. Há uma conversa no YouTube sobre essa funcionalidade estendida. Falei no Meetup de Monitoramento na primavera em São Petersburgo.

Canal Telegram VictoriaMetrics.

Apenas usuários registrados podem participar da pesquisa. Entrarpor favor

O que está impedindo você de mudar para o VictoriaMetrics como armazenamento de longo prazo para o Prometheus? (Escreva nos comentários, vou adicioná-lo à enquete))

  • 71,4%Eu não uso Prometheus5

  • 28,6%Não sabia sobre VictoriaMetrics2

7 usuários votaram. 12 usuários se abstiveram.

Fonte: habr.com

Adicionar um comentário