trilha

Tecnologias/02 - Bancos de Dados/01 - Relacionais/01 - PostgreSQL6 min

Arquitetura de Processos e Memória

Conceito

O Postgres é multiprocesso, não multithread. Essa é a decisão mais consequente da sua arquitetura, e ela é de 1986 — mantida porque isolamento de processo dá robustez: um backend que corrompe memória morre sozinho, sem derrubar o servidor.

Os processos

postmaster (pai)
├── backend            ← UM POR CONEXÃO. executa suas queries.
├── backend
├── background writer  ← empurra páginas sujas do buffer para o disco, aos poucos
├── checkpointer       ← força TODAS as páginas sujas ao disco, periodicamente
├── WAL writer         ← grava o WAL buffer em disco
├── autovacuum launcher → autovacuum worker (N)  ← limpa tuplas mortas
├── stats collector / logger
└── walsender / walreceiver  ← replicação

Quando um cliente conecta, o postmaster faz fork. O backend nasce com ~5–10 MB de memória privada e vive até a conexão fechar. Consequências, todas práticas:

  • Conectar é caro (fork + inicialização de catálogo): ~1–3 ms, contra microssegundos de uma thread.
  • Cada backend consome memória própria (work_mem por operação, cache de catálogo, planos preparados).
  • Não há cap de "muitas conexões ociosas serem baratas": 500 conexões idle são 500 processos no scheduler do SO.

Daí a regra prática: max_connections alto não é generosidade, é risco. Ver 6. Particionamento e Pooling de Conexões.

A memória, em dois blocos

1. Memória compartilhada — alocada uma vez, na inicialização:

Parâmetro O que é Ponto de partida
shared_buffers cache de páginas de dados (8 KB cada) 25% da RAM
wal_buffers buffer do WAL antes de ir ao disco auto (1/32 de shared_buffers)

2. Memória por backend / por operação — multiplica pelo número de conexões:

Parâmetro O que é Cuidado
work_mem por operação de sort/hash, não por query uma query com 4 sorts × 50 conexões = 200 × work_mem
maintenance_work_mem VACUUM, CREATE INDEX, REINDEX pode ser generoso (poucos concorrentes)
temp_buffers tabelas temporárias por sessão

E um parâmetro que não aloca nada:

  • effective_cache_size — apenas informa ao planejador quanta memória o SO provavelmente tem em cache. Valor alto (≈ 50–75% da RAM) faz o planejador confiar mais em index scan; valor baixo o empurra para seq scan. É puro palpite para o otimizador. Ver 5. Índices e o Planejador.

O caminho de uma página

query pede uma linha
   ↓
está em shared_buffers?  ── sim → serve da RAM (buffer hit)
   ↓ não
está no page cache do SO? ─ sim → cópia rápida
   ↓ não
lê do disco
   ↓
UPDATE marca a página como "suja" (dirty)
   ↓
background writer escreve aos poucos  /  checkpointer força tudo

O Postgres depende do page cache do SO como segundo nível de cache — é por isso que shared_buffers em 25% (e não 80%) costuma ser melhor: você deixa RAM para o SO cachear os mesmos arquivos, sem dupla contabilidade.

Trade-offs

Processo dá robustez e custa escala de conexão. O MySQL usa thread por conexão e suporta milhares; o Postgres precisa de pooler antes disso. Em troca, um crash de backend no Postgres é isolado (o postmaster reinicia o conjunto se necessário, sem corromper).

work_mem é o parâmetro mais perigoso do arquivo. Ele parece "por conexão" e é por nó de execução. Subir de 4 MB para 256 MB porque "temos RAM" é a receita clássica de OOM: 100 conexões × 3 operações × 256 MB = 76 GB de teto teórico.

Ajustar por parâmetro global é grosseiro. A saída madura é work_mem conservador no postgresql.conf e SET LOCAL work_mem = '256MB' na transação do relatório que realmente precisa.

O trade-off central: o Postgres separa "memória compartilhada de cache" de "memória por operação", e só a primeira tem teto. Toda configuração de memória no Postgres é uma multiplicação implícita pelo número de conexões — e é por isso que limitar conexão é pré-requisito para ajustar memória.

Exemplo prático

Baseline defensável para uma máquina de 16 GB de RAM, 4 vCPU, SSD, carga OLTP:

# --- memória compartilhada ---
shared_buffers = 4GB                  # 25% da RAM
effective_cache_size = 12GB           # só um palpite para o planejador
maintenance_work_mem = 1GB            # VACUUM/CREATE INDEX mais rápidos

# --- memória por operação ---
work_mem = 16MB                       # conservador de propósito; suba por sessão
max_connections = 100                 # com PgBouncer na frente

# --- WAL e checkpoint ---
wal_compression = on
max_wal_size = 4GB                    # checkpoints mais espaçados, menos I/O em pico
min_wal_size = 1GB
checkpoint_completion_target = 0.9    # espalha o I/O do checkpoint no tempo

# --- planejador em SSD ---
random_page_cost = 1.1                # padrão 4.0 assume disco mecânico!
effective_io_concurrency = 200

# --- autovacuum mais agressivo que o padrão ---
autovacuum_vacuum_scale_factor = 0.05     # padrão 0.2 = 20% da tabela morta
autovacuum_vacuum_cost_limit = 2000
autovacuum_naptime = 15s

# --- observabilidade ---
shared_preload_libraries = 'pg_stat_statements'
track_io_timing = on
log_min_duration_statement = 1000     # loga query > 1s
log_lock_waits = on
log_autovacuum_min_duration = 0       # veja o autovacuum trabalhando
random_page_cost = 4.0 é o padrão errado em 2026

O valor default assume disco rotacional, onde acesso aleatório era 4× mais caro que sequencial. Em SSD/NVMe a diferença é pequena. Deixar 4.0 faz o planejador evitar índices e preferir varredura sequencial — é uma das causas mais comuns de "por que ele não usa meu índice?". Ver 5. Índices e o Planejador.

Inspecionar em produção

-- quem está conectado e fazendo o quê
SELECT pid, usename, state, wait_event_type, wait_event,
       now() - query_start AS duracao, left(query, 60) AS query
  FROM pg_stat_activity
 WHERE backend_type = 'client backend' AND state <> 'idle'
 ORDER BY duracao DESC;

-- taxa de acerto do cache (queira > 0.99 em OLTP)
SELECT sum(blks_hit)::float / nullif(sum(blks_hit) + sum(blks_read), 0) AS cache_hit
  FROM pg_stat_database;

-- conexões por estado: 'idle in transaction' é o inimigo (bloqueia VACUUM)
SELECT state, count(*) FROM pg_stat_activity GROUP BY state ORDER BY 2 DESC;

Como isso falha em produção

Sintoma Causa
FATAL: sorry, too many clients already max_connections estourado — falta pooler (5. Go em Produção)
OOM killer matando o postmaster work_mem × conexões × operações
Latência com picos periódicos checkpoint escrevendo tudo de uma vez: suba max_wal_size e checkpoint_completion_target
CPU alta com pouca query context switch de centenas de backends
Planejador ignorando índices random_page_cost de disco mecânico; estatísticas velhas
Servidor lento após restart shared_buffers frio; considere pg_prewarm

Aprofundamento aplicado

Construir um orçamento de memória

Imagine 40 consultas simultâneas, cada uma com duas ordenações que cheguem a usar 32 MiB. Só essa conta ilustrativa alcança 2.560 MiB; ainda faltam buffers compartilhados, outros nós do plano, workers, conexões e memória do sistema operacional.

work_mem não é uma reserva única por conexão nem um teto total do servidor. Operações de hash também podem usar o multiplicador correspondente. A concorrência efetiva e o plano determinam como os consumos se somam. Referência: parâmetros de memória.

Sintoma e experimento

Se um relatório grava temporários, aumentar memória só para essa sessão pode ser um experimento útil. Aumentar globalmente pode resolver um relatório e causar pressão de memória durante o pico de tráfego. Compare duração, temporários e memória sob concorrência.

Exercício: calcule dois cenários: 5 relatórios simultâneos e 50. Resultado esperado: perceba por que uma configuração que cabe no teste isolado pode exceder o orçamento real. Explicite quais parcelas a estimativa deixou de fora.

Relacionado


Parte de MOC PostgreSQL · Resource Consumption (docs)

Buscar

Busca por título, seção e texto das notas