Deadpool vs bb8 em Rust: qual pool usar? | Rust Brasil

Compare deadpool, bb8 e pools nativos do SQLx em Rust. Veja diferenças, exemplos com PostgreSQL e Redis, timeouts, observabilidade e decisão em produção.

Se o projeto usa SQLx, escolha primeiro o pool nativo do SQLx; se usa tokio-postgres, Redis ou recursos heterogêneos, escolha Deadpool pela variedade de adaptadores e configuração prática; use bb8 quando você quer uma abstração enxuta baseada em ManageConnection ou quando o driver já oferece integração bb8. O critério decisivo não é um benchmark isolado: é compatibilidade com o driver, política de reciclagem, timeout de aquisição, observabilidade e comportamento sob saturação.

Um pool de conexões parece detalhe de infraestrutura até a API receber mais requisições do que o banco suporta. Nesse momento, a diferença entre “aguardar com limite” e “criar conexões sem controle” vira latência, erro 5xx ou indisponibilidade. Este guia compara Deadpool vs bb8, mostra exemplos com PostgreSQL e Redis e explica quando evitar ambos em favor do SQLx.

Resposta rápida: Deadpool, bb8 ou SQLx Pool?

SituaçãoEscolha inicialPor quê
Aplicação já usa SQLxsqlx::Poolintegração nativa com queries, transactions e drivers
tokio-postgres com configuração prontadeadpool-postgresadapter consolidado e reciclagem configurável
Redis assíncronodeadpool-redisintegração direta e ergonomia consistente
Driver fornece bb8::ManageConnectionbb8abstração pequena e familiar
Recurso customizado reutilizávelDeadpool ou bb8escolha pela API de manager e pelas métricas necessárias
SQLite local simplesfrequentemente pool nativo do driverlimite de escrita e modelo do SQLite importam mais que a marca do pool
Uma única conexão em CLI curtanenhum poolpooling adiciona complexidade sem ganho real

A regra de arquitetura é: use o pool mais próximo do driver. Um wrapper extra cria mais estados, tipos de erro e pontos de configuração. Só troque quando houver uma necessidade concreta que o pool nativo não atende.

O que um pool realmente faz

Abrir uma conexão com banco ou serviço remoto pode envolver DNS, TCP, TLS, autenticação e negociação de sessão. Repetir isso em toda requisição desperdiça tempo e recursos. O pool mantém um conjunto limitado de conexões prontas e executa um ciclo:

  1. uma task solicita um recurso;
  2. o pool entrega uma conexão ociosa ou cria uma até o limite;
  3. a task usa a conexão;
  4. ao liberar o guard, a conexão retorna ao pool;
  5. antes de reutilizá-la, o pool pode validar ou reciclar o recurso;
  6. se o limite está ocupado, a task aguarda — idealmente com timeout.

O pool não aumenta magicamente a capacidade do PostgreSQL. Ele impõe orçamento. Se o banco aguenta 40 conexões da aplicação e você inicia quatro réplicas, configurar 40 por réplica pode abrir 160 conexões. O cálculo precisa considerar todas as instâncias, migrações, ferramentas administrativas e margem de segurança.

Deadpool: adapters prontos e recursos assíncronos

Deadpool é uma família de crates para gerenciar objetos reutilizáveis em aplicações assíncronas. O crate base oferece a abstração; crates como deadpool-postgres e deadpool-redis conectam o pool a clientes concretos.

Ele costuma encaixar bem quando:

  • o projeto usa tokio-postgres diretamente;
  • há PostgreSQL e Redis no mesmo serviço;
  • você quer configuração serializável e factories por adapter;
  • a política de reciclagem precisa de ajuste;
  • o time prefere uma API de pool consistente entre recursos diferentes.

Exemplo com deadpool-postgres

O Cargo.toml varia conforme as versões estáveis e os recursos TLS escolhidos. A estrutura típica é:

[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio-postgres = "0.7"
deadpool-postgres = "0.14"

Criação e uso do pool:

use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod};
use tokio_postgres::{Config, NoTls};

fn criar_pool(database_url: &str) -> Result<Pool, Box<dyn std::error::Error>> {
    let pg_config: Config = database_url.parse()?;
    let manager = Manager::from_config(
        pg_config,
        NoTls,
        ManagerConfig {
            recycling_method: RecyclingMethod::Fast,
        },
    );

    let pool = Pool::builder(manager)
        .max_size(16)
        .build()?;

    Ok(pool)
}

async fn buscar_nome(
    pool: &Pool,
    usuario_id: i64,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
    let client = pool.get().await?;
    let row = client
        .query_opt(
            "select nome from usuarios where id = $1",
            &[&usuario_id],
        )
        .await?;

    Ok(row.map(|r| r.get("nome")))
}

NoTls simplifica o exemplo, mas não é recomendação universal. Em produção, configure TLS de acordo com o provedor e valide certificados. O ponto importante é que o valor retornado por pool.get() controla o empréstimo; quando sai de escopo, a conexão pode voltar ao pool.

Reciclagem não é detalhe cosmético

Uma conexão pode continuar aberta no cliente e já estar inválida no servidor. Reinícios, proxies, timeouts de rede e mudanças de credencial criam conexões zumbis. A política de reciclagem decide quanto trabalho fazer antes de entregar o recurso novamente.

  • Validação mais leve: menor overhead, mas o primeiro comando pode descobrir uma conexão quebrada.
  • Validação por query: mais confiança antes do uso, ao custo de uma ida adicional ao banco.
  • Recriação após erro: remove o recurso defeituoso e abre outro dentro do orçamento.

Não copie uma política sem medir. Uma query de validação em toda aquisição pode multiplicar tráfego; validação leve demais pode elevar erros logo após failover.

bb8: uma abstração pequena com ManageConnection

bb8 modela o pool em torno de um manager que sabe:

  • criar uma conexão;
  • verificar se ela continua válida;
  • decidir se ela está quebrada.

Essa separação deixa a biblioteca genérica e torna o contrato explícito. bb8 é uma boa escolha quando a stack já possui um adapter, como bb8-postgres, ou quando você quer implementar um manager pequeno para um protocolo interno.

Exemplo com bb8-postgres

[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio-postgres = "0.7"
bb8 = "0.8"
bb8-postgres = "0.8"
use bb8::Pool;
use bb8_postgres::PostgresConnectionManager;
use tokio_postgres::NoTls;

type PgPool = Pool<PostgresConnectionManager<NoTls>>;

async fn criar_pool(
    database_url: &str,
) -> Result<PgPool, Box<dyn std::error::Error>> {
    let manager = PostgresConnectionManager::new_from_stringlike(
        database_url,
        NoTls,
    )?;

    let pool = Pool::builder()
        .max_size(16)
        .build(manager)
        .await?;

    Ok(pool)
}

async fn buscar_email(
    pool: &PgPool,
    usuario_id: i64,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
    let conn = pool.get().await?;
    let row = conn
        .query_opt(
            "select email from usuarios where id = $1",
            &[&usuario_id],
        )
        .await?;

    Ok(row.map(|r| r.get("email")))
}

A experiência no handler é parecida com Deadpool: adquirir, executar e liberar. A diferença aparece mais na configuração, no contrato do manager, nos adapters disponíveis e no modo como o projeto instrumenta estado e erros.

Deadpool vs bb8: comparação prática

CritérioDeadpoolbb8
Abstração centralmanager + objetos gerenciadosManageConnection
PostgreSQLdeadpool-postgresbb8-postgres
Redisadapter dedicado populardepende do adapter escolhido
Recursos não-SQLum dos focos do ecossistemapossível com manager customizado
Curva inicialsimples com adapters prontossimples quando já existe manager
Reciclagempolíticas e hooks do adaptervalidação/quebra no manager
Melhor argumentoconsistência entre recursoscontrato genérico enxuto
Razão ruim para escolher“parece mais rápido” sem carga real“tem menos código” ignorando operação

Em aplicações normais, a latência da query e da rede domina diferenças minúsculas de bookkeeping do pool. Compare sob o workload real: quantidade de tasks, duração das queries, p95 de espera, falhas de conexão e comportamento durante restart do banco.

Quando o pool nativo do SQLx é a resposta

Se o projeto já usa SQLx, PgPool, MySqlPool e SqlitePool integram aquisição, executors e transactions. O caminho idiomático é:

use sqlx::postgres::PgPoolOptions;
use std::time::Duration;

async fn criar_pool(database_url: &str) -> Result<sqlx::PgPool, sqlx::Error> {
    PgPoolOptions::new()
        .max_connections(16)
        .min_connections(2)
        .acquire_timeout(Duration::from_secs(2))
        .idle_timeout(Duration::from_secs(600))
        .connect(database_url)
        .await
}

Você ganha um único modelo de erro e integração direta com query!, migrations e transactions. Colocar Deadpool ou bb8 em volta de SQLx normalmente duplica responsabilidade.

Use outro pool apenas se ele estiver gerenciando outro tipo de recurso. Exemplo: SQLx para PostgreSQL e deadpool-redis para Redis é uma combinação coerente; SQLx dentro de bb8 dentro de um state Axum raramente é.

Para uma comparação de acesso a dados mais ampla, veja Diesel vs SQLx e o guia de migrations SQLx com PostgreSQL.

Redis com Deadpool: o pool também vale fora do SQL

Redis é um caso comum porque conexões e multiplexação dependem do cliente e do padrão de carga. Com deadpool-redis, a estrutura costuma ser:

use deadpool_redis::{Config, Pool, Runtime};
use deadpool_redis::redis::AsyncCommands;

fn criar_pool_redis(url: &str) -> Result<Pool, deadpool_redis::CreatePoolError> {
    let config = Config::from_url(url);
    config.create_pool(Some(Runtime::Tokio1))
}

async fn buscar_cache(
    pool: &Pool,
    chave: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
    let mut conn = pool.get().await?;
    let valor: Option<String> = conn.get(chave).await?;
    Ok(valor)
}

Antes de adotar um pool Redis, confira se o cliente e o modo escolhido já oferecem multiplexação adequada. Pooling não é reflexo automático: ele é útil quando a concorrência, o protocolo e as garantias do cliente justificam múltiplos recursos.

O guia de Redis como cache em backends Rust cobre TTL, stampede, invalidação e fallback — problemas que um pool sozinho não resolve.

Integração com Axum sem espalhar o pool

O pool deve entrar no state da aplicação e permanecer na borda da infraestrutura. Handlers podem chamar um repositório; regras de negócio não precisam conhecer deadpool_postgres::Pool.

use axum::{extract::State, http::StatusCode, Json};
use serde::Serialize;
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    usuarios: Arc<dyn UsuarioRepository>,
}

#[derive(Serialize)]
struct UsuarioDto {
    id: i64,
    nome: String,
}

#[axum::async_trait]
trait UsuarioRepository: Send + Sync {
    async fn por_id(&self, id: i64) -> anyhow::Result<Option<UsuarioDto>>;
}

async fn obter_usuario(
    State(state): State<AppState>,
    axum::extract::Path(id): axum::extract::Path<i64>,
) -> Result<Json<UsuarioDto>, StatusCode> {
    state
        .usuarios
        .por_id(id)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

A implementação concreta guarda Deadpool, bb8 ou SQLx. Essa fronteira facilita testes e evita uma migração dolorosa se o acesso a dados mudar. Para o restante da stack web, continue pelo tutorial de API REST com Axum e pelo guia de serviços resilientes com Tower e Axum.

Timeout, backpressure e saturação

Um pool cheio cria uma fila invisível de tasks aguardando. Sem timeout, a latência cresce até o balanceador desistir. Sem limite externo, milhares de tasks podem consumir memória enquanto esperam 16 conexões.

Uma política robusta combina:

  1. max_size coerente com a capacidade do banco;
  2. timeout curto de aquisição, abaixo do timeout HTTP total;
  3. limite de concorrência antes do handler ou repositório;
  4. timeout de query no servidor/cliente;
  5. cancelamento quando a requisição do usuário desaparece;
  6. resposta diferenciada para saturação temporária, sem vazar detalhes.

Exemplo de timeout em torno da aquisição quando a API do pool/projeto não o aplica diretamente:

use std::time::Duration;

let conn = tokio::time::timeout(
    Duration::from_secs(2),
    pool.get(),
)
.await
.map_err(|_| AppError::PoolSaturado)??;

Não segure a conexão enquanto faz chamada HTTP externa, espera fila ou processa um arquivo. Adquira o mais tarde possível e libere logo após a transaction/query. Uma conexão presa durante trabalho não relacionado reduz throughput do sistema inteiro.

Observabilidade: meça a espera, não apenas a query

Duas latências importam:

  • tempo de aquisição: quanto uma task esperou pelo pool;
  • tempo de operação: quanto a query/comando levou após obter conexão.

Se você mede só a query, uma requisição pode parecer ter executado SQL em 20 ms enquanto ficou 1,8 s esperando uma conexão. Registre:

  • conexões em uso e ociosas;
  • tamanho máximo configurado;
  • aquisições bem-sucedidas;
  • timeouts de aquisição;
  • erros de criação/reciclagem;
  • histograma de espera;
  • histograma de query por operação normalizada.

Não use SQL completo, ID de usuário ou URL de banco como label. O guia de Prometheus e métricas em Rust mostra como evitar cardinalidade explosiva; tracing ajuda a correlacionar a espera do pool com a requisição.

Como dimensionar o pool

Não existe fórmula universal, mas existe método:

  1. descubra o limite seguro de conexões do banco;
  2. reserve conexões para administração, migrations e jobs;
  3. divida o restante pelo máximo de réplicas da aplicação;
  4. comece conservador;
  5. rode carga representativa;
  6. observe espera, CPU do banco, locks, I/O e p95/p99;
  7. aumente apenas se o banco tem folga e a espera é realmente o gargalo.

Exemplo: PostgreSQL aceita 100 conexões; você reserva 20; pode ter até quatro réplicas. O teto teórico seria 20 por réplica, mas começar com 12–16 deixa margem para deploy rolling e variação operacional.

Mais conexões podem piorar performance ao aumentar contenção, memória e concorrência de disco. Queries lentas, índices ausentes e transactions longas não são corrigidos com max_size = 200.

Testes que valem mais que um benchmark sintético

Esgotamento

Configure pool de tamanho 2, segure duas conexões e confirme que a terceira aquisição expira no tempo esperado.

Conexão inválida

Reinicie o banco ou encerre uma sessão e verifique se o pool descarta/recicla o recurso sem envenenar requisições seguintes.

Transaction longa

Force uma operação lenta e confirme que o limite de concorrência protege o restante da API.

Shutdown

Interrompa o serviço durante carga e valide graceful shutdown: novas requisições param, operações em voo encerram dentro do prazo e recursos são liberados.

Réplicas

Teste o total agregado. Um container local não revela o que acontece quando seis pods multiplicam o pool.

Combine esses testes com as estratégias de testes em Rust e instrumente o cenário antes de comparar bibliotecas.

Erros comuns

Escolher por stars ou microbenchmark

Adapters, manutenção e comportamento de falha pesam mais que nanossegundos no caminho feliz.

Não configurar timeout de aquisição

A fila cresce e transforma saturação breve em cascata de timeouts.

Abrir um pool por requisição

O pool deve ser criado no bootstrap e compartilhado. Criá-lo por handler elimina o benefício e pode atacar o banco com novas conexões.

Manter conexão durante trabalho externo

Chamada a API, renderização, upload e processamento CPU-bound não deveriam prender uma conexão ociosa.

Ignorar o número de réplicas

max_size é por processo. Autoscaling multiplica conexões.

Tratar pool como cache de resultado

Pool reutiliza conexões; cache reutiliza dados. São problemas diferentes.

Misturar três pools para o mesmo driver

Padronize por datastore. SQLx Pool para PostgreSQL e Deadpool para Redis pode fazer sentido; dois pools concorrentes para o mesmo PostgreSQL quase nunca fazem.

Checklist de decisão para produção

  • O driver já possui pool nativo bem integrado?
  • Existe adapter mantido para Deadpool ou bb8?
  • O pool suporta timeout de aquisição e limites explícitos?
  • A reciclagem funciona após restart/failover?
  • As métricas mostram espera, uso e erro?
  • O tamanho considera todas as réplicas?
  • Transactions são curtas e conexões são liberadas cedo?
  • Há backpressure antes que milhares de tasks aguardem?
  • TLS e credenciais são configurados fora do código?
  • O shutdown foi testado sob carga?
  • O repositório esconde o tipo concreto do pool do domínio?
  • O time documentou por que escolheu Deadpool, bb8 ou SQLx?

Deadpool e bb8 como sinal de carreira

Em entrevistas de backend Rust, dizer “usei PostgreSQL” é menos forte que explicar:

  • como calculou o pool por réplica;
  • por que definiu timeout de aquisição;
  • como evitou segurar conexão durante I/O externo;
  • quais métricas detectam saturação;
  • como o serviço se comporta em failover;
  • por que escolheu o pool nativo ou um adapter.

Um projeto de portfólio pode combinar Axum, PostgreSQL, migrations, métricas e teste de esgotamento. Isso demonstra engenharia de produção e conversa diretamente com vagas Rust, empresas que usam Rust e a trilha de carreira em backend web.

Perguntas frequentes

Deadpool ou bb8: qual pool escolher em Rust?

Deadpool é uma escolha forte para adapters prontos e múltiplos tipos de recurso. bb8 é atraente quando a stack já oferece um ManageConnection ou quando você prefere seu contrato genérico. A compatibilidade com o driver deve decidir antes da preferência estética.

Preciso de Deadpool ou bb8 se já uso SQLx?

Normalmente não. Comece com sqlx::Pool. Use Deadpool ou bb8 para outro recurso ou integração que realmente precise deles, não como camada redundante.

Deadpool serve apenas para banco de dados?

Não. Ele gerencia recursos assíncronos reutilizáveis; PostgreSQL e Redis são adapters populares, mas managers customizados também são possíveis.

Como evitar que o pool derrube a API sob carga?

Combine limite correto, timeout de aquisição, backpressure, queries rápidas e métricas de espera. Não tente resolver saturação apenas aumentando conexões.

Qual é a diferença entre pool e semáforo?

Pool mantém recursos reutilizáveis. Semáforo limita concorrência. Os dois podem trabalhar juntos para proteger banco, memória e latência.

Como testar um pool de conexões em Rust?

Force esgotamento, timeout, conexão inválida, shutdown e concorrência entre réplicas. Teste o caminho de falha, não apenas uma query bem-sucedida.

Conclusão

A escolha entre Deadpool e bb8 é menos dramática do que a operação correta do pool. Para SQLx, fique no pool nativo. Para tokio-postgres, compare os adapters e escolha a API que o time consegue configurar, observar e testar. Para Redis e recursos variados, Deadpool costuma oferecer um caminho prático; para managers pequenos e integrações existentes, bb8 continua uma opção sólida.

O resultado maduro tem quatro propriedades: limite global pensado por réplica, timeout de aquisição, conexões mantidas pelo menor tempo possível e métricas que separam espera de execução. Continue pelo guia de SQLx em Rust, PostgreSQL com Rust, métricas Prometheus e resiliência com Tower/Axum. Um pool bem escolhido ajuda; um pool bem operado mantém o serviço vivo.