Ir para o conteúdo principal
Engenharia

O que acontece nos milissegundos após você tocar em pagar

Um passo a passo de um Databricks App que combina otimização de rotas do Model Serving, Lakebase e autoscaling para analisar transações em milissegundos, com código e benchmarks em todas as camadas.

por Harsha Pasala e Subhadip Chanda

  • Um exemplo de Databricks App (FastAPI + React) que avalia transações de cartão de crédito para detecção de fraude em tempo real, usando a otimização de rotas do Model Serving para inferência de baixa latência e o Lakebase Postgres para consultas online de features e perfis.
  • Apenas uma inferência rápida não é suficiente. O app combina o Model Serving com otimização de rotas ao Lakebase, além de pooling de conexões, rotação de tokens OAuth e padrões de autoscaling que mantêm a latência estável sob carga.
  • Em 5.000 requisições, o endpoint com otimização de rotas respondeu em 27 ms no p50 e 37 ms no p95 de ponta a ponta, com mediana de 8,9 ms nas consultas de features no Lakebase e 100% de sucesso, bem dentro do limite de latência aceitável para o checkout.

Você está no caixa. Você aproxima o seu cartão. Um pequeno ícone de carregamento aparece por talvez meio segundo, talvez menos, e então aparece "Aprovado". Ou não.

Durante esse tempo, algo teve que decidir se essa cobrança se parece com você ou com alguém que roubou o número do seu cartão em um vazamento de dados seis meses atrás. Esse sistema precisava saber coisas sobre você: seus padrões de consumo, seu limite diário, se você permite compras em outros países. E precisava fazer tudo isso rápido o suficiente para que você nem percebesse.

Este post é sobre como é esse "algo" quando você o constrói no Databricks. Vamos explorar o retail-app, um aplicativo de exemplo (back-end em FastAPI, front-end em React, implantado como um Databricks App) que une dois recursos da plataforma:

  • Model Serving com otimização de rota, um caminho de rede mais rápido para o seu modelo implantado.
  • Lakebase, um Postgres gerenciado para os dados de perfil e de features que o modelo precisa no momento da previsão, com escalonamento automático (autoscaling) para que o banco de dados acompanhe a demanda em vez de se tornar o novo gargalo.

O repositório completo está no GitHub. Você pode fazer um fork dele, implantá-lo em seu workspace e clicar em "pagar" você mesmo.

O fluxo: o que realmente acontece em uma transação

Antes de olharmos o código, aqui está a história simples de um único pagamento. Duas verificações ocorrem em sequência: um modelo avalia a cobrança e, em seguida, o aplicativo verifica as regras do seu perfil. Qualquer uma delas pode recusar a transação.

image2.png

O modelo é executado primeiro porque queremos seus números de latência, independentemente do resultado. Em seguida, a consulta de perfil (limite diário de gastos, permissão de transação internacional, país de residência) alimenta algumas instruções condicionais (if-statements). A resposta inclui o tempo de cada etapa para que você possa ver exatamente para onde foram os milissegundos.

Atualizar seu perfil (alterar seu limite diário, ativar/desativar transações internacionais) é uma ação separada. Você salva as alterações no banco de dados e o próximo pagamento já as utiliza. Sem necessidade de nova implantação ou invalidação de cache.

Otimização de rota: por que o caminho de rede importa

Quando um modelo é implantado atrás do Databricks Model Serving, há um salto de rede (network hop) entre sua aplicação e o contêiner de inferência. Para cargas de trabalho em lote (batch), alguns milissegundos extras por solicitação são irrelevantes. Para uma experiência de checkout, isso é tudo.

A otimização de rota encurta esse caminho de rede. Quando você ativa a otimização de rota em um endpoint, o Databricks Model Serving melhora o caminho de rede para solicitações de inferência, resultando em uma comunicação mais rápida e direta entre seu cliente e o modelo. Esse roteamento otimizado permite mais consultas por segundo (QPS) em comparação com endpoints não otimizados e oferece latências mais baixas e estáveis para suas aplicações.

Você a ativa ao criar o endpoint e faz as consultas por meio do fluxo do data-plane usando OAuth, e não tokens de acesso pessoal. Você obtém menor latência e maior taxa de transferência (throughput) para a mesma computação, que é exatamente o que um caso de uso interativo de análise de fraude precisa.

No aplicativo de exemplo, o endpoint é chamado fraud-detection-lakebase. Aqui está a constante e a função que o chama:

Algumas coisas a serem observadas:

  • **serving_endpoints_data_plane.query**: Este é o caminho de consulta do data-plane, que é o que a otimização de rota utiliza. O Databricks SDK gerencia a troca de tokens OAuth nos bastidores.
  • **asyncio.to_thread**: O método de consulta do SDK é síncrono. Envolvê-lo em to_thread mantém o loop de eventos do FastAPI livre enquanto o modelo é executado.
  • **dataframe_records**: O payload é uma lista de dicionários (um por linha). Para a análise de fraude, enviamos uma transação por vez.

O próprio modelo retorna fraud_probability, fraud_flag e (crucialmente) seu próprio tempo interno: lookup_ms (quanto tempo levou a busca de features dentro do contêiner do modelo), inference_ms (previsão do CatBoost) e total_ms. O back-end mapeia esses dados para que o front-end possa exibir um gráfico de cascata de latência:

Portanto, quando você vê o detalhamento da latência na UI ("Inferência do modelo: 45ms" com "Busca de features: 8ms" aninhado logo abaixo), esses são os números medidos em cada camada e combinados em uma única resposta.

Para saber mais sobre como configurar isso: Otimização de rota · Consultando endpoints com otimização de rota.

Lakebase: Postgres para os dados que o modelo precisa

O modelo de fraude não analisa apenas a transação isoladamente. Ele busca as features históricas do cliente (valor médio da transação, proporção de transações internacionais, taxa de chargeback, frequência nas últimas 24 horas) usando os seis primeiros dígitos do cartão de crédito (o BIN) como chave de busca. Essas features residem em uma tabela Postgres do Lakebase chamada customer_features.

Esta é a mesma tabela de onde o back-end lê os dados de perfil (nome, limite diário, permissão internacional). Uma tabela, dois leitores: o contêiner do modelo lê as features para inferência, e o aplicativo FastAPI lê os campos de perfil para as regras de negócio.

No lado da leitura, o back-end solicita uma conexão do pool, executa um SELECT parametrizado por user_id e retorna a conexão em um bloco finally. Um psycopg2 simples, mas a disciplina de solicitar/retornar importa quando isso é executado em cada transação. A consulta usa marcadores (placeholders) parametrizados para a entrada do usuário, de modo que a chave de busca nunca é concatenada no SQL.

No lado da gravação, quando um usuário altera seu limite diário ou ativa/desativa transações internacionais na UI, o back-end constrói um UPDATE dinâmico a partir de uma lista de permissões (allowlist) de colunas editáveis. Apenas os campos dessa lista são gravados; o cliente não pode injetar nomes de colunas arbitrários. A gravação é executada com autocommit = True, de modo que a alteração fica visível imediatamente: a transação seguinte já vê o limite atualizado sem precisar esperar por uma gravação em lote (batch flush) ou invalidação de cache.

Pool de conexões e rotação de tokens OAuth

Cada transação neste aplicativo acessa o Lakebase pelo menos duas vezes: uma dentro do contêiner do modelo para a busca de features e outra no back-end para a verificação do perfil. Abrir uma nova conexão a cada vez significa um handshake TCP mais uma negociação TLS em cada solicitação, o que adicionaria facilmente de 20 a 50 ms de sobrecarga (overhead) por chamada. Um pool de conexões mantém algumas conexões abertas e prontas, de modo que a maioria das solicitações apenas pega uma e continua.

Aqui está como isso funciona na prática. Cada operação do banco de dados segue o mesmo padrão de solicitar/consultar/retornar:

Pegue uma conexão emprestada, execute sua consulta e retorne a conexão em um bloco finally para que ela sempre retorne, mesmo se algo der erro. Cada leitura e gravação no aplicativo segue esse mesmo padrão.

O pool em si é um psycopg2.pool.ThreadedConnectionPool com 3 a 10 conexões. Mas a criação dele é onde as coisas ficam interessantes, porque o Lakebase se autentica via OAuth. O aplicativo troca as credenciais da entidade de serviço por um token de acesso e, em seguida, usa o client ID como o nome de usuário do Postgres e o token como a senha. Sem senhas de banco de dados de longa duração.

Isso significa que, quando o token expirar, você não pode simplesmente continuar usando a senha antiga nas conexões do pool, pois elas falhariam na próxima consulta. Portanto, o _ensure_pool verifica se o token foi alterado e, se tiver sido, cria um novo pool:

Na maioria das vezes, o caminho rápido é acionado: o pool existe, o token não mudou e retornamos imediatamente. Quando o token é rotacionado, o bloqueio de verificação dupla impede que duas threads recriem o pool ao mesmo tempo, e o temporizador de 30 segundos para fechar o pool antigo dá tempo para que as consultas em andamento terminem antes que suas conexões desapareçam.

O lado do modelo: busca de features dentro do container

O próprio modelo de fraude (implantado como um pyfunc do MLflow) faz sua própria busca no Lakebase no momento da previsão. Ele extrai o BIN do cartão, consulta o customer_features, monta um vetor de features e executa a inferência do CatBoost. Cada etapa é cronometrada:

O container do modelo mantém sua própria ThreadedConnectionPool para o Lakebase (a classe LakebaseConnectionPool em fraud_model.py), com atualização de token em segundo plano para que o pool permaneça válido em instâncias de serviço de longa execução. Este é o mesmo padrão do back-end (pool + rotação de OAuth), mas executado dentro do container do modelo, e não no processo FastAPI.

Esses valores de lookup_ms e inference_ms retornam por meio da resposta de serviço, passam pelo back-end e chegam ao front-end. É assim que você obtém visibilidade de ponta a ponta: o modelo relata seu tempo interno, o back-end adiciona sua própria medição de tempo de execução real e o usuário vê tudo isso.

Regras de negócio: a verificação de perfil

Depois que o modelo pontua a transação, o back-end lê o perfil do cliente no Lakebase e executa duas regras simples. Primeiro, ele compara o valor da transação com o limite de gastos diários do usuário. Se a cobrança exceder o limite, a transação será recusada com uma mensagem informando ao usuário que ele pode aumentá-lo nas configurações de perfil. Segundo, se o usuário tiver desativado transações internacionais, o back-end verificará se o país da transação corresponde ao país de residência do usuário. Uma divergência resulta em recusa.

Qualquer uma das regras pode sobrepor a aprovação de um modelo. Uma transação que o modelo considera adequada ainda pode ser recusada porque o usuário definiu um limite diário de $500. Isso é intencional. O modelo lida com o risco estatístico; o perfil lida com as preferências do usuário. Ambos leem da mesma tabela do Lakebase, mas servem a propósitos diferentes.

O tempo gasto na busca de perfil e nas verificações de regras é rastreado como business_logic_ms e retornado junto com o tempo do modelo, para que você possa ver exatamente quanta sobrecarga a lógica de negócios adiciona a cada transação.

Autoscaling do Lakebase com scale-to-zero: lidando com a demanda

Em produção, sua instância do Postgres precisa lidar tanto com as buscas de features do container do modelo quanto com as leituras de perfil do back-end, potencialmente muitas de cada por segundo durante os horários de pico. O Lakebase faz autoscaling, ajustando a computação dentro de um intervalo mínimo/máximo configurado para que você não pague pela capacidade de pico às 3h, mas também não perca consultas ao meio-dia.

Nos resultados de benchmark abaixo, os tempos de busca consistentes de um único dígito de p50 a p75 refletem o que uma instância ativa do Lakebase oferece sob carga constante. O salto no p95 (13,9 ms) é típico de rotatividade do pool de conexões ou de breves eventos de scale-up, ainda bem dentro do limite de latência para um fluxo de checkout, e exatamente o tipo de pico que o autoscaling absorve antes de se tornar visível para o usuário. Com o scale-to-zero ativado, você também deixa de pagar quando não houver transações ocorrendo.

Juntando tudo: detalhamento da latência de ponta a ponta

Aqui está o panorama completo da latência para uma única transação:

MediçãoO que ela capturaOnde é medida
model_call_msTempo total de execução (wall-clock) para toda a chamada de serviçoBack-end (router.py)
model_lookup_msBusca de features dentro do container do modeloModelo (fraud_model.py)
model_interfere_msTempo de previsão do CatBoostModelo (fraud_model.py)
model_total_msTempo total dentro do container do modeloModelo (fraud_model.py)
business_logic_msLeitura de perfil + avaliação de regrasBack-end (router.py)
backend_total_msTempo total de execução (wall-clock) desde o início da solicitação até a chamada do modelo de fraudeBack-end (router.py)

A diferença entre round_trip_ms e model_total_ms é a sobrecarga de rede, e é exatamente aí que a otimização de rotas ajuda. A diferença entre backend_total_ms e model_call_ms é a sobrecarga do framework (serialização, roteamento, etc.).

Quando você executa o aplicativo e envia uma transação, a UI mostra as principais: Inferência do modelo, Busca de features e Lógica de negócios. Isso facilita ver a diferença que a otimização de rotas faz ou mostrar que uma busca de features no Lakebase adiciona milissegundos de um único dígito, em vez das centenas que você poderia esperar de uma conexão fria com o banco de dados.

Resultados: quão rápido ele é de verdade?

Enviamos 5.000 solicitações sequenciais (concorrência = 1, atraso de 50 ms entre as chamadas) para o endpoint fraud-detection-lakebase otimizado para rotas (CPU, tamanho de carga de trabalho "Small", região única do Azure) e coletamos a latência em cada camada, desde o container do modelo até o tempo de ida e volta (round-trip) do chamador. O objetivo era isolar a anatomia da latência por solicitação, para onde vão os milissegundos em cada camada (busca de features, inferência, sobrecarga de rede), em vez de testar a taxa de transferência sob concorrência. Esses números vêm do script de benchmark (scripts/benchmark.py), que chama o endpoint do modelo diretamente. O detalhamento da latência da UI mostra uma perspectiva diferente (Inferência do modelo, Busca de features e Lógica de negócios) medida por meio da rota completa do back-end.

MétricaO que ela medep50p75p90p95
Busca de features (model_lookup_ms)Leitura do Lakebase dentro do container do modelo8,9 ms9,8 ms11,7 ms13,9 ms
Inferência (model_inference_ms)Predição do CatBoost0.4 ms0.5 ms1.6 ms6.0 ms
Tempo total do modelo (model_total_ms)Busca + inferência + overhead do container9.5 ms10.9 ms14.9 ms17.6 ms
Ida e volta ponta a ponta (round_trip_ms)Chamada completa do plano de dados do chamador até a resposta27.2 ms29.6 ms33.8 ms37.3 ms
Overhead de rede (round_trip_ms - model_total_ms)Ida e volta menos o tempo do modelo17.4 ms18.5 ms19.8 ms21.1 ms

Alguns pontos se destacam:

  • A ida e volta ponta a ponta é de 27 ms na mediana e 37 ms no p95. Essa é a jornada completa: chamador → plano de dados otimizado para rotas → container do modelo → busca no Lakebase → inferência do CatBoost → resposta. Bem dentro do orçamento de latência para um fluxo de checkout.
  • A busca de features (feature lookup) fica na casa dos milissegundos de dígito único no p50 (8,9 ms). O pool de conexões do modelo com o Lakebase mantém as conexões ativas, de modo que a maioria das leituras ignora totalmente o handshake TLS. Mesmo no p95, a busca permanece abaixo de 14 ms.
  • A inferência é essencialmente gratuita. A predição do CatBoost em um vetor de 12 features leva 0,4 ms na mediana. O tempo do modelo é dominado pela busca de features, não pela predição em si.
  • O overhead de rede é de aproximadamente 17 ms. A diferença entre o que o container do modelo relata e o que o chamador vê é a infraestrutura de serviço: roteamento de requisições, serialização e o salto do plano de dados. A otimização de rotas mantém isso consistente: a variação de p50 para p95 é de apenas 4 ms.

Experimente você mesmo: implante o aplicativo no seu workspace

O aplicativo foi desenvolvido como um Databricks App (backend em FastAPI, frontend em React) usando o apx. Instale-o usando:

Você também precisará da autenticação da Databricks CLI configurada para o workspace onde o endpoint de serving de modelo e a instância do Lakebase estão implantados.

Para executar localmente:

Para implantar no seu workspace:

Os arquivos principais:

  • src/retail_app/backend/router.py: Endpoint de transação, verificação de fraude, regras de negócio
  • src/retail_app/backend/postgres.py: Pool de conexões do Lakebase, CRUD de perfil
  • model_training/fraud_model.py: MLflow pyfunc com busca de features e CatBoost
  • app.yml: Configuração de implantação (entrypoint do uvicorn, variáveis de ambiente)

Documentação relacionada

Casos de uso em tempo real com requisitos de latência abaixo de 50 ms, como pontuação de fraude, personalização e precificação dinâmica, podem ser desenvolvidos nativamente no Databricks. A otimização de rotas do Model Serving e o Lakebase colocam o caminho de inferência e o caminho de dados na mesma plataforma governada. Se você concluiu anteriormente que uma arquitetura lakehouse não conseguiria atender à latência de nível de checkout, vale a pena analisar novamente os números de benchmark apresentados aqui (mediana de 27 ms ponta a ponta com buscas de features de dígito único). Se uma carga de trabalho em tempo real estava na lista de tarefas "difíceis demais", este é o momento de revisitá-la. Implante o aplicativo em seu próprio workspace para ver o fluxo de latência por si mesmo e explore o Lakebase e o Databricks Model Serving.

(Esta publicação no blog foi traduzida utilizando ferramentas baseadas em inteligência artificial) Publicação original

Receba os posts mais recentes na sua caixa de entrada

Assine nosso blog e receba os posts mais recentes diretamente na sua caixa de entrada.