Introducción
Conceptos de ClickHouse
| Data type | Table |
|---|---|
| Logs | otel_logs |
| Trazas | otel_traces |
| Métricas (gauges) | otel_metrics_gauge |
| Métricas (sumas) | otel_metrics_sum |
| Métricas (histograma) | otel_metrics_histogram |
| Métricas (histogramas exponenciales) | otel_metrics_exponentialhistogram |
| Métricas (resumen) | otel_metrics_summary |
| Sesiones | hyperdx_sessions |
default; esto se puede modificar en el collector de OpenTelemetry.
Como mínimo, deberías comprender los siguientes conceptos fundamentales de ClickHouse:
| Concepto | Descripción |
|---|---|
| Tablas | Cómo las fuentes de datos en ClickStack se corresponden con las tablas subyacentes de ClickHouse. Las tablas en ClickHouse usan principalmente el motor MergeTree. |
| Partes | Cómo los datos se escriben en partes inmutables y se fusionan con el tiempo. |
| Particiones | Las particiones agrupan las partes de una tabla en unidades lógicas organizadas. Estas unidades son más fáciles de administrar, consultar y optimizar. |
| Merges | El proceso interno que fusiona partes para reducir la cantidad de partes que hay que consultar. Esencial para mantener el rendimiento de las consultas. |
| Gránulos | La unidad más pequeña de datos que ClickHouse lee y descarta durante la ejecución de consultas. |
| Claves primarias (de ordenación) | Cómo la clave ORDER BY determina la disposición de los datos en disco, la compresión y el descarte de datos en las consultas. |
- Creación de tablas en ClickHouse - Una introducción sencilla a las tablas.
- Parts
- Partitions
- Merges
- Primary keys/indexes
- Cómo almacena datos ClickHouse: partes y gránulos - Guía más avanzada sobre cómo se estructuran y consultan los datos en ClickHouse, con explicaciones detalladas de los gránulos y las claves primarias.
- MergeTree- Guía avanzada de referencia de MergeTree útil para comandos y detalles internos.
Optimización 1. Materializar atributos consultados con frecuencia
LogAttributes, ScopeAttributes y ResourceAttributes, y convertirlos en columnas de nivel superior mediante columnas materializadas.
Por sí sola, esta optimización suele ser suficiente para escalar despliegues de ClickStack a decenas de terabytes al día y debe aplicarse antes de considerar técnicas de ajuste más avanzadas.
Por qué materializar atributos
Map(String, String). Aunque esto aporta flexibilidad, consultar subclaves de un mapa tiene una implicación importante en el rendimiento.
Al consultar una sola clave de una columna Map, ClickHouse debe leer del disco toda la columna del mapa. Si el mapa contiene muchas claves, esto genera E/S innecesaria y hace que las consultas sean más lentas en comparación con leer una columna dedicada.
Materializar los atributos a los que se accede con frecuencia evita esta sobrecarga, ya que extrae el valor en el momento de la inserción y lo almacena como una columna de primera clase.
Columnas materializadas:
- Se calculan automáticamente durante las inserciones
- No se pueden establecer explícitamente en sentencias INSERT
- Admiten cualquier expresión de ClickHouse
- Permiten convertir String en tipos numéricos o de fecha más eficientes
- Permiten usar skip indexes y la clave primaria
- Reducen las lecturas de disco al evitar acceder al mapa completo
ClickStack detecta automáticamente las columnas materializadas extraídas de mapas y las utiliza de forma transparente durante la ejecución de consultas, incluso cuando los usuarios siguen consultando la ruta del atributo original.
Ejemplo
ResourceAttributes:
ResourceAttributes.k8s.pod.name:"checkout-675775c4cc-f2p9c":
Esto genera un predicado SQL similar a:
ResourceAttributes completa para cada fila coincidente, lo que puede ser muy grande si el Map contiene muchas claves.
Si este atributo se consulta con frecuencia, debe materializarse como una columna de nivel superior.
Para extraer el nombre del pod de Kubernetes en el momento de la inserción, añada una columna materializada:
PodName.
Ahora los usuarios pueden consultar los nombres de los pods de Kubernetes de forma eficiente con sintaxis Lucene; por ejemplo, PodName:"checkout-675775c4cc-f2p9c"
En los datos recién insertados, esto evita por completo el acceso al mapa y reduce significativamente la E/S.
Sin embargo, aunque los usuarios sigan consultando la ruta del atributo original, por ejemplo ResourceAttributes.k8s.pod.name:"checkout-675775c4cc-f2p9c", ClickStack reescribirá automáticamente la consulta internamente para usar la columna materializada PodName, es decir, con el predicado:
De forma predeterminada, las columnas materializadas se excluyen de las consultas
SELECT *. Así se mantiene la garantía de que los resultados de las consultas siempre puedan volver a insertarse en la tabla.Materialización de datos históricos
system.mutations, por ejemplo.
is_done = 1 en la mutación correspondiente.
Optimización 2. Añadir índices de omisión
- Filtrado de cadenas con alta cardinalidad, como TraceId, identificadores de sesión, claves de atributo o valores
- Filtrado por rangos numéricos, como la duración del span
Filtros de Bloom
PodName:"checkout-675775c4cc-f2p9c".
Los filtros de Bloom son más eficaces cuando la distribución de los valores hace que un valor dado aparezca en un número relativamente pequeño de partes. Esto suele ocurrir de forma natural en cargas de trabajo de observabilidad, donde metadatos como los nombres de pods de Kubernetes, los ID de trace o los identificadores de sesión se correlacionan con el tiempo y, por tanto, se agrupan según la clave de ordenación de la tabla.
Como ocurre con todos los índices de omisión, los filtros de Bloom deben añadirse de forma selectiva y validarse con patrones de consulta reales para garantizar que aportan un beneficio medible; consulte “Evaluar la eficacia del índice de omisión.”
Índices MinMax
SpanAttributes:
Materializar el índice de omisión
Materialización de índices de omisiónMaterializar un índice de omisión suele ser una operación ligera y segura, especialmente en el caso de los índices MinMax. En el caso de índices de filtro de Bloom sobre conjuntos de datos grandes, puede ser preferible materializarlos por partición para controlar mejor el uso de recursos; por ejemplo:
is_done = 1 para la mutación correspondiente.
Una vez completada, confirme que se han creado los datos del índice:
0.01 a 0.05 produce un índice más pequeño que se evalúa más rápido, a costa de una poda menos agresiva. Aunque pueden omitirse menos gránulos, la latencia global de la consulta puede mejorar gracias a una evaluación más rápida del índice.
Por lo tanto, ajustar los parámetros del filtro de Bloom es una optimización que depende de la carga de trabajo y debe validarse con patrones de consulta reales y volúmenes de datos similares a los de producción.
Para obtener más información sobre los índices de omisión, consulta la guía “Comprender los índices de omisión de datos de ClickHouse.”
Evaluación de la eficacia de los skip indexes
EXPLAIN indexes = 1, que muestra cuántas partes y gránulos se descartan en cada etapa de la planificación de la consulta. En la mayoría de los casos, conviene ver una reducción significativa del número de gránulos en la etapa Skip, idealmente después de que la clave primaria ya haya reducido el espacio de búsqueda. Los skip indexes se evalúan después de la poda de particiones y de la poda por clave primaria, por lo que su impacto se mide mejor en relación con las partes y los gránulos que quedan.
EXPLAIN confirma si se está aplicando la poda, pero no garantiza una mejora neta del rendimiento. Los skip indexes tienen un coste de evaluación, especialmente si el índice es grande. Haga siempre benchmark de las consultas antes y después de añadir y materializar un índice para confirmar mejoras reales de rendimiento.
Por ejemplo, considere el skip index predeterminado de filtro Bloom para TraceId incluido en el esquema Traces predeterminado:
EXPLAIN indexes = 1 para ver lo eficaz que es en una consulta selectiva:
FORMAT Null para evitar la sobrecarga de la serialización de resultados y desactiva la caché de condiciones de consulta para que las ejecuciones sean repetibles:
use_query_condition_cache garantiza que los resultados no se vean afectados por decisiones de filtrado almacenadas en caché, y establecer use_skip_indexes = 0 proporciona una base de referencia limpia para la comparación. Si la poda es efectiva y el coste de evaluar el índice es bajo, la consulta indexada debería ser notablemente más rápida, como en el ejemplo anterior.
Cuándo añadir skip indexes
minmax casi siempre es una buena opción. Es ligero, barato de evaluar y puede ser eficaz para predicados de rango, especialmente cuando los valores están poco ordenados o quedan confinados a rangos estrechos dentro de las partes. Incluso cuando minmax no ayuda con un patrón de consulta concreto, su sobrecarga suele ser lo bastante baja como para que siga siendo razonable mantenerlo.
Columnas de texto. Use filtros Bloom cuando la cardinalidad sea alta y los valores sean dispersos.
Los filtros Bloom son más eficaces en columnas de texto con alta cardinalidad en las que cada valor tiene una frecuencia relativamente baja, lo que significa que la mayoría de las partes y los gránulos no contienen el valor buscado. Como regla general, los filtros Bloom son más prometedores cuando la columna tiene al menos 10.000 valores distintos, y a menudo ofrecen el mejor rendimiento con más de 100.000 valores distintos. También son más eficaces cuando los valores coincidentes se agrupan en un número reducido de partes secuenciales, lo que normalmente ocurre cuando la columna está correlacionada con la clave de ordenación. De nuevo, esto puede variar según el caso; nada sustituye a las pruebas en el mundo real.
Optimización 3. Modificar la clave primaria
Nota sobre la terminologíaA lo largo de este documento, el término “ordering key” se usa indistintamente con “primary key”. En sentido estricto, estos conceptos difieren en ClickHouse, pero en ClickStack normalmente se refieren a las mismas columnas especificadas en la cláusula
ORDER BY de la tabla. Para más información, consulta la documentación de ClickHouse sobre cómo elegir una clave primaria distinta de la clave de ordenación.- Logs (
otel_logs) -(ServiceName, TimestampTime, Timestamp) - Traces (‘otel_traces) -
(ServiceName, SpanName, toDateTime(Timestamp))
Cómo elegir una clave primaria
Modificar la clave primaria predeterminadaLas claves primarias predeterminadas son suficientes en la mayoría de los casos. Los cambios deben hacerse con cautela y solo con una comprensión clara de los patrones de consulta. Modificar una clave primaria puede degradar el rendimiento de otros flujos de trabajo, por lo que es fundamental realizar pruebas.
- Seleccione columnas que se ajusten a sus filtros habituales y patrones de acceso. Si normalmente inicia investigaciones de observabilidad filtrando por una columna específica, p. ej., el nombre del pod de Kubernetes, esta columna se usará con frecuencia en las cláusulas
WHERE. Priorice incluirlas en la clave frente a otras que se utilicen con menos frecuencia. - Prefiera columnas que ayuden a excluir un gran porcentaje del total de filas al filtrar, reduciendo así la cantidad de datos que es necesario leer. Los nombres de servicio y los códigos de estado suelen ser buenos candidatos; en este último caso, solo si filtra por valores que excluyen la mayoría de las filas. Por ejemplo, filtrar por códigos 200 coincidirá, en la mayoría de los sistemas, con la mayor parte de las filas, mientras que los errores 500 corresponderán a un subconjunto pequeño.
- Prefiera columnas que probablemente estén muy correlacionadas con otras columnas de la tabla. Esto ayudará a garantizar que esos valores también se almacenen de forma contigua, lo que mejora la compresión.
- Las operaciones
GROUP BY(agregaciones para gráficos) yORDER BY(ordenación) sobre columnas incluidas en la clave de ordenación pueden ser más eficientes en el uso de memoria.
Cambio de la clave primaria
SeverityText antes de ServiceName.
Crear una nueva tabla
Clave de ordenación frente a clave primariaTen en cuenta que, en el ejemplo anterior, es necesario especificar
PRIMARY KEY y ORDER BY.
En ClickStack, casi siempre son iguales.
ORDER BY controla la disposición física de los datos, mientras que PRIMARY KEY define el índice disperso.
En casos poco frecuentes, con cargas de trabajo muy grandes, pueden diferir, pero la mayoría de los usuarios debería mantenerlos alineados.Rara vez compensa hacer backfill de los datos existentes en una nueva tabla a gran escala. El coste de cómputo y E/S suele ser alto y no justifica las ventajas de rendimiento. En su lugar, deja que los datos más antiguos expiren mediante TTL, mientras que los datos más recientes se benefician de la clave mejorada.
SeverityText como primera columna de la clave primaria. En este caso, se crea una tabla para los datos nuevos y se conserva la tabla anterior para el análisis histórico.
Crear una nueva tabla
Crea la nueva tabla con la clave primaria deseada. Observa el sufijo_23_01_2025: adáptalo para que sea la fecha actual. Por ejemplo:Crear una tabla Merge
El motor Merge (no debe confundirse con MergeTree) no almacena datos por sí mismo, pero permite leer al mismo tiempo de cualquier número de tablas.currentDatabase() asume que el comando se ejecuta en la base de datos correcta. De lo contrario, especifica explícitamente el nombre de la base de datos.otel_logs.Actualizar HyperDX para leer desde la tabla Merge
Configura HyperDX para usarotel_logs_merge como tabla de la fuente de datos de logs.En este punto, las escrituras continúan en otel_logs con la clave primaria original, mientras que las lecturas usan la tabla Merge. No hay ningún cambio visible para los usuarios ni impacto en la ingestión.Intercambiar las tablas
Ahora se usa una instrucciónEXCHANGE para intercambiar atómicamente los nombres de las tablas otel_logs y otel_logs_23_01_2025.otel_logs con la clave primaria actualizada. Los datos existentes permanecen en otel_logs_23_01_2025 y siguen siendo accesibles a través de la tabla Merge. El sufijo indica la fecha en que se aplicó el cambio y representa el timestamp más reciente contenido en esa tabla.Este proceso permite cambiar la clave primaria sin interrumpir la ingestión y sin impacto visible para el usuario.SeverityNumber debería formar parte de la clave primaria en lugar de SeverityText. El siguiente proceso puede adaptarse tantas veces como sea necesario para aplicar cambios en la clave primaria.
Crear una nueva tabla
Crea la nueva tabla con la clave primaria deseada. En el ejemplo siguiente,30_01_2025 se usa como sufijo para indicar la fecha de la tabla. Por ejemplo:Intercambiar las tablas
Ahora se utiliza una sentenciaEXCHANGE para intercambiar atómicamente los nombres de las tablas otel_logs y otel_logs_30_01_2025.otel_logs con la clave primaria actualizada. Los datos antiguos permanecen en otel_logs_30_01_2025, accesibles a través de la tabla Merge.Tablas redundantesSi hay políticas TTL configuradas, lo cual se recomienda, las tablas con claves primarias antiguas que ya no reciben escrituras se irán vaciando gradualmente a medida que caduquen los datos. Deben supervisarse y limpiarse periódicamente cuando ya no contengan datos. Por ahora, este proceso de limpieza es manual.
Optimización 4. Uso de vistas materializadas
Optimización 5. Aprovechar las proyecciones
ORDER BY de la tabla base, lo que permite a ClickHouse descartar datos de forma más eficaz para patrones de acceso que no se ajustan al orden original.
Las vistas materializadas pueden lograr un efecto similar al escribir explícitamente filas en una tabla de destino independiente con una clave de ordenación distinta. La diferencia principal es que ClickHouse mantiene las proyecciones de forma automática y transparente, mientras que las vistas materializadas son tablas explícitas que ClickStack debe registrar y seleccionar de forma intencionada.
Cuando una consulta se ejecuta sobre la tabla base, ClickHouse evalúa la disposición base y las proyecciones disponibles, examina sus índices primarios y selecciona la disposición que puede producir el resultado correcto leyendo la menor cantidad de gránulos. Esta decisión la toma automáticamente el analizador de consultas.
Por lo tanto, en ClickStack, las proyecciones son más adecuadas para la reordenación pura de datos, donde:
- Los patrones de acceso son sustancialmente distintos de la clave primaria predeterminada
- No resulta práctico cubrir todos los flujos de trabajo con una sola clave de ordenación
- Quiere que ClickHouse elija de forma transparente la disposición física óptima
Proyecciones de ejemplo
Usa comodinesEn la proyección de ejemplo anterior, se usa un comodín (
SELECT *). Aunque seleccionar un subconjunto de columnas puede reducir la sobrecarga de escritura, también limita cuándo puede usarse la proyección, ya que solo pueden aprovecharla las consultas que puedan resolverse por completo con esas columnas. En ClickStack, esto suele restringir el uso de la proyección a casos muy específicos. Por este motivo, en general se recomienda usar un comodín para maximizar su aplicabilidad.Materializar una proyección puede llevar mucho tiempo y consumir una cantidad considerable de recursos. Como los datos de observabilidad normalmente expiran por TTL, esto solo debe hacerse cuando sea absolutamente necesario. En la mayoría de los casos, basta con dejar que la proyección se aplique solo a los datos recién ingeridos, para que optimice los intervalos de tiempo consultados con más frecuencia, como las últimas 24 horas.
SELECT *) y los filtros de la consulta se ajustan claramente al ORDER BY de la proyección.
Las consultas que filtran por TraceId (especialmente con igualdad) e incluyen un intervalo de tiempo se beneficiarían de la proyección anterior. Por ejemplo:
TraceId, o que filtran principalmente por otras dimensiones que no ocupan las primeras posiciones en la clave de ordenación de la proyección, normalmente no se beneficiarán de ello (y en su lugar pueden leer desde la estructura base).
Las proyecciones también pueden almacenar agregaciones (de forma similar a las vistas materializadas). En ClickStack, por lo general no se recomiendan las agregaciones basadas en proyecciones, porque su selección depende del analizador de ClickHouse y su uso puede ser más difícil de controlar y comprender. En su lugar, prefiera vistas materializadas explícitas que ClickStack pueda registrar y seleccionar intencionadamente en la capa de aplicación.
TraceId específico).
Costes y recomendaciones
- Sobrecarga de inserción: Una proyección
SELECT *con una clave de ordenación distinta equivale, en la práctica, a escribir los datos dos veces, lo que incrementa la E/S de escritura y puede requerir CPU adicional y mayor rendimiento de disco para sostener la ingestión. - Úselas con moderación: Lo mejor es reservar las proyecciones para patrones de acceso realmente distintos, en los que una segunda ordenación física permita una poda significativa en una gran parte de las consultas; por ejemplo, cuando dos equipos consultan el mismo conjunto de datos de maneras fundamentalmente diferentes.
- Valídelo con benchmarks: Como con cualquier ajuste, compare la latencia real de las consultas y el uso de recursos antes y después de añadir y materializar una proyección.
Proyecciones ligeras con _part_offset
Las proyecciones ligeras están en Beta para ClickStackNo se recomiendan las proyecciones ligeras
basadas en _part_offset para las cargas de trabajo de ClickStack. Aunque reducen el almacenamiento y la E/S de escritura, pueden introducir más accesos aleatorios en tiempo de consulta, y su comportamiento en producción a escala de observabilidad aún se está evaluando. Esta recomendación puede cambiar a medida que esta funcionalidad madure y recopilemos más datos operativos._part_offset a la tabla base, en lugar de duplicar filas completas. Esto puede reducir considerablemente la sobrecarga de almacenamiento, y las mejoras recientes permiten la poda a nivel de gránulo, por lo que se comportan más como verdaderos índices secundarios. Consulte:
Alternativas
- Configurar su OpenTelemetry Collector para que escriba en dos tablas con claves
ORDER BYdiferentes y crear fuentes de ClickStack independientes para cada tabla. - Crear una vista materializada como canalización de copia; es decir, adjuntar una vista materializada a la tabla principal que seleccione filas sin procesar en una tabla secundaria con una clave de ordenación distinta (un patrón de desnormalización o enrutamiento). Cree una fuente para esta tabla de destino. Puede encontrar ejemplos aquí.