R
Interview Deckreact · next.js · senior/staff
0·0·122
System Design

System Design — Streaming (Kafka + Flink)

Broker (Kafka) para durabilidade, Stream Processor (Flink) para enrich/dedupe/sessionização. O coração.

Camada de streaming é onde o evento vira útil. Componentes: (1) Message Broker (Kafka/Kinesis/Pub-Sub) — fila durável particionada, absorve picos. (2) Stream Processor (Flink/Kafka Streams) — consome do broker, enriquece (geo, device, user), agrupa em sessão por event time, deduplica por event_id, filtra bots. (3) Schema Registry — guarda e versiona o contrato de cada evento.

Streaming — broker + processor
publishconsumecold pathhot pathCollectorincoming/collectKafkapartition by session_iddurable bufferFlinkenrich+dedupe+sessionevent-time + watermarkLakeraw, imutávelS3 · ParquetOLAPagregadoClickHouse
Problema
Sem broker, queda do processor = perda de evento. Sem dedupe, evento reenviado conta duplicado nas métricas. Sem event time, eventos fora de ordem (latência de rede) caem na sessão errada. Sem schema versionado, mudança de formato quebra consumers em produção.
Solução
Kafka particionado por session_id — eventos da mesma sessão na mesma partição, ordem preservada por sessão. Flink processa por event_time com watermarks (tolera atraso). Dedupe por event_id usando state (RocksDB local do Flink). Schema Registry valida cada evento antes de publicar — compatibility check garante backward.
Tip
Partição por session_id (não por user_id) — sessões são naturalmente isoladas, e session_id distribui melhor. event_id resolve o problema "exatamente uma vez" das filas: o broker entrega ao menos uma vez, o processor descarta duplicados.
// Flink job — enrichment + dedupe + sessionização
import { StreamExecutionEnvironment } from '@apache/flink'

const env = StreamExecutionEnvironment.getExecutionEnvironment()

env.fromSource(kafkaSource('events.raw'))
  // Watermark: tolera atraso de 30s
  .assignTimestampsAndWatermarks(
    WatermarkStrategy
      .forBoundedOutOfOrderness(Duration.ofSeconds(30))
      .withTimestampAssigner((e: Event) => e.client_timestamp)
  )
  // Enriquecimento (geo do IP, device parsing)
  .map(enrichEvent)
  // Dedupe por event_id (state TTL 1h)
  .keyBy(e => e.event_id)
  .process(new DedupeFunction(Duration.ofHours(1)))
  // Sessionização: sessão fecha após 30min de inatividade
  .keyBy(e => e.session_id)
  .window(EventTimeSessionWindows.withGap(Time.minutes(30)))
  .aggregate(new SessionAggregator())
  // Saída em dois caminhos
  .addSink(s3LakeSink)       // cold path: raw → Lake
  .addSink(clickHouseSink)   // hot path: agregado → OLAP

// Schema Registry valida antes de publicar
const schema = registry.getSchema('events.raw', 'latest')
if (!schema.validate(event)) {
  metrics.increment('schema_violation')
  return  // descarta evento inválido
}
Q.Por que Kafka particionado por session_id e não por user_id?
A.Session_id distribui melhor (mais valores únicos), evita hot partition (um usuário muito ativo concentrando tráfego), e preserva ordem por sessão — que é o que importa para sessionização. user_id seria útil se você precisasse processar tudo do usuário junto, mas sessões são naturalmente isoladas.
Q.O que é watermark no Flink?
A.Sinal que diz "todos os eventos com timestamp < X já chegaram (com 95% de certeza)". Permite fechar janelas de sessionização tolerando eventos atrasados. Watermark agressivo = janelas fecham cedo, perde eventos atrasados. Watermark conservador = latência maior mas captura tudo.
Q.Como funciona dedupe por event_id no Flink?
A.State backend (RocksDB) guarda event_ids já vistos com TTL (ex: 1h). Cada evento entrante consulta o state. Se já existe, descarta. TTL evita crescimento infinito. RocksDB local = rápido (sem rede). Para dedupe global cross-partition, usar partição por event_id_prefix.
107/122← →