Pular para o conteúdo

Design de sistema de app de transporte: localização, matching e despacho

Projete passo a passo o backend de um app de transporte: ingestão de localizações, índice geoespacial, matching e despacho sem atribuição dupla e estados da corrida.

· Fundador e desenvolvedor

Publicado em

Numa moldura de cidade, o app do motorista envia localizações por um gateway ao Kafka, que alimenta um índice geo no Redis e um repositório de objetos. O app do passageiro pede uma corrida; o matching consulta índice e ETA e faz a oferta; os estados da corrida vão para o Postgres.

Um passageiro toca em “Solicitar” e espera um carro em segundos. Por trás desse toque, o sistema precisa saber onde estão agora centenas de milhares de motoristas em movimento, escolher um bom, garantir que nenhum outro passageiro fique com o mesmo motorista e depois acompanhar uma corrida que termina com um pagamento. É uma pergunta favorita em entrevistas de design de sistemas porque mistura um stream em tempo real de alto volume com um núcleo transacional pequeno e rígido.

Este post projeta o backend passo a passo: requisitos e números de capacidade, ingestão de localizações, indexação geoespacial com geohash, S2 e H3, matching e despacho, a máquina de estados da corrida e as falhas que você precisa prever, com um diagrama de arquitetura que você pode copiar.

Requisitos

Funcionais:

  • Os motoristas ficam online e transmitem a localização continuamente.
  • Os passageiros pedem uma corrida e veem carros próximos e um tempo estimado de embarque.
  • O sistema faz o matching entre um passageiro e um motorista, que pode aceitar ou recusar.
  • Os dois apps acompanham a corrida ao vivo; a corrida termina com uma tarifa e um pagamento.

Não funcionais:

  • Um matching em poucos segundos.
  • Localizações de motoristas com no máximo alguns segundos de atraso.
  • Nunca atribuir um motorista a dois passageiros.
  • Problemas de uma cidade não podem derrubar outras cidades.

Capacidade: os números que moldam o design

Suponha, para uma operação grande, um milhão de motoristas online no pico, cada um enviando uma localização a cada 4 segundos, e 20.000 pedidos de corrida por minuto:

Grandeza Cálculo Resultado
Atualizações de localização 1.000.000 / 4 s 250.000 por segundo
Banda de ingestão 250.000 × ~100 bytes ~25 MB/s
Histórico bruto no ritmo de pico 25 MB/s × 86.400 s ~2 TB por dia
Pedidos de corrida 20.000 / 60 s ~333 por segundo

Aparecem duas cargas de trabalho muito diferentes. As atualizações de localização têm um volume enorme, mas cada uma é pequena, descartável e substituída 4 segundos depois. Os pedidos de corrida são três ordens de grandeza mais raros, mas cada um precisa ser tratado exatamente uma vez e está ligado a dinheiro. A arquitetura separa os dois.

Ingestão de localizações: guarde só o que é atual

Os apps dos motoristas enviam localizações por uma conexão persistente, como um WebSocket, ou como pequenas requisições HTTPS periódicas. Um gateway de localização autentica o motorista, descarta pontos impossíveis (um salto de 50 km em 4 segundos) e grava cada atualização em um stream como o Kafka, particionado por cidade.

Três consumidores leem o stream:

  1. O índice geoespacial ao vivo guarda só a última posição de cada motorista disponível, com uma expiração curta, para que um motorista cujo celular parou de responder desapareça depois de, digamos, 30 segundos.
  2. O acompanhamento da corrida repassa a posição do motorista atribuído para o app do passageiro.
  3. O armazenamento frio guarda o histórico completo em armazenamento de objetos para análises, modelos de ETA e resolução de disputas.

A decisão-chave é que o índice quente guarda uma linha por motorista, e não uma linha por atualização. Um milhão de motoristas com cerca de cem bytes cada um dá uns 100 MB, pequeno o bastante para ficar em memória por cidade.

Indexação geoespacial

A pergunta do matching é “quais motoristas disponíveis estão perto deste ponto?”. Um índice de banco de dados comum sobre latitude e longitude responde mal a isso, então os sistemas mapeiam as coordenadas para células.

Índice Formato da célula Pontos fortes Atenção a
Geohash Retângulos, codificados como prefixo de string Simples; os prefixos se aninham; o Redis GEO o usa As células variam de formato; vizinhas podem ter prefixos diferentes
S2 Quadriláteros sobre um cubo projetado Hierárquico, usado pelo Google Mais complexo de raciocinar
H3 Hexágonos Toda vizinha está à mesma distância; 16 resoluções Hexágonos não se aninham perfeitamente

O H3, que a Uber abriu como código aberto em 2018, torna a busca por raio uniforme: pegue a célula do passageiro e o anel de células ao redor, depois reúna os motoristas que estão dentro. A resolução importa. Pela tabela de resoluções do H3:

Resolução H3 Área média da célula Comprimento médio da aresta
7 5,16 km² 1,41 km
8 0,74 km² 0,53 km
9 0,11 km² 0,20 km

As resoluções 8 ou 9 servem para cidades densas; áreas rurais precisam de anéis mais largos ou células mais grosseiras.

Se você quer começar simples, o Redis tem comandos geoespaciais embutidos:

GEOADD drivers:sf -122.4194 37.7749 driver:8812
GEOSEARCH drivers:sf FROMLONLAT -122.4100 37.7800 BYRADIUS 2 km ASC COUNT 20 WITHDIST

O GEOSEARCH, disponível desde o Redis 6.2, retorna os 20 motoristas mais próximos em um raio de 2 km, ordenados por distância. Faça sharding primeiro por cidade, a fronteira natural, e divida lugares muito movimentados, como aeroportos ou estádios, em células mais finas.

Matching e despacho

A distância em linha reta é um mau indicador do tempo de embarque: um motorista a 500 m do outro lado de um rio pode estar a 15 minutos. Por isso o matching funciona em duas etapas:

  1. Geração de candidatos. Consulte o índice geo para obter motoristas disponíveis perto do ponto de embarque, filtrados por tipo de veículo e outras restrições.
  2. Ranqueamento. Peça a um serviço de ETA os tempos de embarque pela malha viária para os melhores candidatos e ordene por ETA, somando fatores como a direção do motorista.

Fazer o matching de cada pedido na hora com o motorista mais próximo é guloso. Juntar pedidos por um ou dois segundos e resolvê-los juntos como um problema de atribuição minimiza o tempo total de espera dos passageiros. O preço é um atraso curto em troca de matchings melhores nos horários de pico.

Depois, o despacho envia uma oferta a um motorista por vez, com um timeout de cerca de 15 segundos. Em caso de recusa ou timeout, o próximo candidato recebe a oferta.

Nunca atribua um motorista duas vezes

Dois workers de matching podem escolher o mesmo motorista para dois passageiros no mesmo segundo. Evite isso no estado do motorista, não na lógica de matching:

  • Guarde o estado de cada motorista (available, offered, on_trip) com um número de versão.
  • Uma oferta é uma atualização condicional: defina offered só se o estado ainda for available na versão que você leu.
  • Exatamente um worker vence; o outro passa para o próximo candidato.
  • Uma oferta expirada devolve o motorista para available, de novo de forma condicional.

A mesma ideia de compare-and-set protege o lado do passageiro: um pedido ativo por passageiro.

A máquina de estados da corrida

Uma corrida passa por requested, matching, driver_assigned, driver_arrived, in_progress, completed e paid, com cancelamento possível a partir de vários estados. O serviço de corridas guarda cada corrida em um banco de dados relacional como o Postgres, aplica as transições só quando o estado atual permite e publica cada mudança como um evento para cobrança, notificações, recibos e análises.

Torne cada transição idempotente, com o ID da corrida e a versão do estado como chave. Redes móveis repetem requisições, e um “motorista chegou” entregue duas vezes não pode fazer nada duas vezes. O exemplo de diagrama de estados mostra como desenhar esse ciclo de vida separadamente.

Atualizações ao vivo, ETAs e preços

Enquanto a corrida está ativa, o app do passageiro recebe a posição do motorista por uma conexão push, limitada a cada poucos segundos e suavizada no cliente. O mesmo stream de localizações alimenta o serviço de ETA, que precisa do trânsito ao vivo, e o preço dinâmico, que compara por célula os pedidos com os motoristas disponíveis em uma janela deslizante. O pagamento acontece depois da corrida por meio de um provedor de pagamentos; o diagrama de integração de gateway de pagamento cobre essa parte.

Falhas que você precisa prever

  • Os motoristas perdem a conexão. O app envia só o último ponto quando reconecta, e o índice expira os motoristas que ficam em silêncio.
  • Um worker de matching cai no meio de uma oferta. As ofertas têm prazo de expiração; o motorista volta para available quando a oferta vence.
  • O cluster de uma cidade falha. Como tudo é particionado por cidade, a falha fica local. As corridas em andamento mantêm o estado no armazenamento durável de corridas.
  • GPS falsificado. Verificações de plausibilidade de velocidade e rota no gateway pegam os casos grosseiros.

O diagrama do app de transporte

Numa moldura de cidade, o app do motorista envia localizações por um gateway ao Kafka, que alimenta um índice geo no Redis e um repositório de objetos. O app do passageiro pede uma corrida; o matching consulta índice e ETA e faz a oferta; os estados da corrida vão para o Postgres.

12 componentes · 10 conexões

Abrir no Archboard
Conteúdo do diagrama
  • City partition: everything inside is per city
  • Driver app
  • Rider app
  • Location gateway
  • locations topic
  • Live geo index (latest, 30 s TTL)
  • Location history
  • Matching + dispatch
  • ETA service
  • Trip service
  • Trips (Postgres)
  • GPS every 4 s
  • 250k/s
  • latest position
  • every point
  • POST /rides
  • nearby available drivers
  • rank by pickup ETA
  • driver assigned (CAS)
  • state transitions
  • ride offer, 15 s to accept
O backend de transporte de uma cidade. As localizações passam por um gateway para um stream e um índice geoespacial ao vivo; um pedido vai ao matching, que ordena os motoristas próximos por ETA e envia uma oferta por vez; o serviço de corridas registra cada mudança de estado.

Como ler:

  • A moldura é uma cidade, a fronteira de partição de tudo o que está dentro dela.
  • O caminho das localizações passa por cima: app do motorista, gateway de localização, Kafka e, a partir dele, o índice geo no Redis e o repositório de objetos para o histórico.
  • O caminho dos pedidos passa por baixo: o app do passageiro chama o matching, que lê candidatos do índice geo, pede ao serviço de ETA os tempos de embarque e envia a oferta ao motorista.
  • O serviço de corridas é dono da máquina de estados e a guarda no Postgres.
  • O bloco Markdown mostra os números de capacidade.

Desenhar diagramas de design de sistemas em tempo real

  • Separe o caminho quente do caminho transacional. Aqui, o stream de localizações e o ciclo de vida da corrida têm volumes, garantias e modos de falha diferentes; desenhe-os como raias separadas.
  • Escreva a chave de partição no diagrama. “Particionado por cidade” explica a escala e o isolamento de falhas em três palavras.
  • Mostre o que é atual e o que é histórico. O índice das últimas posições e o armazenamento do histórico respondem a perguntas diferentes.
  • Rotule as arestas com taxas: 250.000 atualizações por segundo em uma seta mostram aos revisores onde está a carga.

Para uma abordagem geral desses diagramas, veja o exemplo de diagrama de design de sistemas. Conexões push são tratadas no diagrama de arquitetura de app de chat, e o stream em si no diagrama de arquitetura do Kafka.

Monte no Archboard

  1. Entre com Google ou GitHub e crie um diagrama.
  2. Adicione uma Moldura do pack Básico com o nome da cidade.
  3. Adicione dois nós Dispositivo do pack Rede para o app do motorista e o app do passageiro.
  4. Adicione um Gateway de API do pack Nuvem para o gateway de localização, um nó Kafka do pack Dados para o stream, um nó Redis para o índice geo e um Repositório de objetos para o histórico.
  5. Adicione três nós Instância de computação do pack Nuvem para os serviços de matching, ETA e corridas, e um nó Postgres do pack Dados para as corridas.
  6. Conecte o caminho das localizações por cima e o dos pedidos por baixo, e rotule as arestas com o que elas carregam e com que frequência.
  7. Adicione um bloco Markdown do pack Básico com os números de capacidade.

O exemplo usa 12 nós de diagrama e fica dentro do plano Free.

Perguntas frequentes

Com que frequência os motoristas devem enviar a localização? A cada poucos segundos é uma faixa comum. Atualizações mais frequentes deixam as posições mais frescas, mas multiplicam o volume de ingestão: reduzir o intervalo pela metade, de 4 para 2 segundos, dobra o tráfego.

Devo usar geohash, S2 ou H3? Qualquer um funciona. O geohash é o mais simples e vem embutido no Redis; o H3 dá distâncias uniformes entre vizinhas, o que deixa mais limpas as buscas por raio e o preço por célula. Escolha um e use de forma consistente.

Como evitar atribuir um motorista a dois passageiros? Faça da oferta uma atualização condicional no estado do motorista, para que ela só dê certo se o motorista ainda estiver disponível na versão que o matcher leu. Só um matcher pode vencer.

Por que não fazer o matching de cada passageiro imediatamente com o motorista mais próximo? O matching guloso pode deixar os passageiros seguintes com esperas longas. Agrupar os pedidos por um ou dois segundos e resolvê-los juntos reduz o tempo total de espera, ao custo de um atraso curto.

Que banco de dados guarda as corridas? Um banco de dados relacional encaixa bem: as corridas são transacionais, precisam de transições de estado rígidas e alimentam a cobrança. Os dados de localização de alto volume ficam em outro lugar, em memória e no armazenamento de objetos.

Comece grátis — um assento, sem cartão de crédito.

Artigos relacionados

← Voltar ao blog

Abra um canvas em um minuto

Entre com Google ou GitHub. Grátis para um assento. Faça upgrade quando a equipe entrar.