Postgres – Pooler de conexão com Cache

Postgres é sensacional, é parrudo e todo mundo sabe que é o “queridinho” para dados hoje em dia. Mas ele não faz milagre. Se você tem uma aplicação que espanca o banco com queries repetitivas (aqueles SELECTs que não mudam quase nunca, mas rodam milhares de vezes por segundo), o seu I/O vai pro espaço, a CPU sobe e a latência vira um pesadelo.

Recentemente, trabalhei em uma adaptação do PG_Dog (um proxy/pooler focado em Postgres) para resolver exatamente esse problema. O “pulo do gato”? Coloquei uma camada de cache chave-valor usando #Redis em paralelo ao pooler.

Por que colocar cache no Pooler?

Se a sua aplicação é legada ou se você tem um time de dev que não quer (ou não pode) mexer no código para implementar cache, você resolve isso no “meio do caminho”. O PG_Dog intercepta a query, vê se ela já foi feita antes e entrega o resultado na velocidade da memória.

A vantagem de usar Redis com Sharding em paralelo é que a gente não cria um novo gargalo. Se um nó de Redis ficar cheio ou sobrecarregado, a carga está distribuída entre vários shards.

Como a mágica acontece?

Basicamente, a estrutura funciona assim:

  1. Identificação: A aplicação manda a query pro PG_Dog.
  2. Hashing: O PG_Dog gera um hash único baseado no SQL e nos parâmetros. Esse hash é a chave.
  3. Check de Cache: Ele consulta os #shards do Redis em paralelo.
  4. Cache Hit: Se o dado estiver lá, ele devolve pro usuário em <5ms. O Postgres nem acorda.
  5. Cache Miss: Se não estiver, ele executa no Postgres, popula o Redis para a próxima e entrega o resultado.

Invalidação?

Claro que temos, funciona assim:

  1. Identificação: A aplicação manda o DML ou DDL pro PG_Dog.
  2. Check de Cache: Ele consulta os #shards do Redis em paralelo.
  3. Cache Delete Se a tabela estiver lá ele deleta a chave ou chaves envolvidas.
  4. No Commit: Se uma transação for aberta mas não comitada eu não limpo o cache.

Por que cache externo?

A vantagem de ter um pooler é poder escalar ele horizontalmente, afinal, não queremos gerar um ponto único de falha no ambiente.

Mas, estalar ele horizontal implica em trazer um outro problema para a mesa, um select pode criar um cache para aquele select, mas e se um DML ou DDL usar um outro pooler para fazer a alteração, como invalidar esse cache para evitar um falso positivo? por isso a ideia de usar um Redis (ou qualquer outro cache que use a mesma tecnologia) externo ao pooler.

O container do pooler fica pequeno e você escala o cache da melhor forma possível. (Shard, Cluster, Single, aí é com você).

    Configuração (Mão na massa)

    Não adianta só falar, tem que mostrar como configura. No arquivo de configuração do seu PG_Dog (que agora aceita múltiplos backends de cache), a coisa fica mais ou menos assim:

    YAML

    # Exemplo de config do PG_Dog com Redis Sharding
    [result_cache]
    enabled = true
    # Redis/Valkey/Dragonfly connection URL.
    redis_url = "redis://127.0.0.1:6379"
    # TTL (seconds). If omitted, PgDog applies a default.
    expire_seconds = 30
    # Don't cache very large results.
    max_entry_bytes = 524288
    # Redis key prefix.
    key_prefix = "pgdog:result_cache"
    # Optional allow/deny lists (regex) to control what gets cached.
    # Unsafe lists take precedence over safe lists.
    cache_safe_schema_list = []
    cache_unsafe_schema_list = []
    cache_safe_table_list = []
    cache_unsafe_table_list = []
    
    

    Na configuração acima você pode ver que da pra personalizar como no PG_Pool2, não fazer cache de tabelas específicas ou de schemas específicos, e por sinal, da pra usar regex caso precise.

    Conclusão

    Essa adaptação transforma o PG_Dog em uma ferramenta de aceleração ativa. Você ganha fôlego no banco de dados principal, economiza em instância de nuvem (RDS/CloudSQL).

    O código está aberto para quem quiser testar, quebrar ou melhorar lá no meu GitHub:

    👉 github.com/bigleka/pgdog

    Ainda estou em fase de testes totalmente Alfa, se alguém tiver coragem de começar a testar e ir encontrando erros é só avisar.