Qualquer solução de observabilidade requer uma forma de coletar e exportar logs e traces. Para isso, o ClickHouse recomenda o projeto OpenTelemetry (OTel).
“OpenTelemetry é um framework e kit de ferramentas de observabilidade projetado para criar e gerenciar dados de telemetria, como traces, métricas e logs.”
Ao contrário do ClickHouse ou do Prometheus, o OpenTelemetry não é um backend de observabilidade; em vez disso, ele se concentra na geração, coleta, gerenciamento e exportação de dados de telemetria. Embora o objetivo inicial do OpenTelemetry fosse permitir instrumentar facilmente aplicações ou sistemas usando SDKs específicos de linguagem, ele passou a incluir também a coleta de logs por meio do collector OpenTelemetry - um agente ou proxy que recebe, processa e exporta dados de telemetria.
Componentes relevantes do ClickHouse
O OpenTelemetry consiste em vários componentes. Além de fornecer uma especificação de dados e de API, um protocolo padronizado e convenções de nomenclatura para campos/colunas, o OTel oferece dois recursos fundamentais para criar uma solução de observabilidade com ClickHouse:
- O OpenTelemetry Collector é um proxy que recebe, processa e exporta dados de telemetria. Uma solução baseada em ClickHouse usa esse componente tanto para a coleta de logs quanto para o processamento de eventos antes do agrupamento em lotes e da inserção.
- SDKs de linguagem que implementam a especificação, as APIs e a exportação de dados de telemetria. Esses SDKs garantem, na prática, que traces sejam registrados corretamente no código da aplicação, gerando os spans que os compõem e assegurando a propagação do contexto entre serviços por meio de metadados — formando, assim, traces distribuídos e permitindo correlacionar spans. Esses SDKs são complementados por um ecossistema que instrumenta automaticamente bibliotecas e frameworks comuns, o que significa que o usuário não precisa alterar seu código e obtém instrumentação pronta para uso.
Uma solução de observabilidade baseada em ClickHouse aproveita essas duas ferramentas.
O collector OpenTelemetry tem diversas distribuições. O receiver filelog, junto com o exportador ClickHouse, necessários para uma solução com ClickHouse, estão presentes apenas na OpenTelemetry Collector Contrib Distro.
Essa distribuição contém muitos componentes e permite experimentar várias configurações. No entanto, em produção, recomenda-se limitar o collector para incluir apenas os componentes necessários para o ambiente. Alguns motivos para fazer isso:
- Reduzir o tamanho do collector, diminuindo o tempo de implantação
- Melhorar a segurança do collector, reduzindo a superfície de ataque disponível
A criação de um collector personalizado pode ser feita usando o OpenTelemetry Collector Builder.
Papéis de implantação do collector
Para coletar logs e inseri-los no ClickHouse, recomendamos usar o OpenTelemetry Collector. O OpenTelemetry Collector pode ser implantado em dois papéis principais:
- Agent - As instâncias de agente coletam dados na borda, por exemplo, em servidores ou nós do Kubernetes, ou recebem eventos diretamente de aplicações instrumentadas com um SDK do OpenTelemetry. Neste último caso, a instância do agente é executada junto com a aplicação ou no mesmo host da aplicação (como um sidecar ou um Conjunto de Daemon). Os agentes podem enviar seus dados diretamente para o ClickHouse ou para uma instância de gateway. No primeiro caso, isso é chamado de padrão de implantação de agente.
- Gateway - As instâncias de gateway fornecem um serviço independente (por exemplo, uma implantação no Kubernetes), normalmente por cluster, por data center ou por região. Elas recebem eventos de aplicações (ou de outros collectors atuando como agentes) por meio de um único endpoint OTLP. Normalmente, um conjunto de instâncias de gateway é implantado, com um balanceador de carga pronto para uso distribuindo a carga entre elas. Se todos os agentes e aplicações enviarem seus sinais para esse único endpoint, isso geralmente é chamado de padrão de implantação de gateway.
Abaixo, assumimos um collector do tipo agente simples, enviando seus eventos diretamente para o ClickHouse. Consulte Escalabilidade com Gateways para mais detalhes sobre o uso de gateways e quando eles são aplicáveis.
A principal vantagem de usar um collector é permitir que seus serviços descarreguem os dados rapidamente, deixando o Collector cuidar de etapas adicionais, como novas tentativas, agrupamento em lotes, criptografia e até filtragem de dados sensíveis.
O Collector usa os termos receiver, processador e exportador para seus três principais estágios de processamento. Receivers são usados para a coleta de dados e podem operar por extração ou envio. Processadores permitem realizar transformações e enriquecimento das mensagens. Exportadores são responsáveis por enviar os dados para um serviço downstream. Embora esse serviço possa, em teoria, ser outro collector, assumimos que todos os dados sejam enviados diretamente ao ClickHouse na discussão inicial abaixo.
Recomendamos que os usuários se familiarizem com o conjunto completo de receivers, processadores e exportadores.
O collector oferece dois principais receivers para coletar logs:
Via OTLP - Nesse caso, os logs são enviados (push) diretamente ao collector a partir dos SDKs do OpenTelemetry por meio do protocolo OTLP. A demo do OpenTelemetry usa essa abordagem, com os exportadores OTLP em cada linguagem assumindo um endpoint local do collector. Nesse caso, o collector deve ser configurado com o receiver OTLP — veja a demo acima para uma configuração. A vantagem dessa abordagem é que os dados de log conterão automaticamente IDs de trace, permitindo que os usuários identifiquem depois os traces de um log específico e vice-versa.
Essa abordagem exige que os usuários instrumentem seu código com o SDK de linguagem apropriado.
- Coleta via filelog receiver - Esse receiver acompanha arquivos em disco e gera log messages, enviando-as ao ClickHouse. Esse receiver lida com tarefas complexas, como detectar mensagens de várias linhas, tratar rotações de logs, fazer checkpointing para maior robustez em reinicializações e extrair estrutura. Além disso, esse receiver também consegue acompanhar logs de contêineres Docker e Kubernetes, podendo ser implantado como um Chart do Helm, extraindo a estrutura deles e enriquecendo-os com os detalhes do pod do Kubernetes.
A maioria das implantações usará uma combinação dos receivers acima. Recomendamos que os usuários leiam a documentação do collector e se familiarizem com os conceitos básicos, além da estrutura de configuração e dos métodos de instalação.
Dica: otelbin.iootelbin.io é útil para validar e visualizar configurações.
Estruturados vs. não estruturados
Os logs podem ser estruturados ou não estruturados.
Um log estruturado usa um formato de dados como JSON, definindo campos de metadados como código HTTP e endereço IP de origem.
Os logs não estruturados, embora geralmente também tenham alguma estrutura inerente que possa ser extraída por meio de um padrão regex, representarão o log apenas como uma string.
Recomendamos que os usuários usem logs estruturados e, sempre que possível, façam o registro em JSON (ou seja, ndjson). Isso simplificará o processamento necessário dos logs mais adiante, seja antes do envio ao ClickHouse com processadores do collector ou no momento da inserção, usando visões materializadas. No fim das contas, logs estruturados economizam recursos de processamento posteriores, reduzindo a CPU necessária na sua solução ClickHouse.
Para fins de exemplo, fornecemos um conjunto de dados de logs estruturados (JSON) e não estruturados, cada um com aproximadamente 10 milhões de linhas, disponíveis nos links a seguir:
Usamos o conjunto de dados estruturado no exemplo abaixo. Certifique-se de que esse arquivo foi baixado e extraído para reproduzir os exemplos a seguir.
A seguir está uma configuração simples do OTel Collector que lê esses arquivos do disco usando o filelog receiver e envia as mensagens resultantes para stdout. Usamos o operador json_parser porque nossos logs são estruturados. Modifique o caminho para o arquivo access-structured.log.
Considere usar o ClickHouse para fazer o parsingO exemplo abaixo extrai o timestamp do log. Isso exige o uso do operador json_parser, que converte toda a linha de log em uma string JSON, colocando o resultado em LogAttributes. Isso pode ter um custo computacional alto e pode ser feito com mais eficiência no ClickHouse - Extração de estrutura com SQL. Um exemplo equivalente com logs não estruturados, que usa o regex_parser para fazer isso, pode ser encontrado aqui.
config-structured-logs.yaml
Você pode seguir as instruções oficiais para instalar o collector localmente. É importante garantir que as instruções sejam adaptadas para usar a distribuição contrib (que contém o receiver filelog); por exemplo, em vez de otelcol_0.102.1_darwin_arm64.tar.gz, os usuários baixariam otelcol-contrib_0.102.1_darwin_arm64.tar.gz. As versões podem ser encontradas aqui.
Depois de instalado, o OTel collector pode ser executado com os seguintes comandos:
Ao usar logs estruturados, as mensagens terão o seguinte formato na saída:
O texto acima representa uma única mensagem de log, conforme gerada pelo OTel collector. Fazemos a ingestão dessas mesmas mensagens no ClickHouse nas seções posteriores.
O esquema completo das mensagens de log, juntamente com colunas adicionais que podem estar presentes ao usar outros receivers, é mantido aqui. Recomendamos fortemente que os usuários se familiarizem com esse esquema.
O ponto principal aqui é que a própria linha de log é armazenada como uma string no campo Body, mas o JSON foi extraído automaticamente para o campo Attributes graças ao json_parser. Esse mesmo operador foi usado para extrair o timestamp para a coluna Timestamp apropriada. Para recomendações sobre como processar logs com OTel, consulte Processing.
OperadoresOperadores são a unidade mais básica do processamento de logs. Cada operador cumpre uma única função, como ler linhas de um arquivo ou fazer o parsing de JSON de um campo. Em seguida, os operadores são encadeados em um pipeline para alcançar o resultado desejado.
As mensagens acima não têm um campo TraceID nem SpanID. Se esses campos estiverem presentes, por exemplo, em casos em que os usuários estejam implementando rastreamento distribuído, eles poderão ser extraídos do JSON usando as mesmas técnicas mostradas acima.
Para usuários que precisam coletar arquivos de log locais ou do Kubernetes, recomendamos que se familiarizem com as opções de configuração disponíveis para o filelog receiver e com a forma como offsets e o parsing de logs multilinha é tratado.
Coleta de logs do Kubernetes
Para a coleta de logs do Kubernetes, recomendamos o guia da documentação do OpenTelemetry. O Kubernetes Attributes Processor é recomendado para enriquecer logs e métricas com metadados dos pods. Isso pode gerar metadados dinâmicos, por exemplo, labels, armazenados na coluna ResourceAttributes. Atualmente, o ClickHouse usa o tipo Map(String, String) para essa coluna. Consulte Usando Maps e Extraindo de maps para mais detalhes sobre como tratar e otimizar esse tipo.
Para usuários que desejam instrumentar seu código e coletar traces, recomendamos seguir a documentação oficial do OTel.
Para enviar eventos ao ClickHouse, você precisará implantar um OTel collector para receber eventos de trace pelo protocolo OTLP, por meio do receiver apropriado. A demonstração do OpenTelemetry fornece um exemplo de instrumentação para cada linguagem compatível e de envio de eventos para um collector. Um exemplo de configuração adequada de collector que envia eventos para stdout é mostrado abaixo:
Como os traces devem ser recebidos via OTLP, usamos a ferramenta telemetrygen para gerar dados de trace. Siga as instruções aqui para instalar.
A configuração a seguir recebe eventos de trace por um receiver OTLP antes de enviá-los para stdout.
config-traces.xml
Execute esta configuração usando:
Envie eventos de trace ao collector usando telemetrygen:
Isso resultará em mensagens de trace semelhantes ao exemplo abaixo, sendo enviadas para stdout:
O texto acima representa uma única mensagem de trace, gerada pelo OTel collector. Fazemos a ingestão dessas mesmas mensagens no ClickHouse nas seções seguintes.
O esquema completo das mensagens de trace é mantido aqui. Recomendamos fortemente que os usuários se familiarizem com esse esquema.
Como demonstrado no exemplo anterior de definição do timestamp de um evento de log, você inevitavelmente vai querer filtrar, transformar e enriquecer mensagens de evento. Isso pode ser feito usando vários recursos do OpenTelemetry:
-
Processadores - Os processadores pegam os dados coletados pelos receivers e os modificam ou transformam antes de enviá-los aos exporters. Os processadores são aplicados na ordem configurada na seção
processors da configuração do collector. Eles são opcionais, mas o conjunto mínimo normalmente é recomendado. Ao usar um OTel collector com ClickHouse, recomendamos limitar os processadores a:
- Um memory_limiter é usado para evitar situações de falta de memória no collector. Consulte Estimando recursos para recomendações.
- Qualquer processador que faça enriquecimento com base em contexto. Por exemplo, o Kubernetes Attributes Processor permite definir automaticamente atributos de recurso de spans, métricas e logs com metadados do k8s, por exemplo, enriquecendo eventos com o ID do pod de origem.
- Tail ou head sampling se necessário para traces.
- Filtragem básica - descarte de eventos desnecessários, caso isso não possa ser feito por meio de operator (veja abaixo).
- Batching - essencial ao trabalhar com ClickHouse para garantir que os dados sejam enviados em lotes. Consulte “Exportando para ClickHouse”.
-
Operators - Operators fornecem a unidade mais básica de processamento disponível no receiver. Há suporte a parsing básico, permitindo definir campos como Severity e Timestamp. Há suporte a parsing de JSON e regex, além de filtragem de eventos e transformações básicas. Recomendamos fazer a filtragem de eventos aqui.
Recomendamos que os usuários evitem fazer processamento excessivo de eventos usando operators ou transform processors. Eles podem gerar uma sobrecarga considerável de memória e CPU, especialmente no parsing de JSON. É possível fazer todo o processamento no ClickHouse no momento da inserção com visões materializadas e colunas, com algumas exceções - especificamente, enriquecimento sensível ao contexto, por exemplo, a adição de metadados do k8s. Para mais detalhes, consulte Extraindo estrutura com SQL.
Se o processamento for feito com o OTel collector, recomendamos realizar as transformações nas instâncias de gateway e minimizar qualquer trabalho feito nas instâncias de agent. Isso garantirá que os recursos exigidos pelos agents na borda, executados em servidores, sejam os menores possíveis. Normalmente, vemos usuários fazendo apenas filtragem (para minimizar o uso desnecessário da rede), definição de timestamp (via operators) e enriquecimento, que exige contexto nos agents. Por exemplo, se as instâncias de gateway estiverem em um cluster Kubernetes diferente, o enriquecimento de k8s precisará ocorrer no agent.
A configuração a seguir mostra a coleta de um arquivo de log não estruturado. Observe o uso de operadores para extrair estrutura das linhas de log (regex_parser) e filtrar eventos, juntamente com um processador para agrupar eventos em lotes e limitar o uso de memória.
config-unstructured-logs-with-processor.yaml
Exportando para o ClickHouse
Exportadores enviam dados para um ou mais backends ou destinos. Os exportadores podem ser baseados em extração ou em envio. Para enviar eventos ao ClickHouse, você precisará usar o ClickHouse exporter baseado em envio.
Um arquivo de configuração completo é mostrado abaixo.
clickhouse-config.yaml
Observe as seguintes configurações importantes:
- pipelines - A configuração acima destaca o uso de pipelines, compostos por um conjunto de receivers, processors e exporters, com um pipeline para logs e traces.
- endpoint - A comunicação com o ClickHouse é configurada por meio do parâmetro
endpoint. A string de conexão tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1 faz com que a comunicação ocorra via TCP. Se você preferir HTTP por motivos de alternância de tráfego, modifique essa string de conexão conforme descrito aqui. Os detalhes completos da conexão, incluindo a possibilidade de especificar nome de usuário e senha nessa string de conexão, estão descritos aqui.
Importante: Observe que a string de conexão acima habilita tanto a compressão (lz4) quanto os inserts assíncronos. Recomendamos que ambos estejam sempre habilitados. Consulte Batching para mais detalhes sobre inserts assíncronos. A compressão deve sempre ser especificada e, em versões mais antigas do exporter, não é habilitada por padrão.
- ttl - o valor aqui determina por quanto tempo os dados são retidos. Mais detalhes em “Gerenciando dados”. Isso deve ser especificado como uma unidade de tempo em horas, por exemplo, 72h. Desabilitamos o TTL no exemplo abaixo, já que nossos dados são de 2019 e serão removidos pelo ClickHouse imediatamente se forem inseridos.
- traces_table_name e logs_table_name - determinam o nome das tabelas de logs e traces.
- create_schema - determina se as tabelas são criadas com os schemas padrão na inicialização. O padrão é true para getting started. Você deve defini-lo como false e definir seu próprio schema.
- database - banco de dados de destino.
- retry_on_failure - configurações que determinam se batches com falha devem ser tentados novamente.
- batch - um batch processor garante que os eventos sejam enviados em batches. Recomendamos um valor de pelo menos 10.000 com um timeout de 5s (valores de até 100.000 podem ser usados se a memória permitir). O que for atingido primeiro iniciará um batch a ser enviado ao exporter. Reduzir esses valores resultará em uma pipeline de menor latência, com dados disponíveis para consulta mais cedo, ao custo de mais connections e batches enviados ao ClickHouse. Isso não é recomendado se você não estiver usando inserts assíncronos, pois pode causar problemas de partes em excesso no ClickHouse. Por outro lado, se você estiver usando inserts assíncronos, a disponibilidade desses dados para consulta também dependerá das configurações de insert assíncrono — embora os dados ainda sejam enviados do connector mais cedo. Consulte Batching para mais detalhes.
- sending_queue - controla o tamanho da fila de envio. Cada item na fila contém um batch. Se essa fila for excedida, por exemplo, porque o ClickHouse está inacessível, mas os eventos continuam chegando, os batches serão descartados.
Supondo que os usuários tenham extraído o arquivo de log estruturado e tenham uma instância local do ClickHouse em execução (com autenticação padrão), você pode executar essa configuração com o comando:
Para enviar dados de trace para este collector, execute o seguinte comando usando a ferramenta telemetrygen:
Quando estiver em execução, confirme com uma consulta simples se os eventos de log estão presentes:
O ClickStack oferece um schema padrão otimizadoO ClickStack fornece schemas prontos para uso para logs, traces e métricas que incorporam os recursos mais recentes do ClickHouse (índices de texto para busca de texto completo e por chave de map, colunas materializadas e arrays ALIAS para filtragem por leitura direta, lookups de linha por número de bloco) e foram submetidos a benchmark para oferecer bom desempenho imediato para workloads de logging e traces. Use-os como ponto de referência para o seu próprio design.
Por padrão, o exportador do ClickHouse cria tabelas de destino para logs e traces. Isso pode ser desativado por meio da configuração create_schema. Além disso, os nomes das tabelas de logs e traces podem ser alterados em relação aos padrões otel_logs e otel_traces por meio das configurações indicadas acima.
Nos schemas abaixo, assumimos que o TTL está habilitado para 72h.
O schema padrão para logs é mostrado abaixo (otelcol-contrib v0.102.1):
As colunas aqui correspondem à especificação oficial do OTel para logs, documentada aqui.
Algumas observações importantes sobre este schema:
- Por padrão, a tabela é particionada por data via
PARTITION BY toDate(Timestamp). Isso torna eficiente remover dados expirados.
- O TTL é definido via
TTL toDateTime(Timestamp) + toIntervalDay(3) e corresponde ao valor definido na configuração do collector. ttl_only_drop_parts=1 significa que apenas partes inteiras são removidas quando todas as linhas que elas contêm tiverem expirado. Isso é mais eficiente do que remover linhas dentro das partes, o que implica uma operação de delete custosa. Recomendamos que isso esteja sempre definido. Consulte Gerenciamento de dados com TTL para mais detalhes.
- A tabela usa o motor clássico
MergeTree. Isso é recomendado para logs e traces e não deve precisar ser alterado.
- A tabela é ordenada por
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId). Isso significa que as consultas serão otimizadas para filtros em ServiceName, SeverityText, Timestamp e TraceId — colunas mais no início da lista serão filtradas mais rapidamente do que as posteriores; por exemplo, filtrar por ServiceName será significativamente mais rápido do que filtrar por TraceId. Você deve modificar essa ordenação de acordo com os padrões de acesso esperados — consulte Escolhendo uma chave primária.
- O esquema acima aplica
ZSTD(1) às colunas. Isso oferece a melhor compressão para logs. Você pode aumentar o nível de compressão do ZSTD (acima do padrão de 1) para obter uma compressão melhor, embora isso raramente seja benéfico. Aumentar esse valor acarretará maior sobrecarga de CPU no momento do insert (durante a compressão), embora a descompressão (e, portanto, as consultas) deva permanecer comparável. Consulte aqui para mais detalhes. A codificação delta adicional também é aplicada ao Timestamp com o objetivo de reduzir seu tamanho em disco.
- Observe como
ResourceAttributes, LogAttributes e ScopeAttributes são map. É importante entender as diferenças entre eles. Consulte “Usando map” para saber como acessar esses map e otimizar o acesso às chaves dentro deles.
- A maioria dos outros tipos aqui, por exemplo
ServiceName como LowCardinality, está otimizada. Observe que Body, que é JSON em nossos logs de exemplo, é armazenado como String.
- Filtros de Bloom são aplicados às chaves e aos valores dos map, bem como à coluna
Body. Eles têm como objetivo melhorar os tempos de consulta para consultas que acessam essas colunas, mas normalmente não são necessários. Consulte Índices secundários/data skipping indices.
Novamente, isso se correlacionará com as colunas correspondentes à especificação oficial do OTel para traces, documentada aqui. O schema aqui usa muitas das mesmas configurações do schema de logs acima, com colunas Link adicionais específicas para spans.
Recomendamos que os usuários desativem a criação automática de schema e criem suas tabelas manualmente. Isso permite modificar as chaves primária e secundária, além de possibilitar a introdução de colunas adicionais para otimizar o desempenho das consultas. Para mais detalhes, consulte Schema design.
Para alcançar alto desempenho nas inserções e, ao mesmo tempo, obter fortes garantias de consistência, você deve seguir regras simples ao inserir dados de observabilidade no ClickHouse por meio do collector. Com a configuração correta do OTel collector, as regras a seguir devem ser fáceis de aplicar. Isso também evita problemas comuns que os usuários encontram ao usar o ClickHouse pela primeira vez.
Por padrão, cada inserção enviada ao ClickHouse faz com que o ClickHouse crie imediatamente uma parte de armazenamento contendo os dados da inserção, junto com outros metadados que também precisam ser armazenados. Portanto, enviar menos inserções, cada uma com mais dados, em vez de enviar mais inserções, cada uma com menos dados, reduz o número de gravações necessárias. Recomendamos inserir dados em lotes relativamente grandes, com pelo menos 1.000 linhas por vez. Mais detalhes aqui.
Por padrão, as inserções no ClickHouse são síncronas e idempotentes quando idênticas. Para tabelas da família de engines MergeTree, o ClickHouse, por padrão, desduplica inserções automaticamente. Isso significa que as inserções toleram casos como os seguintes:
- (1) Se o nó que recebe os dados apresentar problemas, a consulta de inserção atingirá o tempo limite (ou retornará um erro mais específico) e não receberá uma confirmação.
- (2) Se os dados forem gravados pelo nó, mas a confirmação não puder ser devolvida ao remetente da consulta devido a interrupções de rede, o remetente receberá um timeout ou um erro de rede.
Na perspectiva do collector, pode ser difícil distinguir entre (1) e (2). No entanto, em ambos os casos, a inserção sem confirmação pode simplesmente ser repetida imediatamente. Desde que a consulta de inserção repetida contenha os mesmos dados na mesma ordem, o ClickHouse ignorará automaticamente a nova tentativa de inserção se a inserção original (sem confirmação) tiver sido bem-sucedida.
Recomendamos que os usuários usem o batch processor mostrado nas configurações anteriores para atender a esses requisitos. Isso garante que as inserções sejam enviadas em lotes consistentes de linhas, em conformidade com os requisitos acima. Se for esperado que um collector tenha alto throughput (eventos por segundo), e pelo menos 10.000 eventos puderem ser enviados em cada inserção, normalmente esse é o único processamento em lotes necessário no pipeline. Valores de até 100.000 podem ser usados, se a memória permitir. Nesse caso, o collector fará o flush dos lotes antes que o timeout do batch processor seja atingido, garantindo que a latência de ponta a ponta do pipeline permaneça baixa e que os lotes tenham tamanho consistente.
Use inserções assíncronas
Normalmente, os usuários precisam enviar batches menores quando o throughput de um collector é baixo, mas ainda esperam que os dados cheguem ao ClickHouse com a menor latência ponta a ponta possível. Nesse caso, batches pequenos são enviados quando o timeout do batch processor expira. Isso pode causar problemas, e é aí que as inserções assíncronas se tornam necessárias. Esse cenário geralmente ocorre quando collectors na função de agent são configurados para enviar dados diretamente ao ClickHouse. Gateways, por atuarem como agregadores, podem amenizar esse problema — veja Escalabilidade com Gateways.
Se não for possível garantir batches grandes, você pode delegar o batching ao ClickHouse usando Asynchronous Inserts. Com inserções assíncronas, os dados são inseridos primeiro em um buffer e depois gravados no armazenamento do banco de dados posteriormente, ou seja, de forma assíncrona.
Com inserções assíncronas habilitadas, quando o ClickHouse ① recebe uma consulta de insert, os dados da consulta são ② gravados imediatamente em um buffer na memória. Quando ③ ocorre o próximo flush do buffer, os dados do buffer são ordenados e gravados como uma parte no armazenamento do banco de dados. Observe que os dados não podem ser consultados antes de serem gravados no armazenamento do banco de dados; o flush do buffer é configurável.
Para habilitar inserções assíncronas no collector, adicione async_insert=1 à connection string. Recomendamos que os usuários usem wait_for_async_insert=1 (o padrão) para ter garantias de entrega — veja aqui para mais detalhes.
Os dados de uma inserção assíncrona são inseridos assim que o buffer do ClickHouse é descarregado. Isso acontece quando async_insert_max_data_size é excedido ou após async_insert_busy_timeout_ms milissegundos desde a primeira consulta INSERT. Se async_insert_stale_timeout_ms estiver definido com um valor diferente de zero, os dados serão inseridos após async_insert_stale_timeout_ms milliseconds desde a última consulta. Você pode ajustar essas configurações para controlar a latência ponta a ponta do pipeline. Outras configurações que podem ser usadas para ajustar o flush do buffer estão documentadas aqui. Em geral, os valores padrão são adequados.
Considere inserções assíncronas adaptativasNos casos em que há poucos agents em uso, com baixo throughput, mas com requisitos rígidos de latência ponta a ponta, adaptive asynchronous inserts podem ser úteis. Em geral, elas não se aplicam a casos de uso de observabilidade com alto throughput, como os vistos com ClickHouse.
Por fim, o comportamento anterior de desduplicação associado às inserções síncronas no ClickHouse não é habilitado por padrão ao usar inserções assíncronas. Se necessário, consulte a configuração async_insert_deduplicate.
Os detalhes completos sobre como configurar esse recurso podem ser encontrados aqui, com uma análise mais aprofundada aqui.
Arquiteturas de implantação
Várias arquiteturas de implantação são possíveis ao usar o OTel collector com o ClickHouse. Descrevemos cada uma abaixo e em quais casos ela tende a ser mais adequada.
Em uma arquitetura somente com agents, os usuários implantam o OTel collector como agents na borda. Eles recebem traces de aplicações locais (por exemplo, como um contêiner sidecar) e coletam logs de servidores e nós do Kubernetes. Nesse modo, os agents enviam seus dados diretamente para o ClickHouse.
Essa arquitetura é apropriada para implantações de pequeno a médio porte. Sua principal vantagem é que não requer hardware adicional e mantém mínimo o consumo total de recursos da solução de observabilidade do ClickHouse, com um mapeamento simples entre aplicações e collectors.
Você deve considerar migrar para uma arquitetura baseada em gateway quando o número de agents ultrapassar várias centenas. Essa arquitetura tem várias desvantagens que tornam sua escalabilidade desafiadora:
- Escalabilidade das conexões - Cada agent estabelecerá uma conexão com o ClickHouse. Embora o ClickHouse seja capaz de manter centenas (senão milhares) de conexões de inserção concorrentes, isso acabará se tornando um fator limitante e tornará as inserções menos eficientes — ou seja, o ClickHouse usará mais recursos para manter essas conexões. O uso de gateways minimiza o número de conexões e torna as inserções mais eficientes.
- Processamento na borda - Quaisquer transformações ou processamentos de eventos precisam ser executados na borda ou no ClickHouse nessa arquitetura. Além de ser restritivo, isso pode significar visões materializadas complexas no ClickHouse ou deslocar uma carga computacional significativa para a borda — onde serviços críticos podem ser impactados e os recursos podem ser escassos.
- Lotes pequenos e latências - Os collectors como agents podem, individualmente, coletar pouquíssimos eventos. Isso normalmente significa que eles precisam ser configurados para fazer flush em um intervalo definido a fim de atender aos SLAs de entrega. Isso pode fazer com que o collector envie pequenos lotes ao ClickHouse. Embora seja uma desvantagem, isso pode ser mitigado com inserções assíncronas — consulte Otimizando inserções.
OTel collectors podem ser implantados como instâncias de gateway para contornar as limitações mencionadas acima. Eles fornecem um serviço independente, normalmente por data center ou por região. Essas instâncias recebem eventos de aplicações (ou de outros collectors no papel de agent) por meio de um único endpoint OTLP. Normalmente, um conjunto de instâncias de gateway é implantado, com um load balancer nativo sendo usado para distribuir a carga entre elas.
O objetivo desta arquitetura é descarregar dos agents o processamento computacionalmente intensivo, minimizando assim o uso de recursos. Esses gateways podem executar tarefas de transformação que, de outra forma, precisariam ser realizadas pelos agents. Além disso, ao agregar eventos de muitos agents, os gateways podem garantir o envio de batches maiores ao ClickHouse, permitindo uma inserção eficiente. Esses collectors de gateway podem ser facilmente escalados à medida que mais agents são adicionados e o throughput de eventos aumenta. Um exemplo de configuração de gateway, com uma configuração de agent associada consumindo o arquivo de log estruturado de exemplo, é mostrado abaixo. Observe o uso de OTLP na comunicação entre o agent e o gateway.
clickhouse-agent-config.yaml
clickhouse-gateway-config.yaml
Estas configurações podem ser executadas com os comandos a seguir.
A principal desvantagem dessa arquitetura é o custo e a sobrecarga associados ao gerenciamento de um conjunto de collectors.
Para ver um exemplo de gerenciamento de arquiteturas maiores baseadas em gateway e os aprendizados relacionados, recomendamos esta postagem no blog.
Os leitores podem notar que as arquiteturas acima não usam o Kafka como fila de mensagens.
Usar uma fila do Kafka como buffer de mensagens é um padrão de arquitetura popular em arquiteturas de logging e que foi popularizado pela stack ELK. Isso oferece alguns benefícios; principalmente, ajuda a fornecer garantias mais fortes de entrega de mensagens e a lidar com backpressure. As mensagens são enviadas dos agents de coleta para o Kafka e gravadas em disco. Em teoria, uma instância Kafka em cluster deve fornecer um buffer de mensagens de alta vazão, já que gravar dados linearmente em disco gera menos sobrecarga computacional do que analisar e processar uma mensagem — no Elastic, por exemplo, a tokenização e a indexação geram uma sobrecarga significativa. Ao afastar os dados dos agents, você também reduz o risco de perder mensagens como resultado da rotação de logs na origem. Por fim, isso oferece alguns recursos de reprocessamento de mensagens e replicação entre regiões, o que pode ser atraente em alguns casos de uso.
No entanto, o ClickHouse consegue inserir dados muito rapidamente — milhões de linhas por segundo em hardware moderado. Backpressure do ClickHouse é raro. Muitas vezes, usar uma fila Kafka significa mais complexidade arquitetural e mais custo. Se você puder adotar o princípio de que logs não precisam das mesmas garantias de entrega que transações bancárias e outros dados de missão crítica, recomendamos evitar a complexidade do Kafka.
No entanto, se você precisar de altas garantias de entrega ou da capacidade de reprocessar dados (potencialmente para várias fontes), o Kafka pode ser uma adição útil à arquitetura.
Nesse caso, os agents OTel podem ser configurados para enviar dados ao Kafka por meio do exportador Kafka. As instâncias de gateway, por sua vez, consomem mensagens usando o receiver Kafka. Recomendamos a documentação da Confluent e do OTel para mais detalhes.
Os requisitos de recursos do OTel collector dependem da taxa de eventos, do tamanho das mensagens e da quantidade de processamento realizada. O projeto OpenTelemetry mantém benchmarks que os usuários podem usar para estimar os requisitos de recursos.
Na nossa experiência, uma instância de gateway com 3 núcleos e 12 GB de RAM pode processar cerca de 60 mil eventos por segundo. Isso pressupõe um pipeline de processamento mínimo, responsável por renomear campos e sem usar expressões regulares.
Para instâncias de agent responsáveis por enviar eventos para um gateway e apenas definir o timestamp do evento, recomendamos que os usuários dimensionem com base no volume previsto de logs por segundo. Os valores a seguir são aproximados e podem ser usados como ponto de partida: