antonio leandro

sistemas distribuídos

Real-time Data Infrastructure at Uber

paper · Yupeng Fu, Chinmay Soman · · ~57 min de leitura do original

a tese

adotar kafka, flink e pinot é a parte barata: o que sustenta o tempo real na uber é a camada de indireção construída em cima deles — federação, proxy, sql — porque é ela que absorve dados, casos de uso e usuários

o que fica

  1. Um cluster Kafka rende melhor abaixo de 150 nós, então escalar virou adicionar clusters e esconder isso atrás de um metadata server que serve um cluster lógico ao cliente.
  2. Cliente gordo é dívida organizacional: atualizar a biblioteca do Kafka em todas as aplicações levava meses, e a saída foi um proxy que empurra mensagens por gRPC para um client fino gerado por máquina.
  3. O proxy push também quebra o teto do Kafka de um consumidor por partição, que era o limite real de paralelismo de quem consumia devagar.
  4. A arquitetura Kappa é inviável quando a retenção do Kafka é de poucos dias, e por isso a Uber escreveu o Kappa+, que roda a mesma lógica de streaming direto sobre datasets do Hive.
  5. No surge, o estado do job Flink é grande demais para replicar entre regiões, então roda-se o pipeline inteiro redundante em cada região e confia-se na convergência a partir do mesmo input.
  6. Pré-agregar no Flink para o Pinot compra latência de consulta pagando com flexibilidade: a query fica rápida, mas só as queries previstas continuam possíveis.

o problema

Em outubro de 2020 a Uber gerava trilhões de mensagens e petabytes de dados por dia, vindos de apps de motorista, passageiro e entregador, de logs de milhares de microserviços e de changelogs de bancos OLTP. Tudo isso replicado em várias regiões, o que multiplica o volume agregado. Em cima desse fluxo pendem decisões que precisam sair em segundos: preço dinâmico, detecção de fraude, incentivo ao cliente, monitoramento de modelo de machine learning.

O incômodo não é o volume sozinho. É que os requisitos brigam entre si. O dashboard financeiro quer consistência entre regiões e zero perda. O surge quer frescor e disponibilidade, e pelo CAP não pode ter as duas coisas junto com consistência. O Restaurant Manager do UberEats quer p99 de consulta abaixo de 1 segundo em cada carregamento de página. E os usuários vão de engenheiro que orquestra pipeline multi-estágio até pessoal de operações que só sabe SQL. A saída fácil seria uma solução sob medida por caso de uso. A cara é uma plataforma só, com abstração padrão, que sirva a todos.

a ideia

A decisão central foi não escrever motor. Kafka para stream, Flink para compute, Pinot para OLAP, HDFS para arquivo, Presto para query interativa — tudo open source, escolhido por maturidade e por tamanho de comunidade, não só por benchmark. A engenharia da Uber foi gasta em outro lugar: na camada de indireção entre o usuário e cada um desses motores.

Essa camada é o produto. Ela define contratos mínimos por nível — o stream garante partição e at least once, o OLAP garante at least once na ingestão e exactly once por chave primária num punhado de casos críticos, o SQL fica próximo de ANSI com extensões de janela. Acima desses contratos, os motores podem ser trocados, tunados e escondidos. É o que permite que o mesmo protocolo de consumidor Kafka sirva tanto log descartável quanto dado financeiro sem perda.

como funciona

No Kafka, quatro adições. Federação: um metadata server agrega tópicos de vários clusters físicos e roteia o cliente, o que permite crescer somando clusters e migrar tópico com consumidor vivo sem reiniciar aplicação. Dead letter queue: a mensagem que falha depois de algumas tentativas vai para um tópico à parte, em vez de ser descartada ou travar a fila, e pode ser purgada ou reprocessada depois. Consumer proxy: o proxy consome e despacha por gRPC para um endpoint registrado pelo usuário, transformando poll em push, concentrando retry e DLQ, e liberando paralelismo acima do número de partições. Replicação: o uReplicator, com rebalanceamento que minimiza partições afetadas, mais o Chaperone, que conta mensagens únicas por janela em cada etapa e alerta quando os números não batem.

No Flink, o FlinkSQL compila uma query Calcite em plano lógico, passa pelo otimizador e gera um job Flink completo, com ciclo de vida gerenciado. Em volta disso, uma arquitetura em três camadas — plataforma, gestão de jobs, infraestrutura — e automação: correlação empírica entre tipo de job e recurso (job stateless é CPU bound, join de dois streams é memory bound), auto-scaling por carga e GC, e um motor de regras que compara métricas com o estado desejado e reinicia job travado.

No Pinot, upsert sem coordenador: o stream é particionado por chave primária, cada partição vai para um nó, e uma estratégia de roteamento manda as subqueries dos segmentos daquela partição para o mesmo nó. Shared-nothing, sem ponto único de falha. O connector do Presto empurra predicado, projeção, agregação e limit para o Pinot. E a recuperação de segmento virou peer-to-peer entre réplicas, no lugar do backup síncrono via controller único para um segment store externo, que era gargalo e derrubava a ingestão quando falhava.

Entre regiões, dois modos. Active-active: o surge roda o pipeline inteiro em cada região sobre o mesmo input dos clusters agregados, e um serviço coordenador elege qual região escreve o resultado. Active-passive, para pagamento e auditoria: o uReplicator faz checkpoint do mapeamento de offsets entre origem e destino, e um job sincroniza offsets entre regiões para o consumidor retomar do ponto certo no failover.

o que isso custou

Esconder o Flink atrás de SQL transfere o custo, não o elimina: o usuário não sabe que existe um job, então a equipe de plataforma herda tuning, capacidade e recuperação de tudo. O active-active do surge queima compute redundante em toda região, porque o estado do job é grande demais para replicar. Pré-agregar no Flink acelera o Pinot e amarra o formato da query.

Os autores listam o que não fecha. O Pinot não faz join: quem faz é o Presto, inteiramente em memória no worker, o que os autores dizem não servir para caso crítico. Não há JSON nativo, então um job Flink achata o tópico antes da ingestão. O Flink ainda não reinicia sem downtime, e isso está com a comunidade. A retenção curta do Kafka fecha a porta do Kappa, e o Kappa+ tem casos de borda que o paper reconhece sem avaliar. Vale lembrar também que as comparações citadas — Storm levando horas contra 20 minutos do Flink para drenar backlog, Spark consumindo de 5 a 10 vezes mais memória, Elasticsearch com 4 vezes mais memória e 8 vezes mais disco que o Pinot — são medições da casa, de 2016 e 2018, sobre as versões daquele momento.

onde isso aparece hoje

O paper é a descrição mais completa de um stack de tempo real inteiro montado sobre software aberto, e virou referência de contraste com a arquitetura do Facebook descrita por Chen e colegas, feita de componentes internos. O Kafka no papel de barramento único entre streaming e batch é exatamente o uso que o desenho original previa, agora com federação por cima. O trade-off entre consistência e frescor no surge é aplicação direta de Brewer’s Conjecture, que os autores citam para justificar a escolha. A unificação de streaming e batch que eles listam como trabalho futuro é o problema que o Dataflow atacou pelo lado do modelo de programação. E o tiered storage e a saída para a nuvem, também listados como futuro, são o eixo do Kora, o Kafka nativo de nuvem que a Confluent descreveu depois.

lido na íntegra por pipeline de llm, revisado por antonio leandro antes de publicar ·