Compartilhar este artigo

A versão curta

Quando uma partição do Kafka fica em silêncio, ela terminou, ou está apenas lenta? Vistas de fora, as duas situações parecem idênticas — e a resposta determina se você pode agir com segurança sobre o que já tem. Netflix, Uber e Walmart publicaram como lidam com isso, e cada uma abre mão de algo: um segundo pipeline, uma via de paralelismo, a correção durante o replay. Engenharia sólida, em todos os casos. Nenhuma delas pergunta ao Kafka a única coisa que o Kafka já sabe.

Encontrei esse problema em um stream de inventário de uma empresa da Fortune 50: treze tópicos Kafka, três partições cada, centenas de milhões de eventos por dia, cada evento de um dado produto exigindo ordem estrita de event time — lendo sete dias de histórico do zero e, em seguida, entrando diretamente no tráfego de pico ao vivo. O primeiro design colocava uma janela de trinta minutos na frente de tudo, ainda precisava de um segundo job para varrer o que a janela deixava escapar, e ainda assim deixava 5,5% dos registros saírem na ordem errada.

A versão que entrou em produção não espera por um relógio. Ela lê os offsets do Kafka.

Eis como — e por que a ideia é mais antiga que o Kafka.

A troca que todos fazem / §01

Este não é o problema de uma equipe só. Quatro organizações de engenharia fortes, quatro trocas sensatas:

  • O Walmart fixa cada par item–loja em uma única partição, de modo que apenas um consumidor o atualize. Ordenação por fixação de chaves — abre mão de paralelismo.
  • O Uber executa um pipeline separado de backfill para recomputar após eventos atrasados e fora de ordem, porque o caminho de streaming "pode causar imprecisões ao descartar quaisquer eventos que cheguem após o watermarking". Abre mão de um segundo pipeline.
  • A Netflix, ao fazer backfill de pipelines Flink, constatou que o histórico reproduzido gerava uma ordem diferente e estado descontrolado. Abre mão de correção e de estado.
  • A orientação da própria Confluent para uma partição Kafka ociosa: esperar por ela e travar, ou ignorá-la e aceitar "muitas mensagens atrasadas quando a partição voltar a ficar ativa". Abre mão com travamentos ou dados atrasados.

Todas são razoáveis. O fio condutor é que cada uma infere completude a partir do tempo — uma janela, um timeout, uma espera — quando os offsets das partições dizem exatamente onde os dados terminam. É essa a lacuna que isto fecha.

Por que o stream fica embaralhado / §02

Um tópico Kafka é dividido em partições, e cada partição é seu próprio trilho. Dentro de uma partição, a ordem é garantida: registro um, depois registro onze, sempre. Entre partições, nada coordena nada — os consumidores que as leem nunca estão no mesmo ponto ao mesmo tempo. O Kafka faz exatamente o que promete, os consumidores fazem exatamente o que prometem, e ainda assim a saída combinada fica embaralhada para qualquer coisa que dependa de sequência.

Você não pode agir sobre "vendido" antes de "em estoque" para a mesma unidade. Então, antes de um pipeline liberar qualquer coisa, ele precisa responder a uma pergunta: tenho tudo até este momento? Quase toda resposta em uso hoje é um temporizador — a janela, o timeout de ociosidade, o job de reconciliação. Cada uma é um palpite sobre o silêncio.

Uma solução antiga: reportar, não esperar / §03

Há um precedente, e ele não é um relógio melhor. As primeiras ferrovias operavam pelo sistema de intervalos de tempo: um trem seguinte partia uma vez decorrido um intervalo fixo, de modo que a linha "devia" estar livre.

Em junho de 1889, um trem parou em uma rampa íngreme nos arredores de Armagh, na Irlanda. Os vagões traseiros foram desacoplados, desceram a colina de volta e atingiram o trem que vinha atrás. Cerca de oitenta pessoas morreram.

O trem seguinte havia partido de Armagh assim que os vinte minutos exigidos se passaram. Vinte minutos haviam se passado, logo a linha devia estar livre. Ninguém em Armagh tinha como saber que não estava.

A Grã-Bretanha não respondeu com um relógio melhor. O Parlamento deu ao Board of Trade o poder de exigir o bloqueio absoluto nas ferrovias de passageiros: nenhum trem entra em uma seção da via até que a cabine de sinalização à frente confirme que a seção está livre. A liberação deixou de ser uma duração que alguém esperava passar e tornou-se um fato que alguém reportava. Um trem parado e um trem que já partiu deixaram de parecer a mesma coisa, porque ninguém mais interpretava nada a partir do silêncio.

Esse é todo o movimento. Ler os offsets do Kafka é a cabine de sinalização.

Não infira completude a partir do silêncio. Pergunte ao sistema que sabe.

O relógio — sistema de intervalos de tempo
20 minutos se passaram → SIGA

A liberação é uma duração que você espera passar. Um trem parado e um que já partiu parecem exatamente a mesma coisa.

O sinal — bloqueio absoluto
A cabine à frente confirma “seção livre” → SIGA

A liberação é um fato que alguém reporta. Ninguém mais interpreta nada a partir do silêncio.

O mesmo movimento, no streaming
cronômetro = a janela / timeout de ociosidade
seção da via = uma partição do Kafka
“seção livre” = partição alcançou seu offset final
sinal liberado = o watermark avança

A construção — três peças / §04

O princípio é curto: o tempo só avança quando a evidência prova que pode. Torná-lo real exigiu três peças, e a parte difícil não estava onde as pessoas esperam.

Um: a fila de timers é a ordenação. Cada registro é estacionado em estado keyed sob seu próprio event time, com um timer de event time para aquele milissegundo exato. Nada é emitido ainda. Quando o watermark alcança aquele timestamp, o timer dispara e tudo que estava estacionado ali sai junto. O Flink dispara os timers de uma chave em ordem crescente de timestamp, então nunca ordenamos nada. É um método e um pedaço de estado — a dificuldade nunca esteve aqui.

01 · na chegada

Armazene-o e, em seguida, registre um gatilho para seu próprio event time.

state.put(t, list)
registerEventTimeTimer(t)
nada é emitido ainda
02 · no gatilho

O relógio o alcança, leia aquela chave, libere tudo que está sob ela.

onTimer(t)
collect(state.get(t))
B e C saem juntos
MapState⟨Long, List⟩ · keyed por SKU
event timeregistrostimer
10:00:18—disparado, removido
10:00:26B, Cvence agora
10:00:33Daguardando
10:00:41E, F, Gaguardando

◆ A fila de timers do Flink é ordenada por timestamp: os timers disparam em ordem crescente. Essa é a ordenação.

Dois: o stream sem event time torna-se o relógio. Um dos treze tópicos não tinha event time algum — ele apenas publicava produtos válidos para que todos os outros se enriquecessem com eles. Portanto, não tinha nada a ordenar, e podia fazer o que os outros não podiam: emitir watermarks a partir do tempo de processamento, sempre o timestamp mais recente possível. O porém: o relógio de um pipeline é o mínimo entre suas entradas, então um stream rodando no "agora" não muda nada enquanto os streams de histórico ainda estão sendo contabilizados. Construí essa peça primeiro e ela não teve efeito algum — não estava errada, estava na ordem errada. O stream de controle só importa quando os outros saem do mínimo, e um stream sai do mínimo quando é marcado como ocioso.

Três: o Kafka decide quando um stream terminou — não um timeout. A ociosidade nativa do Flink é um temporizador: sem dados por um tempo, marque como ocioso. Mas um timeout não consegue distinguir "inicializando" de "terminado", e a inicialização é o pior caso — as conexões se formam em velocidades diferentes, um tópico fica em silêncio, o timeout dispara, os outros avançam o relógio para além de event times que ele ainda não leu, e seu primeiro registro chega já atrasado. Isso não é um atraso, é uma violação. Então o timeout tinha que sair. Na inicialização, antes de ler um registro, o job pergunta ao Kafka o último offset de cada partição e o guarda. Um tópico só está ocioso quando cada partição alcançou esse número — não a maioria, todas. Nenhuma duração para ajustar, porque o portão é um fato vindo do Kafka.

// once, at job startup
Map<TopicPartition, Long> endOffsets = consumer.endOffsets(partitions);

// as records arrive
if (offset + 1 >= startupEndOffsets.get(partition)) markCaughtUp(partition);

O que isso rendeu / §05

A janela retinha cada registro até fechar — trinta minutos só para ler o histórico, de novo a cada reinício. Agora um registro é liberado assim que o watermark o ultrapassa: em média menos de 100 milissegundos, incluindo os registros retidos para que não saíssem fora de ordem. Um registro que chega no minuto um sai no minuto um. A ordenação seguiu o mesmo caminho: duas camadas de ociosidade por timeout deixavam 5,5% dos registros fora de ordem; uma camada, 1,5%; ociosidade decidida pelos offsets de inicialização do Kafka, zero. A janela se foi e o job de varredura também — um único job lê sete dias de histórico e depois roda ao vivo.

AntesDepois
Espera até um registro ser liberado em ordemAté 30 minutos (a janela)< 100 ms em média*
Registros fora da ordem de event time5,5% (ociosidade por timeout)0
Jobs a executarStream + varreduraUm

*Latência medida de ponta a ponta no cliente, sobre centenas de milhões de eventos por dia. Ordenação medida na implementação de referência que reconstruí para este artigo.

Duas camadas de ociosidade por timeout
5,5%
Uma camada de ociosidade por timeout
1,5%
Ociosidade pelos offsets de inicialização do Kafka
0

Registros liberados fora da ordem de event time. Implementação de referência reconstruída para este artigo.

O que isso não promete / §06

Determinístico não é clarividente. Um evento só existe para o pipeline quando chega ao Kafka. Se um produtor retém um registro e ele chega depois que o relógio já passou do seu event time, ele sai imediatamente, fora da vez, e o contador de violações o registra. Nenhum processador de streams consegue ordenar um evento que ainda não pode ver. Dentro dessa fronteira a garantia é exata, e o lag do watermark define a largura da fronteira.

relógio de event time · watermark = agora − lagclockRegistro Achega antes de o relógio alcançar seu tempoliberado em ordemRegistro Bchega depois que o relógio já passou do seu tempofora da vez · violação +1

Nenhum processador consegue ordenar um evento que ainda não pode ver. Dentro da fronteira a garantia é exata — o lag do watermark define a largura da fronteira.

A mesma pergunta, em toda parte / §07

Depois de vê-la uma vez, você a vê em todo sistema que decide o que é verdade a partir de streams que não chegam juntos.

Um medidor fluvial fica em silêncio durante uma enchente. O rio está estável, ou a água alcançou o sensor? Em 2018, o USGS relatou medidores de vazão derrubados pelo furacão Florence — justamente os que mais importavam. "Sem notícias" é a leitura errada para esse silêncio.

Um conjunto de câmeras. Capture um atleta com várias câmeras e cada uma é seu próprio stream, como uma partição; você não pode fundir uma pose 3D para um instante até que cada câmera o tenha reportado. Uma câmera silenciosa pode ter perdido um quadro ou estar observando alguém imóvel. Na i3d.ai é a mesma pergunta, dezenas de vezes por segundo.

Um agente. Um agente de IA chama uma ferramenta e não ouve nada de volta. Agentes são muito bons em tratar silêncio como sucesso — nenhum erro retornou, então siga para o próximo passo em velocidade de máquina. Um agente agindo sobre a conta de um cliente como ela estava há uma hora é um trem despachado por um temporizador. A solução é a que as ferrovias encontraram: nenhum passo avança com base no silêncio; todo passo avança com base em uma confirmação que ele pode apontar.

Uma cidade em uma tempestade
Um medidor fluvial para de transmitir
silencioso: quieto, ou desaparecido?

O rio está estável — ou a água acabou de alcançar o sensor?

Um corpo em movimento
Uma câmera não envia nada
silencioso: quieto, ou desaparecido?

Um quadro perdido — ou um atleta perfeitamente imóvel?

Um agente em ação
Uma chamada de ferramenta não retorna nada
silencioso: quieto, ou desaparecido?

Ela teve sucesso, falhou — ou ainda está rodando?

Nenhum passo avança com base no silêncio. Todo passo avança com base em uma confirmação que ele pode apontar.

O ganho de confiabilidade mais barato em streaming não é um engine mais rápido ou uma janela maior. É parar de inferir "terminado" a partir do silêncio e perguntar ao sistema que já sabe. A janela espera pelo pior caso para cada registro; a verificação de offset espera exatamente o quanto a evidência exige, e nem um milissegundo a mais.

A implementação de referência enxuta — gerador, handler, rastreador de recuperação — é a próxima coisa que vou publicar.

Seu pipeline está esperando por um relógio, ou lendo o manifesto?

Jeffrey Patrick Carter · NeoSavant

Base histórica

As trocas do Walmart, Uber, Netflix e Confluent são citadas das fontes de engenharia abaixo. O precedente do bloqueio absoluto (Regulation of Railways Act 1889) e o sistema de intervalos de tempo que ele substituiu provêm de registros públicos. Os mecanismos do Flink e do Kafka citam os documentos de design e o código-fonte dos próprios projetos. Os números de ordenação foram medidos em uma reconstrução do job de produção; o número de latência é de ponta a ponta no cliente, no sistema de produção original.

Fontes

Uber Engineering — Designing a Production-Ready Kappa Architecture for Timely Data Stream Processing

Netflix — Backfill Streaming Data Pipelines in Kappa Architecture (Data + AI Summit 2022)

Confluent — Why Is My Apache Flink Job Not Producing Results?

Apache Flink — FLIP-180: Adjust StreamStatus and Idleness definition · FLINK-35886: Idleness incorrectly detected · Generating Watermarks (DataStream) · InternalTimerServiceImpl (ordem de disparo dos timers)

Apache Kafka — KIP-32: Add timestamps to Kafka message (CreateTime / LogAppendTime)

Wikipedia — Regulation of Railways Act 1889 (bloqueio absoluto) · Desastre ferroviário de Armagh

USGS — Working to keep vital flood information flowing (furacão Florence, 2018)

Library of Congress — Jack Delano — Indiana Harbor Belt RR switchman with a fusee, 1943 (item 2017878076)

Achou útil? Compartilhe!

Jeffrey Patrick Carter

Principal Streaming Engineer, NeoSavant

Jeffrey Patrick Carter is a Principal Streaming Engineer at NeoSavant, with 10+ production Flink and Kafka implementations — including Allstate and i3d.ai.

LinkedIn →