Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Le sugiero que lea la transcripción del informe de finales de 2019 de Alexander Valyalkin "Optimizaciones de Go en VictoriaMetrics".

VictoriaMétricas — un DBMS rápido y escalable para almacenar y procesar datos en forma de series de tiempo (el registro forma el tiempo y un conjunto de valores correspondientes a este tiempo, por ejemplo, obtenidos mediante sondeos periódicos del estado de los sensores o recopilación de métrica).

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Aquí hay un enlace al video de este informe: https://youtu.be/MZ5P21j_HLE

Diapositivas

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Cuéntanos acerca de tí. Soy Alejandro Valyalkin. Aquí mi cuenta de GitHub. Me apasiona Go y la optimización del rendimiento. Escribí muchas bibliotecas útiles y no muy útiles. Comienzan con cualquiera fast, o con quick prefijo.

Actualmente estoy trabajando en VictoriaMetrics. ¿Qué es y qué estoy haciendo allí? Hablaré de esto en esta presentación.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

El esquema del informe es el siguiente:

  • Primero te diré qué es VictoriaMetrics.
  • Luego te diré qué son las series de tiempo.
  • Luego te contaré cómo funciona una base de datos de series temporales.
  • A continuación te hablaré de la arquitectura de la base de datos: en qué consiste.
  • Y luego pasemos a las optimizaciones que tiene VictoriaMetrics. Esta es una optimización para el índice invertido y una optimización para la implementación del conjunto de bits en Go.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

¿Alguien de la audiencia sabe qué es VictoriaMetrics? Vaya, mucha gente ya lo sabe. Es una buena noticia. Para aquellos que no lo saben, esta es una base de datos de series temporales. Se basa en la arquitectura ClickHouse, en algunos detalles de la implementación de ClickHouse. Por ejemplo, en MergeTree, cálculo paralelo en todos los núcleos de procesador disponibles y optimización del rendimiento trabajando en bloques de datos que se colocan en la memoria caché del procesador.

VictoriaMetrics proporciona una mejor compresión de datos que otras bases de datos de series temporales.

Se escala verticalmente, es decir, puede agregar más procesadores y más RAM en una computadora. VictoriaMetrics utilizará con éxito estos recursos disponibles y mejorará la productividad lineal.

VictoriaMetrics también escala horizontalmente, es decir, puede agregar nodos adicionales al clúster de VictoriaMetrics y su rendimiento aumentará casi linealmente.

Como habrás adivinado, VictoriaMetrics es una base de datos rápida, porque no puedo escribir otras. Y está escrito en Go, así que hablaré de ello en esta reunión.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

¿Quién sabe qué es una serie temporal? También conoce a mucha gente. Una serie de tiempo es una serie de pares. (timestamp, значение), donde estos pares están ordenados por tiempo. El valor es un número de coma flotante: float64.

Cada serie temporal se identifica de forma única mediante una clave. ¿En qué consiste esta clave? Consiste en un conjunto no vacío de pares clave-valor.

A continuación se muestra un ejemplo de una serie de tiempo. La clave de esta serie es una lista de pares: __name__="cpu_usage" es el nombre de la métrica, instance="my-server" - esta es la computadora en la que se recopila esta métrica, datacenter="us-east" - este es el centro de datos donde se encuentra esta computadora.

Terminamos con un nombre de serie temporal que consta de tres pares clave-valor. Esta clave corresponde a una lista de pares (timestamp, value). t1, t3, t3, ..., tN - estas son marcas de tiempo, 10, 20, 12, ..., 15 — los valores correspondientes. Este es el uso de CPU en un momento dado para una serie determinada.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

¿Dónde se pueden utilizar las series temporales? ¿Alguien tiene alguna idea?

  • En DevOps, puedes medir CPU, RAM, red, rps, cantidad de errores, etc.
  • IoT: podemos medir la temperatura, la presión, las coordenadas geográficas y algo más.
  • También finanzas: podemos controlar los precios de todo tipo de acciones y divisas.
  • Además, las series temporales se pueden utilizar para monitorear los procesos de producción en las fábricas. Tenemos usuarios que utilizan VictoriaMetrics para monitorear turbinas eólicas, para robots.
  • Las series temporales también son útiles para recopilar información de sensores de varios dispositivos. Por ejemplo, para un motor; para medir la presión de los neumáticos; para medir velocidad, distancia; para medir el consumo de gasolina, etc.
  • Las series temporales también se pueden utilizar para monitorear aeronaves. Cada avión tiene una caja negra que recopila series temporales de varios parámetros del estado del avión. Las series de tiempo también se utilizan en la industria aeroespacial.
  • La atención sanitaria es la presión arterial, el pulso, etc.

Puede que haya más aplicaciones de las que me olvidé, pero espero que entiendas que las series temporales se utilizan activamente en el mundo moderno. Y el volumen de su uso crece cada año.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

¿Por qué necesita una base de datos de series temporales? ¿Por qué no se pueden utilizar una base de datos relacional normal para almacenar series de tiempo?

Porque las series temporales suelen contener una gran cantidad de información, que es difícil de almacenar y procesar en bases de datos convencionales. Por ello, aparecieron bases de datos especializadas en series temporales. Estas bases almacenan puntos de manera efectiva. (timestamp, value) con la clave dada. Proporcionan una API para leer datos almacenados por clave, por un único par clave-valor, por múltiples pares clave-valor o por expresión regular. Por ejemplo, si desea encontrar la carga de CPU de todos sus servicios en un centro de datos en Estados Unidos, entonces debe utilizar esta pseudoconsulta.

Normalmente, las bases de datos de series temporales proporcionan lenguajes de consulta especializados porque SQL de series temporales no es muy adecuado. Aunque existen bases de datos que soportan SQL, no es muy adecuado. Lenguajes de consulta como PromQL, InflujoQL, Flujo, Q. Espero que alguien haya escuchado al menos uno de estos idiomas. Probablemente mucha gente haya oído hablar de PromQL. Este es el lenguaje de consulta de Prometheus.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Así es como se ve una arquitectura de base de datos de series temporales moderna utilizando VictoriaMetrics como ejemplo.

Está formado por dos partes. Este es el almacenamiento para el índice invertido y el almacenamiento para valores de series temporales. Estos repositorios están separados.

Cuando llega un nuevo registro a la base de datos, primero accedemos al índice invertido para encontrar el identificador de serie temporal para un conjunto determinado. label=value para una métrica determinada. Encontramos este identificador y guardamos el valor en el almacén de datos.

Cuando llega una solicitud para recuperar datos de TSDB, primero vamos al índice invertido. consigamos todo timeseries_ids registros que coinciden con este conjunto label=value. Y luego obtenemos todos los datos necesarios del almacén de datos, indexados por timeseries_ids.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Veamos un ejemplo de cómo una base de datos de series temporales procesa una consulta de selección entrante.

  • Primero que nada ella consigue todo timeseries_ids de un índice invertido que contiene los pares dados label=value, o satisfacer una expresión regular dada.
  • Luego recupera todos los puntos de datos del almacenamiento de datos en un intervalo de tiempo determinado para los encontrados. timeseries_ids.
  • Después de esto, la base de datos realiza algunos cálculos sobre estos puntos de datos, según la solicitud del usuario. Y luego de eso devuelve la respuesta.

En esta presentación les hablaré de la primera parte. esta es una busqueda timeseries_ids por índice invertido. Puedes ver la segunda parte y la tercera parte más tarde. Fuentes de VictoriaMetrics, o esperar hasta que prepare otros informes :)

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Pasemos al índice invertido. Muchos pueden pensar que esto es simple. ¿Quién sabe qué es un índice invertido y cómo funciona? Oh, ya no hay tanta gente. Intentemos entender qué es.

En realidad es simple. Es simplemente un diccionario que asigna una clave a un valor. ¿Qué es una clave? esta pareja label=valueDonde label и value - estas son líneas. Y los valores son un conjunto. timeseries_ids, que incluye el par dado label=value.

El índice invertido le permite encontrar todo rápidamente timeseries_ids, que han dado label=value.

También le permite encontrar rápidamente timeseries_ids series de tiempo para varios pares label=value, o para parejas label=regexp. ¿Como sucedió esto? Al encontrar la intersección del conjunto timeseries_ids para cada par label=value.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Veamos varias implementaciones del índice invertido. Comencemos con la implementación ingenua más simple. Ella se parece a esto.

Función getMetricIDs obtiene una lista de cadenas. Cada línea contiene label=value. Esta función devuelve una lista metricIDs.

¿Cómo funciona? Aquí tenemos una variable global llamada invertedIndex. Este es un diccionario normal (map), que asignará la cadena para dividir entradas. La línea contiene label=value.

Implementación de la función: obtener metricIDs Por el primero label=value, luego pasamos por todo lo demás label=value, lo entendemos metricIDs para ellos. Y llama a la función intersectInts, que se discutirá a continuación. Y esta función devuelve la intersección de estas listas.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Como puedes ver, implementar un índice invertido no es muy complicado. Pero esta es una implementación ingenua. ¿Qué desventajas tiene? La principal desventaja de la implementación ingenua es que dicho índice invertido se almacena en la RAM. Después de reiniciar la aplicación perdemos este índice. No se puede guardar este índice en el disco. Es poco probable que un índice invertido de este tipo sea adecuado para una base de datos.

El segundo inconveniente también está relacionado con la memoria. El índice invertido debe caber en la RAM. Si excede el tamaño de la RAM, obviamente obtendremos un error de falta de memoria. Y el programa no funcionará.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Este problema se puede resolver utilizando soluciones ya preparadas como NivelDBO RocasDB.

En definitiva, necesitamos una base de datos que nos permita hacer tres operaciones rápidamente.

  • La primera operación es grabar. ключ-значение a esta base de datos. Ella hace esto muy rápidamente, donde ключ-значение son cadenas arbitrarias.
  • La segunda operación es una búsqueda rápida de un valor utilizando una clave determinada.
  • Y la tercera operación es una búsqueda rápida de todos los valores mediante un prefijo determinado.

LevelDB y RocksDB: estas bases de datos fueron desarrolladas por Google y Facebook. Primero vino LevelDB. Luego los chicos de Facebook tomaron LevelDB y comenzaron a mejorarlo, crearon RocksDB. Ahora casi todas las bases de datos internas funcionan en RocksDB dentro de Facebook, incluidas aquellas que se han transferido a RocksDB y MySQL. lo nombraron MisRocas.

Se puede implementar un índice invertido usando LevelDB. ¿Cómo hacerlo? Guardamos como clave. label=value. Y el valor es el identificador de la serie temporal donde está presente el par. label=value.

Si tenemos muchas series de tiempo con un par determinado label=value, entonces habrá muchas filas en esta base de datos con la misma clave y diferentes timeseries_ids. Para obtener una lista de todos timeseries_ids, que comienzan con esto label=prefix, hacemos un escaneo de rango para el cual esta base de datos está optimizada. Es decir, seleccionamos todas las líneas que comienzan con label=prefix y conseguir lo necesario timeseries_ids.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Aquí hay una implementación de muestra de cómo se vería en Go. Tenemos un índice invertido. Este es LevelDB.

La función es la misma que para la implementación ingenua. Repite la ingenua implementación casi línea por línea. El único punto es que en lugar de recurrir a map accedemos al índice invertido. Obtenemos todos los valores para el primero. label=value. Luego revisamos todos los pares restantes. label=value y obtenga los conjuntos correspondientes de metricID para ellos. Luego encontramos la intersección.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Todo parece estar bien, pero esta solución tiene sus inconvenientes. VictoriaMetrics implementó inicialmente un índice invertido basado en LevelDB. Pero al final tuve que dejarlo.

¿Por qué? Porque LevelDB es más lento que la implementación ingenua. En una implementación ingenua, dada una clave determinada, recuperamos inmediatamente el segmento completo metricIDs. Esta es una operación muy rápida: toda la rebanada está lista para usar.

En LevelDB, cada vez que se llama a una función GetValues necesitas pasar por todas las líneas que comienzan con label=value. Y obtener el valor de cada línea. timeseries_ids. De tal timeseries_ids recoge una porción de estos timeseries_ids. Obviamente, esto es mucho más lento que simplemente acceder a un mapa normal mediante una clave.

El segundo inconveniente es que LevelDB está escrito en C. Llamar a funciones C desde Go no es muy rápido. Se necesitan cientos de nanosegundos. Esto no es muy rápido, porque en comparación con una llamada a función normal escrita en go, que tarda entre 1 y 5 nanosegundos, la diferencia en el rendimiento es decenas de veces. Para VictoriaMetrics esto fue un defecto fatal :)

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Entonces escribí mi propia implementación del índice invertido. Y él la llamó fusionar.

Mergeset se basa en la estructura de datos MergeTree. Esta estructura de datos está tomada de ClickHouse. Obviamente, mergeset debería optimizarse para una búsqueda rápida. timeseries_ids según la clave dada. Mergeset está escrito completamente en Go. Puedes ver Fuentes de VictoriaMetrics en GitHub. La implementación de mergeset está en la carpeta. /lib/mergeset. Puedes intentar descubrir qué está pasando allí.

La API mergeset es muy similar a LevelDB y RocksDB. Es decir, le permite guardar rápidamente nuevos registros allí y seleccionar rápidamente registros con un prefijo determinado.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Hablaremos sobre las desventajas de mergeset más adelante. Ahora hablemos de los problemas que surgieron con VictoriaMetrics en producción al implementar un índice invertido.

¿Por qué surgieron?

La primera razón es la alta tasa de abandono. Traducido al ruso, se trata de un cambio frecuente en las series temporales. Aquí es cuando termina una serie temporal y comienza una nueva, o comienzan muchas series temporales nuevas. Y esto sucede a menudo.

La segunda razón es la gran cantidad de series temporales. Al principio, cuando la monitorización estaba ganando popularidad, el número de series temporales era pequeño. Por ejemplo, para cada computadora necesita monitorear la CPU, la memoria, la red y la carga del disco. 4 series de tiempo por computadora. Digamos que tienes 100 computadoras y 400 series temporales. Esto es muy poco.

Con el tiempo, la gente descubrió que podían medir información más granular. Por ejemplo, mida la carga no de todo el procesador, sino de cada núcleo del procesador por separado. Si tiene 40 núcleos de procesador, entonces tendrá 40 veces más series de tiempo para medir la carga del procesador.

Pero eso no es todo. Cada núcleo de procesador puede tener varios estados, como inactivo, cuando está inactivo. Y también trabajar en el espacio del usuario, trabajar en el espacio del kernel y otros estados. Y cada uno de esos estados también se puede medir como una serie de tiempo separada. Esto además aumenta el número de filas entre 7 y 8 veces.

De una métrica obtuvimos 40 x 8 = 320 métricas para una sola computadora. Multiplicamos por 100 y obtenemos 32 en lugar de 000.

Luego apareció Kubernetes. Y empeoró porque Kubernetes puede alojar muchos servicios diferentes. Cada servicio de Kubernetes consta de muchos pods. Y todo esto necesita ser monitoreado. Además, contamos con un constante despliegue de nuevas versiones de sus servicios. Para cada nueva versión, se deben crear nuevas series temporales. Como resultado, el número de series temporales crece exponencialmente y nos enfrentamos al problema de una gran cantidad de series temporales, lo que se denomina alta cardinalidad. VictoriaMetrics lo afronta con éxito en comparación con otras bases de datos de series temporales.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Echemos un vistazo más de cerca a la alta tasa de abandono. ¿Qué causa una alta tasa de abandono en la producción? Porque algunos significados de etiquetas y rótulos cambian constantemente.

Por ejemplo, tomemos Kubernetes, que tiene el concepto deployment, es decir, cuando se implementa una nueva versión de su aplicación. Por alguna razón, los desarrolladores de Kubernetes decidieron agregar la identificación de implementación a la etiqueta.

¿A qué condujo esto? Además, con cada nueva implementación, todas las series temporales antiguas se interrumpen y, en lugar de ellas, las series temporales nuevas comienzan con un nuevo valor de etiqueta. deployment_id. Puede haber cientos de miles e incluso millones de filas de este tipo.

Lo importante de todo esto es que el número total de series temporales crece, pero el número de series temporales que actualmente están activas y recibiendo datos se mantiene constante. Este estado se llama tasa de abandono alta.

El principal problema de una alta tasa de abandono es garantizar una velocidad de búsqueda constante para todas las series temporales para un conjunto determinado de etiquetas durante un intervalo de tiempo determinado. Normalmente, este es el intervalo de tiempo de la última hora o del último día.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

¿Cómo resolver este problema? Aquí está la primera opción. Esto es para dividir el índice invertido en partes independientes a lo largo del tiempo. Es decir, transcurrido un intervalo de tiempo, terminamos de trabajar con el índice invertido actual. Y crea un nuevo índice invertido. Pasa otro intervalo de tiempo, creamos otro y otro.

Y al tomar muestras de estos índices invertidos, encontramos un conjunto de índices invertidos que caen dentro del intervalo dado. Y, en consecuencia, seleccionamos la identificación de la serie temporal desde allí.

Esto ahorra recursos porque no tenemos que mirar piezas que no se encuentran dentro del intervalo dado. Es decir, normalmente, si seleccionamos datos de la última hora, para intervalos de tiempo anteriores omitimos las solicitudes.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Existe otra opción para solucionar este problema. Esto es para almacenar para cada día una lista separada de identificadores de series temporales que ocurrieron ese día.

La ventaja de esta solución sobre la solución anterior es que no duplicamos información de series temporales que no desaparece con el tiempo. Están constantemente presentes y no cambian.

La desventaja es que dicha solución es más difícil de implementar y de depurar. Y VictoriaMetrics eligió esta solución. Así sucedió históricamente. Esta solución también funciona bien en comparación con la anterior. Porque esta solución no se implementó debido a que es necesario duplicar datos en cada partición para series temporales que no cambian, es decir, que no desaparecen con el tiempo. VictoriaMetrics se optimizó principalmente para el consumo de espacio en disco y la implementación anterior empeoró el consumo de espacio en disco. Pero esta implementación es más adecuada para minimizar el consumo de espacio en disco, por eso se eligió.

Tuve que luchar contra ella. La lucha fue que en esta implementación aún es necesario elegir un número mucho mayor timeseries_ids para datos que cuando el índice invertido tiene partición de tiempo.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

¿Cómo solucionamos este problema? Lo resolvimos de una manera original: almacenando varios identificadores de series temporales en cada entrada del índice invertido en lugar de un identificador. Es decir, tenemos una clave. label=value, que ocurre en todas las series de tiempo. Y ahora salvamos varios. timeseries_ids en una sola entrada.

He aquí un ejemplo. Anteriormente teníamos N entradas, pero ahora tenemos una entrada cuyo prefijo es el mismo que todas las demás. Para la entrada anterior, el valor contiene todos los identificadores de series temporales.

Esto hizo posible aumentar la velocidad de escaneo de dicho índice invertido hasta 10 veces. Y nos permitió reducir el consumo de memoria para el caché, porque ahora almacenamos la cadena. label=value solo una vez en el caché juntos N veces. Y esta línea puede ser grande si almacena líneas largas en sus etiquetas y etiquetas, que a Kubernetes le gusta colocar allí.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Otra opción para acelerar la búsqueda en un índice invertido es la fragmentación. Crear varios índices invertidos en lugar de uno y dividir los datos entre ellos por clave. Este es un conjunto key=value vapor. Es decir, obtenemos varios índices invertidos independientes, que podemos consultar en paralelo en varios procesadores. Las implementaciones anteriores solo permitían la operación en modo de procesador único, es decir, escanear datos en un solo núcleo. Esta solución le permite escanear datos en varios núcleos a la vez, como le gusta hacer a ClickHouse. Esto es lo que planeamos implementar.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Ahora volvamos a nuestra oveja: a la función de intersección. timeseries_ids. Consideremos qué implementaciones puede haber. Esta función le permite encontrar timeseries_ids para un conjunto dado label=value.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

La primera opción es una implementación ingenua. Dos bucles anidados. Aquí obtenemos la entrada de la función. intersectInts dos rodajas - a и b. En la salida, debería devolvernos la intersección de estos sectores.

Una implementación ingenua se ve así. Iteramos sobre todos los valores del segmento. a, dentro de este bucle repasamos todos los valores de corte b. Y los comparamos. Si coinciden, entonces hemos encontrado una intersección. Y guárdalo en result.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

¿Cuales son las desventajas? La complejidad cuadrática es su principal inconveniente. Por ejemplo, si sus dimensiones son rebanadas a и b un millón a la vez, entonces esta función nunca le devolverá una respuesta. Porque será necesario realizar un billón de iteraciones, lo cual es mucho incluso para las computadoras modernas.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

La segunda implementación se basa en el mapa. Creamos mapa. Ponemos todos los valores del corte en este mapa. a. Luego pasamos por el segmento en un bucle separado. b. Y comprobamos si este valor es del segmento. b en el mapa. Si existe, agréguelo al resultado.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

¿Cuales son los beneficios? La ventaja es que sólo hay complejidad lineal. Es decir, la función se ejecutará mucho más rápido para porciones más grandes. Para una porción de tamaño de un millón, esta función se ejecutará en 2 millones de iteraciones, a diferencia de los billones de iteraciones de la función anterior.

La desventaja es que esta función requiere más memoria para crear este mapa.

El segundo inconveniente es la gran sobrecarga del hash. Este inconveniente no es muy obvio. Y para nosotros tampoco era muy obvio, por eso al principio en VictoriaMetrics la implementación de la intersección era a través de un mapa. Pero luego la elaboración de perfiles mostró que el tiempo del procesador principal se dedica a escribir en el mapa y verificar la presencia de un valor en este mapa.

¿Por qué se pierde tiempo de CPU en estos lugares? Porque Go realiza una operación hash en estas líneas. Es decir, calcula el hash de la clave para luego acceder a ella en un índice determinado en el HashMap. La operación de cálculo hash se completa en decenas de nanosegundos. Esto es lento para VictoriaMetrics.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Decidí implementar un conjunto de bits optimizado específicamente para este caso. Así es como se ve ahora la intersección de dos cortes. Aquí creamos un conjunto de bits. Le agregamos elementos del primer segmento. Luego comprobamos la presencia de estos elementos en el segundo segmento. Y agrégalos al resultado. Es decir, casi no se diferencia del ejemplo anterior. Lo único aquí es que reemplazamos el acceso al mapa con funciones personalizadas. add и has.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

A primera vista, parece que esto debería funcionar más lento, si antes se usaba un mapa estándar y luego se llamaban otras funciones, pero el perfilado muestra que esto funciona 10 veces más rápido que el mapa estándar en el caso de VictoriaMetrics.

Además, utiliza mucha menos memoria en comparación con la implementación del mapa. Porque aquí almacenamos bits en lugar de valores de ocho bytes.

La desventaja de esta implementación es que no es tan obvia ni trivial.

Otro inconveniente que muchos quizás no noten es que esta implementación puede no funcionar bien en algunos casos. Es decir, está optimizado para un caso específico, para este caso de intersección de identificadores de series temporales de VictoriaMetrics. Esto no significa que sea adecuado para todos los casos. Si se utiliza incorrectamente no obtendremos un aumento de rendimiento, sino un error de falta de memoria y una ralentización del rendimiento.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Consideremos la implementación de esta estructura. Si quieres mirar, se encuentra en las fuentes de VictoriaMetrics, en la carpeta lib/uint64set. Está optimizado específicamente para el caso de VictoriaMetrics, donde timeseries_id es un valor de 64 bits, donde los primeros 32 bits son básicamente constantes y solo cambian los últimos 32 bits.

Esta estructura de datos no se almacena en el disco, solo opera en la memoria.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Aquí está su API. No es muy complicado. La API está diseñada específicamente para un ejemplo específico de uso de VictoriaMetrics. Es decir, aquí no hay funciones innecesarias. Estas son las funciones que VictoriaMetrics utiliza explícitamente.

hay funciones add, lo que añade nuevos valores. Hay una función has, que busca nuevos valores. Y hay una función del, que elimina valores. Hay una función de ayuda. len, que devuelve el tamaño del conjunto. Función clone clona mucho. y función appendto convierte este conjunto en corte timeseries_ids.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Así es como se ve la implementación de esta estructura de datos. conjunto tiene dos elementos:

  • ItemsCount es un campo auxiliar para devolver rápidamente el número de elementos de un conjunto. Sería posible prescindir de este campo auxiliar, pero tuvo que agregarse aquí porque VictoriaMetrics a menudo consulta la longitud del conjunto de bits en sus algoritmos.

  • El segundo campo es buckets. Este es un corte de la estructura. bucket32. Cada estructura almacena hi campo. Estos son los 32 bits superiores. Y dos rebanadas b16his и buckets de bucket16 estructuras.

Aquí se almacenan los 16 bits superiores de la segunda parte de la estructura de 64 bits. Y aquí se almacenan conjuntos de bits para los 16 bits inferiores de cada byte.

Bucket64 consiste en una matriz uint64. La longitud se calcula utilizando estas constantes. En uno bucket16 máximo se puede almacenar 2^16=65536 poco. Si lo divides por 8, son 8 kilobytes. si divides por 8 nuevamente da 1000 uint64 significado. Eso es Bucket16 – esta es nuestra estructura de 8 kilobytes.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Veamos cómo se implementa uno de los métodos de esta estructura para agregar un nuevo valor.

todo comienza con uint64 significados. Calculamos los 32 bits superiores, calculamos los 32 bits inferiores. repasemos todo buckets. Comparamos los 32 bits superiores de cada depósito con el valor agregado. Y si coinciden, entonces llamamos a la función. add en la estructura b32 buckets. Y agregue los 32 bits inferiores allí. Y si volviera true, entonces esto significa que agregamos ese valor allí y no teníamos ese valor. si regresa false, entonces ese significado ya existía. Luego aumentamos el número de elementos de la estructura.

Si no hemos encontrado el que necesitas bucket con el valor alto requerido, luego llamamos a la función addAlloc, que producirá uno nuevo bucket, agregándolo a la estructura del depósito.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Esta es la implementación de la función. b32.add. Es similar a la implementación anterior. Calculamos los 16 bits más significativos y los 16 bits menos significativos.

Luego revisamos los 16 bits superiores. Encontramos coincidencias. Y si hay una coincidencia, llamamos al método add, que consideraremos en la página siguiente para bucket16.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Y aquí está el nivel más bajo, que conviene optimizar al máximo. Calculamos para uint64 valor de identificación en bit de corte y también bitmask. Esta es una máscara para un valor determinado de 64 bits, que se puede usar para verificar la presencia de este bit o configurarlo. Comprobamos si este bit está establecido, lo configuramos y devolvemos presencia. Esta es nuestra implementación, que nos permitió acelerar la operación de intersección de identificadores de series temporales 10 veces en comparación con los mapas convencionales.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Además de esta optimización, VictoriaMetrics tiene muchas otras optimizaciones. La mayoría de estas optimizaciones se agregaron por una razón, pero después de perfilar el código en producción.

Esta es la regla principal de optimización: no agregue optimización suponiendo que habrá un cuello de botella aquí, porque puede resultar que no habrá un cuello de botella allí. La optimización suele degradar la calidad del código. Por lo tanto, vale la pena optimizar solo después de la elaboración del perfil y preferiblemente en producción, para que se trate de datos reales. Si alguien está interesado, puede consultar el código fuente de VictoriaMetrics y explorar otras optimizaciones que existen.

Vaya optimizaciones en VictoriaMetrics. Alexander Valyalkin

Tengo una pregunta sobre bitset. Muy similar a la implementación de vector bool de C++, conjunto de bits optimizado. ¿Tomaste la implementación desde allí?

No, de ahí no. Al implementar este conjunto de bits, me guié por el conocimiento de la estructura de estas series temporales de identificadores, que se utilizan en VictoriaMetrics. Y su estructura es tal que los 32 bits superiores son básicamente constantes. Los 32 bits inferiores están sujetos a cambios. Cuanto más baja sea la broca, más a menudo podrá cambiar. Por lo tanto, esta implementación está optimizada específicamente para esta estructura de datos. La implementación de C++, hasta donde yo sé, está optimizada para el caso general. Si optimiza para el caso general, esto significa que no será el más óptimo para un caso específico.

También te aconsejo que mires el informe de Alexey Milovid. Hace aproximadamente un mes, habló sobre la optimización en ClickHouse para especializaciones específicas. Simplemente dice que, en el caso general, una implementación de C++ o alguna otra implementación está diseñada para funcionar bien en promedio en un hospital. Puede funcionar peor que una implementación de conocimiento específico como la nuestra, donde sabemos que los 32 bits superiores son en su mayoría constantes.

Tengo una segunda pregunta. ¿Cuál es la diferencia fundamental con InfluxDB?

Hay muchas diferencias fundamentales. En términos de rendimiento y consumo de memoria, InfluxDB en las pruebas muestra un consumo de memoria 10 veces mayor para series temporales de alta cardinalidad, cuando hay muchas, por ejemplo, millones. Por ejemplo, VictoriaMetrics consume 1 GB por millón de filas activas, mientras que InfluxDB consume 10 GB. Y esa es una gran diferencia.

La segunda diferencia fundamental es que InfluxDB tiene lenguajes de consulta extraños: Flux e InfluxQL. No son muy convenientes para trabajar con series temporales en comparación con PromQL, que cuenta con el respaldo de VictoriaMetrics. PromQL es un lenguaje de consulta de Prometheus.

Y una diferencia más es que InfluxDB tiene un modelo de datos un poco extraño, donde cada línea puede almacenar varios campos con un conjunto diferente de etiquetas. Estas líneas se dividen a su vez en varias tablas. Estas complicaciones adicionales complican el trabajo posterior con esta base de datos. Es difícil de apoyar y comprender.

En VictoriaMetrics todo es mucho más sencillo. Allí, cada serie temporal es un valor clave. El valor es un conjunto de puntos. (timestamp, value), y la clave es el conjunto label=value. No hay separación entre campos y medidas. Le permite seleccionar cualquier dato y luego combinar, sumar, restar, multiplicar, dividir, a diferencia de InfluxDB donde, hasta donde yo sé, los cálculos entre diferentes filas aún no se implementan. Incluso si se implementan, es difícil, hay que escribir mucho código.

Tengo una pregunta aclaratoria. ¿Entendí correctamente que hubo algún tipo de problema del que usted habló, que este índice invertido no cabe en la memoria, por lo que hay una partición allí?

Primero, mostré una implementación ingenua de un índice invertido en un mapa Go estándar. Esta implementación no es adecuada para bases de datos porque este índice invertido no se guarda en el disco y la base de datos debe guardarse en el disco para que estos datos permanezcan disponibles al reiniciar. En esta implementación, cuando reinicie la aplicación, su índice invertido desaparecerá. Y perderás el acceso a todos los datos porque no podrás encontrarlos.

¡Hola! ¡Gracias por el informe! Mi nombre es Pablo. Soy de Wildberries. Tengo algunas preguntas para ti. Pregunta uno. ¿Cree que si hubiera elegido un principio diferente al construir la arquitectura de su aplicación y hubiera particionado los datos a lo largo del tiempo, entonces tal vez habría podido cruzar datos durante la búsqueda, basándose únicamente en el hecho de que una partición contiene datos para una ¿Período de tiempo, es decir, en un intervalo de tiempo y no tendrías que preocuparte por el hecho de que tus piezas estén dispersas de manera diferente? Pregunta número 2: dado que está implementando un algoritmo similar con bitset y todo lo demás, ¿tal vez intentó usar las instrucciones del procesador? ¿Quizás has probado este tipo de optimizaciones?

Responderé a la segunda de inmediato. Aún no hemos llegado a ese punto. Pero si es necesario, llegaremos allí. Y la primera, ¿cuál era la pregunta?

Discutiste dos escenarios. Y dijeron que eligieron el segundo con una implementación más compleja. Y no prefirieron el primero, donde los datos están divididos por tiempo.

Sí. En el primer caso, el volumen total del índice sería mayor, porque en cada partición tendríamos que almacenar datos duplicados de aquellas series temporales que continúan por todas esas particiones. Y si la tasa de abandono de su serie temporal es pequeña, es decir, se utilizan constantemente las mismas series, entonces en el primer caso perderíamos mucho más en la cantidad de espacio ocupado en disco en comparación con el segundo caso.

Y entonces, sí, la partición del tiempo es una buena opción. Prometeo lo usa. Pero Prometheus tiene otro inconveniente. Al fusionar estos datos, es necesario mantener en la memoria metainformación para todas las etiquetas y series temporales. Por lo tanto, si los datos que fusiona son grandes, entonces el consumo de memoria aumenta mucho durante la fusión, a diferencia de VictoriaMetrics. Al fusionar, VictoriaMetrics no consume memoria en absoluto; solo se consumen un par de kilobytes, independientemente del tamaño de los datos fusionados.

El algoritmo que estás utilizando utiliza memoria. Marca etiquetas de series temporales que contienen valores. Y de esta manera verifica la presencia de pares en una matriz de datos y en otra. Y comprenderá si se produjo una intersección o no. Normalmente, las bases de datos implementan cursores e iteradores que almacenan su contenido actual y ejecutan los datos ordenados debido a la simple complejidad de estas operaciones.

¿Por qué no usamos cursores para recorrer datos?

Sí.

Almacenamos filas ordenadas en LevelDB o mergeset. Podemos mover el cursor y encontrar la intersección. ¿Por qué no lo usamos? Porque es lento. Porque los cursores significan que necesitas llamar a una función para cada línea. Una llamada a función dura 5 nanosegundos. Y si tienes 100 de líneas, resulta que dedicamos medio segundo a llamar a la función.

Existe tal cosa, sí. Y mi última pregunta. La pregunta puede parecer un poco extraña. ¿Por qué no es posible leer todos los agregados necesarios en el momento en que llegan los datos y guardarlos en el formato requerido? ¿Por qué ahorrar grandes volúmenes en algunos sistemas como VictoriaMetrics, ClickHouse, etc. y luego dedicarles mucho tiempo?

Daré un ejemplo para que quede más claro. Digamos ¿cómo funciona un velocímetro de juguete pequeño? Registra la distancia recorrida, siempre sumándola a un valor y al segundo, la vez. Y divide. Y obtiene velocidad promedio. Puedes hacer más o menos lo mismo. Sume todos los datos necesarios sobre la marcha.

Vale, entiendo la pregunta. Tu ejemplo tiene su lugar. Si sabe qué agregados necesita, esta es la mejor implementación. Pero el problema es que las personas guardan estas métricas, algunos datos en ClickHouse y aún no saben cómo los agregarán y filtrarán en el futuro, por lo que tienen que guardar todos los datos sin procesar. Pero si sabe que necesita calcular algo en promedio, ¿por qué no calcularlo en lugar de almacenar un montón de valores sin procesar allí? Pero esto es sólo si sabes exactamente lo que necesitas.

Por cierto, las bases de datos para almacenar series de tiempo admiten el recuento de agregados. Por ejemplo, Prometheus apoya reglas de grabación. Es decir, esto se puede hacer si sabes qué unidades necesitarás. VictoriaMetrics aún no tiene esto, pero generalmente está precedido por Prometheus, en el que esto se puede hacer en las reglas de recodificación.

Por ejemplo, en mi trabajo anterior necesitaba contar la cantidad de eventos en una ventana deslizante durante la última hora. El problema es que tuve que hacer una implementación personalizada en Go, es decir, un servicio para contar esto. Al final, este servicio no fue trivial porque es difícil de calcular. La implementación puede ser sencilla si necesita contar algunos agregados en intervalos de tiempo fijos. Si desea contar eventos en una ventana deslizante, entonces no es tan simple como parece. Creo que esto aún no se ha implementado en ClickHouse o en bases de datos de series temporales, porque es difícil de implementar.

Y una pregunta más. Estábamos hablando de promediar y recordé que una vez existió el grafito con un backend de carbono. Y sabía cómo adelgazar los datos antiguos, es decir, dejar un punto por minuto, un punto por hora, etc. En principio, esto es bastante conveniente si necesitamos datos sin procesar, relativamente hablando, durante un mes, y todo lo demás puede ser diluido. Pero Prometheus y VictoriaMetrics no admiten esta funcionalidad. ¿Está previsto apoyarlo? ¿Si no, porque no?

Gracias por la pregunta. Nuestros usuarios hacen esta pregunta periódicamente. Preguntan cuándo agregaremos soporte para reducción de resolución. Hay varios problemas aquí. En primer lugar, todo usuario comprende downsampling algo diferente: alguien quiere obtener cualquier punto arbitrario en un intervalo dado, alguien quiere valores máximos, mínimos y promedio. Si muchos sistemas escriben datos en su base de datos, entonces no podrá agruparlos todos. Puede ser que cada sistema requiera un adelgazamiento diferente. Y esto es difícil de implementar.

Y la segunda cosa es que VictoriaMetrics, al igual que ClickHouse, está optimizado para trabajar con grandes cantidades de datos sin procesar, por lo que puede procesar mil millones de líneas en menos de un segundo si tiene muchos núcleos en su sistema. Escaneo de puntos de series de tiempo en VictoriaMetrics: 50 de puntos por segundo por núcleo. Y este rendimiento se adapta a los núcleos existentes. Es decir, si tienes 000 núcleos, por ejemplo, escanearás mil millones de puntos por segundo. Y esta propiedad de VictoriaMetrics y ClickHouse reduce la necesidad de reducir la resolución.

Otra característica es que VictoriaMetrics comprime estos datos de forma eficaz. La compresión promedio en producción es de 0,4 a 0,8 bytes por punto. Cada punto es una marca de tiempo + valor. Y está comprimido en menos de un byte en promedio.

Serguéi. Tengo una pregunta. ¿Cuál es el tiempo mínimo de grabación?

Un milisegundo. Recientemente tuvimos una conversación con otros desarrolladores de bases de datos de series temporales. Su intervalo de tiempo mínimo es de un segundo. Y en Graphite, por ejemplo, también es un segundo. En OpenTSDB también es un segundo. InfluxDB tiene una precisión de nanosegundos. En VictoriaMetrics es un milisegundo, porque en Prometheus es un milisegundo. Y VictoriaMetrics se desarrolló originalmente como almacenamiento remoto para Prometheus. Pero ahora puede guardar datos de otros sistemas.

La persona con la que hablé dice que tiene una precisión de segundo a segundo; eso es suficiente para ellos porque depende del tipo de datos que se almacenan en la base de datos de series temporales. Si se trata de datos de DevOps o datos de la infraestructura, donde los recopila a intervalos de 30 segundos por minuto, entonces la precisión de un segundo es suficiente, no necesita nada menos. Y si recopila estos datos de sistemas comerciales de alta frecuencia, entonces necesitará una precisión de nanosegundos.

La precisión de milisegundos en VictoriaMetrics también es adecuada para el caso de DevOps y puede ser adecuada para la mayoría de los casos que mencioné al principio del informe. Lo único para lo que puede no ser adecuado son los sistemas comerciales de alta frecuencia.

¡Gracias! Y otra pregunta. ¿Qué es la compatibilidad en PromQL?

Compatibilidad total con versiones anteriores. VictoriaMetrics es totalmente compatible con PromQL. Además, agrega una funcionalidad avanzada adicional en PromQL, que se llama MétricasQL. Hay una charla en YouTube sobre esta funcionalidad ampliada. Hablé en la Reunión de Monitoreo en la primavera en San Petersburgo.

Canal de Telegram VictoriaMétricas.

Solo los usuarios registrados pueden participar en la encuesta. Registrarsepor favor

¿Qué le impide cambiar a VictoriaMetrics como almacenamiento a largo plazo para Prometheus? (Escribe en los comentarios, lo agregaré a la encuesta))

  • 71,4%No uso Prometheus5

  • 28,6%No conocía VictoriaMetrics2

7 usuarios votaron. 12 usuarios se abstuvieron.

Fuente: habr.com

Añadir un comentario