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_logdo 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:
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:
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.
Isto foi útil?
Obrigado. Isso me ajuda a decidir o que escrever depois.