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
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:
O repositório completo está no GitHub. Você pode fazer um fork dele, implantá-lo em seu workspace e clicar em "pagar" você mesmo.
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.

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.
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.
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.
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 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.
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.
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.
Aqui está o panorama completo da latência para uma única transação:
| Medição | O que ela captura | Onde é medida |
|---|---|---|
| model_call_ms | Tempo total de execução (wall-clock) para toda a chamada de serviço | Back-end (router.py) |
| model_lookup_ms | Busca de features dentro do container do modelo | Modelo (fraud_model.py) |
| model_interfere_ms | Tempo de previsão do CatBoost | Modelo (fraud_model.py) |
| model_total_ms | Tempo total dentro do container do modelo | Modelo (fraud_model.py) |
| business_logic_ms | Leitura de perfil + avaliação de regras | Back-end (router.py) |
| backend_total_ms | Tempo total de execução (wall-clock) desde o início da solicitação até a chamada do modelo de fraude | Back-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.
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étrica | O que ela mede | p50 | p75 | p90 | p95 |
|---|---|---|---|---|---|
| Busca de features (model_lookup_ms) | Leitura do Lakebase dentro do container do modelo | 8,9 ms | 9,8 ms | 11,7 ms | 13,9 ms |
| Inferência (model_inference_ms) | Predição do CatBoost | 0.4 ms | 0.5 ms | 1.6 ms | 6.0 ms |
| Tempo total do modelo (model_total_ms) | Busca + inferência + overhead do container | 9.5 ms | 10.9 ms | 14.9 ms | 17.6 ms |
| Ida e volta ponta a ponta (round_trip_ms) | Chamada completa do plano de dados do chamador até a resposta | 27.2 ms | 29.6 ms | 33.8 ms | 37.3 ms |
| Overhead de rede (round_trip_ms - model_total_ms) | Ida e volta menos o tempo do modelo | 17.4 ms | 18.5 ms | 19.8 ms | 21.1 ms |
Alguns pontos se destacam:
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óciosrc/retail_app/backend/postgres.py: Pool de conexões do Lakebase, CRUD de perfilmodel_training/fraud_model.py: MLflow pyfunc com busca de features e CatBoostapp.yml: Configuração de implantação (entrypoint do uvicorn, variáveis de ambiente)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
Assine nosso blog e receba os posts mais recentes diretamente na sua caixa de entrada.