sh4.tech

Nota 4 min de leitura Kafka · ClickHouse · validação

1,8 milhão de mensagens, 200 milhões de linhas: conferindo o Kafka contra o ClickHouse

Nossos players mandam uma mensagem no Kafka por envio, com cerca de 112 linhas cada. Como contei um dia de mensagens só pelos offsets, comparei com as linhas que o ClickHouse realmente inseriu e cheguei a 13.912.374 contra 13.912.522.

Quando aconteceu
Publicado
Medido
, produção, só leitura: offsets do Kafka e system.part_log do ClickHouse
Stack
Kafka (3 brokers, KRaft), confluent-kafka em Python, ClickHouse, Apache NiFi
Também em
English

Uma mensagem, cem linhas

Nossas telas mandam a prova de exibição em lote. Cada envio de um player vira uma mensagem no Kafka com todas as linhas daquele envio, separadas por quebra de linha. Numa amostra pequena que li em 5 de outubro, a mensagem mediana do tópico principal de auditoria tinha 12,7 KB e 112 linhas. É por isso que cerca de 200 milhões de linhas por dia viajam em menos de 2 milhões de mensagens.

A gente desenhou esse pipeline junto, no time; eu prototipei e testei a abordagem, e um colega escreveu e colocou no ar a versão final do consumidor. A conferência desta nota é minha. Eu queria duas respostas que os painéis não davam. Quantas linhas uma mensagem carrega de verdade? E o que entra no Kafka sai no ClickHouse, linha por linha?

Contar mensagens sem consumir

Os offsets já têm a resposta. Peça o offset da primeira mensagem depois de "24 horas atrás", peça o fim atual de cada partição e subtraia. Isso lê só metadados, nenhuma mensagem, nenhum commit, então não atrapalha os consumidores de verdade:

mensagens nas últimas 24 h, pelos offsets
import time
from confluent_kafka import Consumer, TopicPartition

c = Consumer({
    "bootstrap.servers": BROKERS,
    "group.id": "readonly-count-tmp",     # descartável, nunca faz subscribe
    "enable.auto.commit": False,
})

def messages_last_24h(topic: str) -> int:
    parts = c.list_topics(topic, timeout=10).topics[topic].partitions
    since = int((time.time() - 86_400) * 1000)
    start = c.offsets_for_times([TopicPartition(topic, p, since) for p in parts], timeout=10)
    total = 0
    for tp in start:
        _low, high = c.get_watermark_offsets(TopicPartition(topic, tp.partition), timeout=10)
        total += high - (tp.offset if tp.offset >= 0 else high)   # -1 = nada mais novo
    return total

Para ver o tamanho de uma mensagem, li uma mensagem por partição, 19 no total, com assign() num grupo descartável e sem commit, e só medi bytes e contei quebras de linha. Nada do conteúdo foi impresso nem guardado. É uma amostra pequena, então o 112 é uma estimativa, e eu falo dele assim.

Contar linhas do outro lado

No ClickHouse, count() na tabela é a ferramenta errada. Nossas tabelas de auditoria são ReplacingMergeTree, então o count() muda quando as partes se fundem, e numa tabela com TTL as linhas antigas também vão embora. O que eu quero é o que foi inserido, e o system.part_log registra cada parte nova com a contagem de linhas no momento do insert:

linhas inseridas nas últimas 24 h
SELECT table, sum(rows) AS rows_inserted
FROM system.part_log
WHERE event_type = 'NewPart'               -- só inserts; merges são MergeParts
  AND database = 'audit'
  AND table LIKE 'play_log%'
  AND event_time >= now() - INTERVAL 24 HOUR
GROUP BY table;

Os números

Na telemetria, em que uma mensagem é uma linha, a comparação é direta: 13.912.374 mensagens nos tópicos de presença contra 13.912.522 linhas na tabela do ClickHouse, nas mesmas 24 horas, vindas de 13.835 telas. Uma diferença de 148 linhas, 0,001%. Minha leitura é que as duas janelas não começam exatamente no mesmo milissegundo (acho que é só isso), e não fui atrás.

A prova de exibição teve 1.782.599 mensagens no tópico principal em 24 horas, e o part_log mostrou 193.924.874 linhas inseridas nas tabelas principais de exibição mais 5.380.579 no ambiente menor, cerca de 199,3 milhões. Multiplique 1,78 milhão de mensagens por ~112 linhas e você chega perto disso, que é o quanto uma amostra de seis mensagens consegue dizer.

Todos os tópicos principais juntos tiveram cerca de 22,5 milhões de mensagens naquele dia, e esse número é um piso, porque a retenção corta parte dos tópicos mais curtos. A retenção é definida por tipo de dado: 3 horas para a telemetria, que só passa, e 3 dias para a prova de exibição. Os 3 dias importam. Uma vez o importador ficou parado umas 39 horas, e as mensagens simplesmente esperaram no tópico até ele voltar.

Por que confiro assim

Um gráfico de lag diz se os consumidores acompanham os offsets. Offsets contra part_log dizem se o dado chegou, e em que quantidade. Usei a mesma ideia do part_log em agosto para conferir uma tabela de resumo contra a origem, e volto sempre a ela porque é barata, lê metadados e dá um número que outra pessoa consegue repetir.

Se for levar uma coisa sóCompare o que entrou no tópico com o que o banco diz que inseriu, na mesma janela. Duas contagens independentes que batem em 0,001% convencem as pessoas mais rápido que qualquer painel.

Arquivado pelo problema que resolve

Luciano Serra
Luciano Serra Founder-engineer · cofundador e CTO, construiu do zero uma rede de 15.000 telas, 2015–2024 · São Paulo, remoto

Desenho sistemas de produção do zero e assumo os que já estão rodando. Nos dois casos eles precisam aguentar quando o negócio cresce. Cofundei uma empresa de mídia out-of-home e construí a tecnologia dela por dez anos; meus sócios captaram o dinheiro e cuidaram do comercial.

Seu negócio está crescendo mais rápido que os sistemas? Vamos conversar →