Toda solución de observabilidad requiere algún mecanismo para recopilar y exportar logs y trazas. Para ello, ClickHouse recomienda el proyecto OpenTelemetry (OTel).
“OpenTelemetry es un framework y conjunto de herramientas de observabilidad diseñado para crear y gestionar datos de telemetría, como trazas, métricas y logs”.
A diferencia de ClickHouse o Prometheus, OpenTelemetry no es un backend de observabilidad, sino que se centra en la generación, recopilación, gestión y exportación de datos de telemetría. Aunque el objetivo inicial de OpenTelemetry era permitir instrumentar fácilmente aplicaciones o sistemas mediante SDKs específicos de cada lenguaje, se ha ampliado para incluir la recopilación de logs a través del OpenTelemetry Collector: un agente o proxy que recibe, procesa y exporta datos de telemetría.
Componentes relevantes de ClickHouse
OpenTelemetry consta de varios componentes. Además de proporcionar una especificación de datos y API, un protocolo estandarizado y convenciones de nomenclatura para campos/columnas, OTel ofrece dos capacidades fundamentales para crear una solución de observabilidad con ClickHouse:
- El OpenTelemetry Collector es un proxy que recibe, procesa y exporta datos de telemetría. Una solución basada en ClickHouse utiliza este componente tanto para la recopilación de logs como para el procesamiento de eventos antes de agruparlos en lotes e insertarlos.
- Los SDK para distintos lenguajes implementan la especificación, las API y la exportación de datos de telemetría. En la práctica, estos SDK garantizan que las trazas se registren correctamente en el código de una aplicación, generando los spans que las componen y asegurando que el contexto se propague entre servicios mediante metadatos; de este modo, se crean trazas distribuidas y se garantiza que los spans puedan correlacionarse. Estos SDK se complementan con un ecosistema que instrumenta automáticamente bibliotecas y frameworks comunes, por lo que el usuario no necesita modificar su código y obtiene instrumentación lista para usar.
Una solución de observabilidad basada en ClickHouse aprovecha ambas herramientas.
El OpenTelemetry Collector tiene varias distribuciones. El filelog receiver, junto con el exportador de ClickHouse, necesarios para una solución con ClickHouse, solo está presente en la distribución Contrib de OpenTelemetry Collector.
Esta distribución contiene muchos componentes y permite experimentar con varias configuraciones. Sin embargo, para ejecutarlo en producción, se recomienda limitar el colector a incluir solo los componentes necesarios para un entorno. Algunas razones para hacerlo:
- Reducir el tamaño del colector, lo que acorta los tiempos de implementación
- Mejorar la seguridad del colector al reducir la superficie de ataque disponible
Es posible crear un colector personalizado con OpenTelemetry Collector Builder.
Ingesta de datos con OTel
Roles de despliegue del colector
Para recopilar logs e insertarlos en ClickHouse, recomendamos usar el OpenTelemetry Collector. El OpenTelemetry Collector puede desplegarse en dos roles principales:
- Agent - Las instancias de Agent recopilan datos en el extremo, por ejemplo, en servidores o en nodos de Kubernetes, o reciben eventos directamente de aplicaciones instrumentadas con un SDK de OpenTelemetry. En este último caso, la instancia de Agent se ejecuta junto con la aplicación o en el mismo host que la aplicación (como un sidecar o un conjunto de daemon). Los agentes pueden enviar sus datos directamente a ClickHouse o a una instancia de gateway. En el primer caso, esto se conoce como patrón de despliegue Agent.
- Gateway - Las instancias de Gateway proporcionan un servicio independiente (por ejemplo, un despliegue en Kubernetes), normalmente por clúster, centro de datos o región. Estas reciben eventos de aplicaciones (u otros colectores que actúan como agentes) a través de un único endpoint OTLP. Normalmente, se despliega un conjunto de instancias de gateway, con un balanceador de carga listo para usar que distribuye la carga entre ellas. Si todos los agentes y las aplicaciones envían sus señales a este único endpoint, suele denominarse patrón de despliegue Gateway.
A continuación, asumimos un colector de agente sencillo que envía sus eventos directamente a ClickHouse. Consulta Escalado con Gateways para obtener más información sobre el uso de gateways y cuándo resultan adecuados.
La principal ventaja de usar un colector es que permite a sus servicios descargar los datos rápidamente y dejar en manos del colector el procesamiento adicional, como reintentos, procesamiento por lotes, cifrado o incluso filtrado de datos sensibles.
El colector utiliza los términos receiver, procesador y exportador para sus tres etapas principales de procesamiento. Los receivers se utilizan para recopilar datos y pueden basarse en pull o en push. Los procesadores permiten realizar transformaciones y enriquecer los mensajes. Los exportadores se encargan de enviar los datos a un servicio de destino. Aunque, en teoría, este servicio podría ser otro colector, para la explicación inicial que sigue asumimos que todos los datos se envían directamente a ClickHouse.
Recomendamos a los usuarios familiarizarse con el conjunto completo de receivers, procesadores y exportadores.
El colector ofrece dos receivers principales para recopilar logs:
Mediante OTLP - En este caso, los logs se envían (push) directamente al colector desde los SDK de OpenTelemetry a través del protocolo OTLP. La demo de OpenTelemetry emplea este enfoque, en el que los exportadores OTLP de cada lenguaje suponen un endpoint de colector local. En este caso, el colector debe configurarse con el receiver OTLP; consulte la demo anterior para ver un ejemplo de configuración. La ventaja de este enfoque es que los datos de logs incluirán automáticamente Trace IDs, lo que permitirá a los usuarios identificar después los traces de un log concreto y viceversa.
Este enfoque requiere que los usuarios instrumenten su código con el SDK del lenguaje correspondiente.
- Scraping mediante el filelog receiver - Este receiver sigue archivos en disco y genera mensajes de log que luego envía a ClickHouse. También se encarga de tareas complejas, como detectar mensajes multilínea, gestionar la rotación de logs, crear puntos de control para resistir reinicios y extraer estructura. Además, este receiver también puede seguir logs de contenedores de Docker y Kubernetes, desplegado como un gráfico de Helm, extraer su estructura y enriquecerlos con los detalles del pod de Kubernetes.
La mayoría de las implementaciones usarán una combinación de los receivers anteriores. Recomendamos a los usuarios leer la documentación del colector y familiarizarse con los conceptos básicos, así como con la estructura de configuración y los métodos de instalación.
Consejo: otelbin.iootelbin.io es útil para validar y visualizar configuraciones.
Estructurados vs. no estructurados
Los logs pueden ser estructurados o no estructurados.
Un log estructurado utiliza un formato de datos como JSON, que define campos de metadatos como el código HTTP y la dirección IP de origen.
Los logs no estructurados, aunque también suelen tener cierta estructura inherente que puede extraerse mediante un patrón regex, se representarán únicamente como una cadena de texto.
Recomendamos que los usuarios empleen logging estructurado y generen logs en JSON (es decir, ndjson) siempre que sea posible. Esto simplificará el procesamiento posterior de los logs, ya sea antes de enviarlos a ClickHouse con procesadores del collector o en el momento de la inserción mediante vistas materializadas. En última instancia, los logs estructurados ahorrarán recursos de procesamiento y reducirán la CPU necesaria en su solución de ClickHouse.
Como ejemplo, proporcionamos un conjunto de datos de logs estructurados (JSON) y otro no estructurados, cada uno con aproximadamente 10 millones de filas, disponibles en los siguientes enlaces:
Usamos el conjunto de datos estructurado en el ejemplo siguiente. Asegúrese de descargar y extraer este archivo para reproducir los ejemplos a continuación.
A continuación se muestra una configuración sencilla del OTel collector que lee estos archivos del disco mediante el filelog receiver y envía los mensajes resultantes a stdout. Usamos el operator json_parser porque nuestros logs están estructurados. Modifique la ruta al archivo access-structured.log.
config-structured-logs.yaml
Puedes seguir las instrucciones oficiales para instalar el collector localmente. Es importante modificar esas instrucciones para usar la distribución contrib (que incluye el receiver filelog); por ejemplo, en lugar de otelcol_0.102.1_darwin_arm64.tar.gz, los usuarios descargarían otelcol-contrib_0.102.1_darwin_arm64.tar.gz. Las versiones están disponibles aquí.
Una vez instalado, el OTel collector puede ejecutarse con los siguientes comandos:
Suponiendo que se usan logs estructurados, los mensajes tendrán la siguiente forma en la salida:
Lo anterior representa un único mensaje de log, tal como lo produce el OTel collector. Ingestamos estos mismos mensajes en ClickHouse en secciones posteriores.
El esquema completo de los mensajes de log, junto con columnas adicionales que pueden estar presentes si se usan otros receivers, se mantiene aquí. Recomendamos encarecidamente a los usuarios que se familiaricen con este esquema.
La clave aquí es que la propia línea de log se almacena como una cadena dentro del campo Body, pero el JSON se ha extraído automáticamente al campo Attributes gracias a json_parser. Este mismo operator se ha utilizado para extraer la marca de tiempo a la columna Timestamp correspondiente. Para ver recomendaciones sobre cómo procesar logs con OTel, consulte Procesamiento.
OperadoresLos operadores son la unidad más básica del procesamiento de logs. Cada operador cumple una única función, como leer líneas de un archivo o analizar JSON de un campo. Después, los operadores se encadenan en un pipeline para lograr el resultado deseado.
Los mensajes anteriores no tienen un campo TraceID ni SpanID. Si están presentes, p. ej., en casos en los que los usuarios estén implementando distributed tracing, podrían extraerse del JSON usando las mismas técnicas mostradas anteriormente.
Para los usuarios que necesitan recopilar archivos de log locales o de Kubernetes, recomendamos familiarizarse con las opciones de configuración disponibles para el filelog receiver, así como con la forma en que se gestionan los offsets y el análisis de logs multilínea.
Recopilación de logs de Kubernetes
Para recopilar logs de Kubernetes, recomendamos la guía de la documentación de OpenTelemetry. Se recomienda el Kubernetes Attributes Processor para enriquecer logs y métricas con metadatos del pod. Esto puede generar metadatos dinámicos, como etiquetas, almacenados en la columna ResourceAttributes. Actualmente, ClickHouse utiliza el tipo Map(String, String) para esta columna. Consulta Using Maps y Extracting from maps para obtener más información sobre cómo manejar y optimizar este tipo.
Para quienes quieran instrumentar su código y recopilar trazas, recomendamos seguir la documentación oficial de OTel.
Para enviar eventos a ClickHouse, deberá desplegar un OTel collector que reciba eventos de trazas mediante el protocolo OTLP a través del receiver adecuado. La demo de OpenTelemetry ofrece un ejemplo de cómo instrumentar cada lenguaje compatible y enviar eventos a un collector. A continuación, se muestra un ejemplo de una configuración de collector adecuada que envía los eventos a stdout:
Dado que las trazas deben recibirse mediante OTLP, usamos la herramienta telemetrygen para generar datos de trazas. Siga las instrucciones aquí para instalarla.
La siguiente configuración recibe eventos de trazas en un receiver OTLP antes de enviarlos a stdout.
config-traces.xml
Ejecute esta configuración con:
Envía eventos de trazas al collector mediante telemetrygen:
Esto hará que se envíen a stdout mensajes de traza similares al ejemplo siguiente:
Lo anterior representa un único mensaje de traza, tal como lo produce el OTel collector. En secciones posteriores, ingestamos estos mismos mensajes en ClickHouse.
El esquema completo de los mensajes de traza está disponible aquí. Recomendamos encarecidamente que los usuarios se familiaricen con este esquema.
Como se mostró en el ejemplo anterior sobre cómo establecer el timestamp de un evento de log, inevitablemente querrá filtrar, transformar y enriquecer los mensajes de eventos. Esto puede lograrse mediante varias capacidades de OpenTelemetry:
-
Procesadores: los procesadores toman los datos recopilados por los receivers y los modifican o transforman antes de enviarlos a los exportadores. Los procesadores se aplican en el orden en que están configurados en la sección
processors de la configuración del collector. Son opcionales, pero normalmente se recomienda el conjunto mínimo. Al usar un OTel collector con ClickHouse, recomendamos limitar los procesadores a:
- Un memory_limiter se usa para evitar situaciones de falta de memoria en el collector. Consulte Estimating Resources para ver recomendaciones.
- Cualquier processor que realice enriquecimiento basado en contexto. Por ejemplo, el Kubernetes Attributes Processor permite establecer automáticamente atributos de recursos en spans, métricas y logs con metadatos de k8s; por ejemplo, enriquecer eventos con el id de su pod de origen.
- Tail or head sampling si es necesario para traces.
- Filtrado básico: descarte de eventos que no se necesitan si esto no puede hacerse mediante un operator (véase más abajo).
- Batching: esencial al trabajar con ClickHouse para garantizar que los datos se envíen en batches. Consulte “Exporting to ClickHouse”.
-
Operators: los Operators proporcionan la unidad de procesamiento más básica disponible en el receiver. Se admite parsing básico, lo que permite establecer campos como Severity y Timestamp. Aquí se admite parsing de JSON y regex, junto con el filtrado de eventos y transformaciones básicas. Recomendamos realizar aquí el filtrado de eventos.
Recomendamos a los usuarios evitar el procesamiento excesivo de eventos mediante operators o transform processors. Estos pueden generar una sobrecarga considerable de memoria y CPU, especialmente el parsing de JSON. Con algunas excepciones, es posible realizar todo el procesamiento en ClickHouse en el momento de la inserción mediante vistas materializadas y columnas; en concreto, el enriquecimiento dependiente del contexto, por ejemplo, añadir metadatos de k8s. Para obtener más detalles, consulte Extracting structure with SQL.
Si el procesamiento se realiza con el OTel collector, recomendamos hacer las transformaciones en instancias gateway y minimizar cualquier trabajo realizado en instancias agent. Esto garantizará que los recursos requeridos por los agents en el borde, que se ejecutan en servidores, sean los mínimos posibles. Normalmente, vemos que los usuarios solo realizan filtrado (para minimizar el uso innecesario de la red), establecimiento de timestamps (mediante operators) y enriquecimiento, que requiere contexto en los agents. Por ejemplo, si las instancias gateway residen en un cluster de Kubernetes distinto, el enriquecimiento de k8s deberá realizarse en el agent.
La siguiente configuración muestra cómo recopilar un archivo de logs no estructurado. Observe el uso de operadores para extraer estructura de las líneas de log (regex_parser) y filtrar eventos, junto con un processor para agrupar eventos en lotes y limitar el uso de memoria.
config-unstructured-logs-with-processor.yaml
Los exportadores envían datos a uno o varios backends o destinos. Los exportadores pueden ser de tipo pull o push. Para enviar eventos a ClickHouse, deberá usar el exportador de ClickHouse basado en push.
A continuación se muestra un archivo de configuración completo.
clickhouse-config.yaml
Tenga en cuenta los siguientes ajustes clave:
- pipelines - La configuración anterior destaca el uso de pipelines, compuestas por un conjunto de receptores, procesadores y exportadores, con una para logs y trazas.
- endpoint - La comunicación con ClickHouse se configura mediante el parámetro
endpoint. La cadena de conexión tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1 hace que la comunicación se realice a través de TCP. Si prefieres HTTP por motivos de conmutación de tráfico, modifica esta cadena de conexión como se describe aquí. Los detalles completos de la conexión, incluida la posibilidad de especificar un nombre de usuario y una contraseña dentro de esta cadena de conexión, se describen aquí.
Importante: Ten en cuenta que la cadena de conexión anterior habilita tanto la compresión (lz4) como las inserciones asíncronas. Recomendamos que ambas estén siempre habilitadas. Consulta Batching para obtener más detalles sobre las inserciones asíncronas. La compresión debe especificarse siempre y, en versiones antiguas del exportador, no se habilita de forma predeterminada.
- ttl - el valor aquí determina durante cuánto tiempo se conservan los datos. Más detalles en “Gestión de datos”. Debe especificarse como una unidad de tiempo en horas, por ejemplo, 72h. Deshabilitamos TTL en el ejemplo siguiente, ya que nuestros datos son de 2019 y ClickHouse los eliminará inmediatamente si se insertan.
- traces_table_name y logs_table_name - determinan el nombre de las tablas de logs y trazas.
- create_schema - determina si las tablas se crean con los esquemas predeterminados al iniciar. El valor predeterminado es true para empezar. Debes establecerlo en false y definir el esquema manualmente.
- database - database de destino.
- retry_on_failure - ajustes para determinar si deben reintentarse los batches fallidos.
- batch - un batch processor garantiza que los eventos se envíen en batches. Recomendamos un valor de al menos 10,000 con un timeout de 5s (pueden usarse valores de hasta 100,000 si la memoria lo permite). Lo que se alcance primero iniciará un batch para volcarlo al exportador. Reducir estos valores implicará una pipeline de menor latencia, con datos disponibles antes para consulta, a costa de más conexiones y batches enviados a ClickHouse. Esto no se recomienda si no estás usando asynchronous inserts, ya que puede causar problemas de Too many parts en ClickHouse. Por el contrario, si estás usando inserciones asíncronas, la disponibilidad de estos datos para consulta también dependerá de la configuración de inserción asíncrona, aunque los datos seguirán volcándose antes desde el conector. Consulta Batching para más detalles.
- sending_queue - controla el tamaño de la cola de envío. Cada elemento de la cola contiene un batch. Si se supera esta cola, por ejemplo, porque ClickHouse no está accesible pero los eventos siguen llegando, los batches se descartarán.
Suponiendo que los usuarios hayan extraído el archivo de logs estructurado y tengan una instancia local de ClickHouse en ejecución (con la autenticación predeterminada), puedes ejecutar esta configuración mediante el comando:
Para enviar trazas a este collector, ejecuta el siguiente comando usando la herramienta telemetrygen:
Una vez que esté en ejecución, confirme que haya eventos de registro con una consulta sencilla:
ClickStack incluye un esquema predeterminado optimizadoClickStack proporciona esquemas listos para usar para logs, trazas y métricas que incorporan las funcionalidades más recientes de ClickHouse (índices de texto para búsquedas de texto completo y por claves de Map, columnas materializadas y arrays ALIAS para filtrado de lectura directa, búsquedas de filas por número de bloque) y se han sometido a benchmark para ofrecer un sólido rendimiento inicial en cargas de trabajo de logging y trazas. Úselos como punto de referencia para su propio diseño.
De forma predeterminada, el exportador de ClickHouse crea una tabla de destino para logs y trazas. Esto se puede desactivar mediante la configuración create_schema. Además, los nombres de las tablas de logs y trazas pueden cambiarse con respecto a sus valores predeterminados, otel_logs y otel_traces, mediante las configuraciones indicadas anteriormente.
En los esquemas siguientes, asumimos que TTL está habilitado con 72h.
A continuación se muestra el esquema predeterminado para logs (otelcol-contrib v0.102.1):
Las columnas aquí se ajustan a la especificación oficial de OTel para logs, documentada aquí.
Algunas notas importantes sobre este esquema:
- De forma predeterminada, la tabla está particionada por fecha mediante
PARTITION BY toDate(Timestamp). Esto permite eliminar con eficiencia los datos que caducan.
- El TTL se establece mediante
TTL toDateTime(Timestamp) + toIntervalDay(3) y corresponde al valor definido en la configuración del collector. ttl_only_drop_parts=1 significa que solo se eliminan partes completas cuando todas las filas que contienen han caducado. Esto es más eficiente que eliminar filas dentro de las partes, lo que implica una operación de borrado costosa. Recomendamos tenerlo siempre configurado así. Consulta Data management with TTL para más detalles.
- La tabla utiliza el motor clásico
MergeTree engine. Se recomienda para logs y trazas, y no debería ser necesario cambiarlo.
- La tabla está ordenada por
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId). Esto significa que las consultas se optimizarán para filtros sobre ServiceName, SeverityText, Timestamp y TraceId: las columnas que aparecen antes en la lista se filtrarán más rápido que las posteriores; por ejemplo, filtrar por ServiceName será significativamente más rápido que filtrar por TraceId. Debes modificar este orden según los patrones de acceso previstos; consulta Choosing a primary key.
- El esquema anterior aplica
ZSTD(1) a las columnas. Esto ofrece la mejor compresión para logs. Puedes aumentar el nivel de compresión de ZSTD (por encima del valor predeterminado de 1) para obtener una mejor compresión, aunque rara vez resulta beneficioso. Aumentar este valor implicará una mayor sobrecarga de CPU en el momento de la inserción (durante la compresión), aunque la descompresión (y, por tanto, las consultas) debería seguir siendo comparable. Consulta aquí para más detalles. También se aplica codificación delta adicional a Timestamp con el objetivo de reducir su tamaño en disco.
- Observa que
ResourceAttributes, LogAttributes y ScopeAttributes son mapas. Es importante comprender las diferencias entre ellos. Consulta “Using maps” para ver cómo acceder a estos mapas y optimizar el acceso a sus claves.
- La mayoría de los demás tipos aquí, por ejemplo
ServiceName como LowCardinality, ya están optimizados. Ten en cuenta que Body, que es JSON en nuestros logs de ejemplo, se almacena como un String.
- Se aplican bloom filters a las claves y los valores de los mapas, así como a la columna
Body. Su objetivo es mejorar los tiempos de consulta al acceder a estas columnas, pero por lo general no son necesarios. Consulta Secondary/Data skipping indices.
Una vez más, esto se correlacionará con las columnas correspondientes a la especificación oficial de OTel para trazas documentada aquí. El esquema aquí emplea muchas de las mismas configuraciones que el esquema de logs anterior, con columnas Link adicionales específicas de los spans.
Recomendamos a los usuarios deshabilitar la creación automática de esquemas y crear sus tablas manualmente. Esto permite modificar las claves primarias y secundarias, así como introducir columnas adicionales para optimizar el rendimiento de las consultas. Para obtener más detalles, consulte Diseño de esquemas.
Optimización de las inserciones
Para lograr un alto rendimiento de inserción y, al mismo tiempo, obtener sólidas garantías de consistencia, debe seguir unas reglas sencillas al insertar datos de observabilidad en ClickHouse mediante el collector. Con la configuración correcta del OTel collector, las siguientes reglas deberían ser fáciles de seguir. Esto también evita problemas comunes que los usuarios encuentran al usar ClickHouse por primera vez.
De forma predeterminada, cada inserción enviada a ClickHouse hace que ClickHouse cree inmediatamente una parte de almacenamiento que contiene los datos de la inserción junto con otros metadatos que deben almacenarse. Por lo tanto, enviar menos inserciones con más datos en cada una, en lugar de más inserciones con menos datos en cada una, reducirá el número de escrituras necesarias. Recomendamos insertar datos en lotes relativamente grandes, de al menos 1.000 filas cada vez. Más detalles aquí.
De forma predeterminada, las inserciones en ClickHouse son síncronas e idempotentes si son idénticas. En las tablas de la familia de motores MergeTree, ClickHouse deduplicará automáticamente las inserciones de forma predeterminada. Esto significa que las inserciones toleran casos como los siguientes:
- (1) Si el nodo que recibe los datos tiene problemas, la consulta de inserción agotará el tiempo de espera (o devolverá un error más específico) y no se recibirá ninguna confirmación.
- (2) Si el nodo escribió los datos, pero la confirmación no puede devolverse al remitente de la consulta debido a interrupciones de red, el remitente recibirá un timeout o un error de red.
Desde la perspectiva del collector, puede ser difícil distinguir entre (1) y (2). Sin embargo, en ambos casos, la inserción no confirmada puede reintentarse de inmediato. Siempre que la consulta de inserción reintentada contenga los mismos datos en el mismo orden, ClickHouse ignorará automáticamente la inserción reintentada si la inserción original (no confirmada) se completó correctamente.
Recomendamos que los usuarios utilicen el batch processor mostrado en configuraciones anteriores para cumplir estos requisitos. Esto garantiza que las inserciones se envíen como lotes uniformes de filas que satisfacen los requisitos anteriores. Si se espera que un collector tenga alto throughput (eventos por segundo) y puedan enviarse al menos 10.000 eventos en cada inserción, normalmente esta es la única agrupación por lotes necesaria en la pipeline. Pueden usarse valores de hasta 100.000 si la memoria lo permite. En este caso, el collector vaciará los lotes antes de que se alcance el timeout del batch processor, lo que garantiza que la latencia de extremo a extremo de la pipeline se mantenga baja y que los lotes tengan un tamaño uniforme.
Usar inserciones asíncronas
Normalmente, los usuarios se ven obligados a enviar lotes más pequeños cuando el rendimiento de un collector es bajo, y aun así esperan que los datos lleguen a ClickHouse con una latencia mínima de extremo a extremo. En este caso, se envían lotes pequeños cuando expira el timeout del batch processor. Esto puede causar problemas, y es ahí donde se requieren las inserciones asíncronas. Este caso suele darse cuando los collectors en el rol de agent están configurados para enviar directamente a ClickHouse. Los gateways, al actuar como agregadores, pueden aliviar este problema; consulta Escalado con gateways.
Si no se pueden garantizar lotes grandes, puedes delegar el batching en ClickHouse usando Inserciones asíncronas. Con las inserciones asíncronas, los datos se insertan primero en un búfer y luego se escriben en el almacenamiento de la base de datos más tarde, es decir, de forma asíncrona.
Con las inserciones asíncronas habilitadas, cuando ClickHouse ① recibe una consulta de inserción, los datos de la consulta ② se escriben inmediatamente en un búfer en memoria. Cuando ③ se produce el siguiente vaciado del búfer, los datos del búfer se ordenan y se escriben como una parte en el almacenamiento de la base de datos. Ten en cuenta que los datos no se pueden consultar antes de escribirse en el almacenamiento de la base de datos; el vaciado del búfer es configurable.
Para habilitar las inserciones asíncronas para el collector, añade async_insert=1 a la cadena de conexión. Recomendamos usar wait_for_async_insert=1 (el valor predeterminado) para obtener garantías de entrega; consulta aquí para más detalles.
Los datos de una inserción asíncrona se insertan una vez que se vacía el búfer de ClickHouse. Esto ocurre cuando se supera async_insert_max_data_size o después de async_insert_busy_timeout_ms milisegundos desde la primera consulta INSERT. Si async_insert_stale_timeout_ms se establece en un valor distinto de cero, los datos se insertan después de async_insert_stale_timeout_ms milliseconds desde la última consulta. Puedes ajustar esta configuración para controlar la latencia de extremo a extremo de tu pipeline. Otras opciones que pueden usarse para ajustar el vaciado del búfer están documentadas aquí. En general, los valores predeterminados son adecuados.
Considere las inserciones asíncronas adaptativasEn casos en los que se usa un número reducido de agents, con bajo rendimiento pero requisitos estrictos de latencia de extremo a extremo, las inserciones asíncronas adaptativas pueden ser útiles. En general, no son aplicables a casos de uso de observabilidad de alto rendimiento, como los habituales en ClickHouse.
Por último, el comportamiento previo de deduplicación asociado con las inserciones síncronas en ClickHouse no está habilitado de forma predeterminada al usar inserciones asíncronas. Si es necesario, consulta la configuración async_insert_deduplicate.
Los detalles completos sobre cómo configurar esta función se pueden encontrar aquí, y un análisis más detallado aquí.
Arquitecturas de implementación
Al usar el OTel collector con ClickHouse, son posibles varias arquitecturas de implementación. A continuación, describimos cada una y en qué casos suele ser adecuada.
En una arquitectura de solo agentes, los usuarios implementan el OTel collector como agentes en el borde. Estos reciben trazas de aplicaciones locales (p. ej., como contenedor sidecar) y recopilan logs de servidores y nodos de Kubernetes. En este modo, los agentes envían sus datos directamente a ClickHouse.
Esta arquitectura es adecuada para implementaciones de pequeñas a medianas. Su principal ventaja es que no requiere hardware adicional y mantiene al mínimo la huella total de recursos de la solución de observabilidad de ClickHouse, con una correspondencia sencilla entre aplicaciones y collectors.
Debe considerar migrar a una arquitectura basada en gateway una vez que el número de agentes supere varios cientos. Esta arquitectura tiene varias desventajas que dificultan su escalado:
- Escalado de conexiones - Cada agente establecerá una conexión con ClickHouse. Aunque ClickHouse puede mantener cientos, si no miles, de conexiones de inserción concurrentes, esto acabará convirtiéndose en un factor limitante y hará que las inserciones sean menos eficientes; es decir, ClickHouse consumirá más recursos en mantener conexiones. El uso de gateways minimiza el número de conexiones y hace que las inserciones sean más eficientes.
- Procesamiento en el borde - En esta arquitectura, cualquier transformación o procesamiento de eventos debe realizarse en el borde o en ClickHouse. Además de ser restrictivo, esto puede implicar vistas materializadas complejas en ClickHouse o trasladar una carga de cómputo significativa al borde, donde los servicios críticos pueden verse afectados y los recursos pueden ser escasos.
- Lotes pequeños y latencias - Los collectors de agentes pueden recopilar individualmente muy pocos eventos. Esto normalmente significa que deben configurarse para vaciar el búfer a intervalos fijos a fin de cumplir los SLA de entrega. Como resultado, el collector puede enviar lotes pequeños a ClickHouse. Aunque esto supone una desventaja, puede mitigarse con inserciones asíncronas; consulte Optimización de inserciones.
Los OTel collectors pueden implementarse como instancias gateway para abordar las limitaciones anteriores. Estas proporcionan un servicio independiente, normalmente por centro de datos o por región. Reciben eventos de las aplicaciones (o de otros collectors en el rol de agente) a través de un único endpoint de OTLP. Normalmente, se implementa un conjunto de instancias gateway, con un balanceador de carga predeterminado para distribuir la carga entre ellas.
El objetivo de esta arquitectura es descargar de los agentes el procesamiento con uso intensivo de cómputo, minimizando así el consumo de recursos. Estos gateways pueden realizar tareas de transformación que, de otro modo, tendrían que ejecutar los agentes. Además, al agregar eventos de muchos agentes, los gateways pueden garantizar el envío de batches grandes a ClickHouse, lo que permite una inserción eficiente. Estos collectors gateway pueden escalarse fácilmente a medida que se añaden más agentes y aumenta el throughput de eventos. A continuación, se muestra una configuración de gateway de ejemplo, junto con una configuración de agente asociada que consume el archivo de log estructurado del ejemplo. Observe el uso de OTLP para la comunicación entre el agente y el gateway.
clickhouse-agent-config.yaml
clickhouse-gateway-config.yaml
Estas configuraciones pueden ejecutarse con los siguientes comandos.
La principal desventaja de esta arquitectura es el costo asociado y la sobrecarga operativa de gestionar un conjunto de collectors.
Para ver un ejemplo de cómo gestionar arquitecturas más grandes basadas en gateway, junto con las lecciones aprendidas, recomendamos esta entrada de blog.
Los lectores pueden notar que las arquitecturas anteriores no usan Kafka como cola de mensajes.
Usar una cola de Kafka como búfer de mensajes es un patrón de diseño popular en arquitecturas de logging y fue popularizado por el stack ELK. Ofrece varias ventajas; principalmente, ayuda a proporcionar garantías de entrega de mensajes más sólidas y a gestionar el backpressure. Los mensajes se envían desde los agentes de recopilación a Kafka y se escriben en disco. En teoría, una instancia de Kafka en clúster debería proporcionar un búfer de mensajes de alto rendimiento, ya que escribir datos linealmente en disco supone menos sobrecarga computacional que analizar y procesar un mensaje; en Elastic, por ejemplo, la tokenización y la indexación generan una sobrecarga considerable. Al alejar los datos de los agentes, también se reduce el riesgo de perder mensajes como resultado de la rotación de logs en el origen. Por último, ofrece ciertas capacidades de reproducción de mensajes y replicación entre regiones, lo que puede resultar atractivo para algunos casos de uso.
Sin embargo, ClickHouse puede gestionar la inserción de datos muy rápidamente: millones de filas por segundo con hardware moderado. El backpressure desde ClickHouse es poco frecuente. A menudo, aprovechar una cola de Kafka implica más complejidad arquitectónica y mayor costo. Si puede asumir el principio de que los logs no necesitan las mismas garantías de entrega que las transacciones bancarias y otros datos de misión crítica, recomendamos evitar la complejidad de Kafka.
Sin embargo, si necesita altas garantías de entrega o la capacidad de reproducir datos (potencialmente hacia múltiples destinos), Kafka puede ser una incorporación arquitectónica útil.
En este caso, los agentes de OTel pueden configurarse para enviar datos a Kafka mediante el exportador de Kafka. A su vez, las instancias gateway consumen mensajes mediante el receptor de Kafka. Recomendamos consultar la documentación de Confluent y de OTel para obtener más detalles.
Los requisitos de recursos del OTel collector dependen del throughput de eventos, del tamaño de los mensajes y de la cantidad de procesamiento que se realice. El proyecto OpenTelemetry mantiene benchmarks que los usuarios pueden usar para estimar los requisitos de recursos.
Según nuestra experiencia, una instancia gateway con 3 núcleos y 12 GB de RAM puede manejar alrededor de 60 mil eventos por segundo. Esto supone un pipeline de procesamiento mínimo, encargado de renombrar campos y sin usar expresiones regulares.
Para las instancias agent encargadas de enviar eventos a un gateway, y solo de establecer el timestamp del evento, recomendamos dimensionarlas en función de la cantidad prevista de logs por segundo. A continuación se muestran cifras aproximadas que puede usar como punto de partida: