# ============================================================================= # P4.7 — PgBouncer em transaction mode, UM POOL POR DATABASE E ROLE. # # ── POR QUE UM POOL POR ROLE, E NAO UM POOL SO ─────────────────────────────── # # Em transaction mode, a conexao de SERVIDOR e devolvida ao pool no fim de cada # transacao e entregue a outro cliente. Se dois tenants dividissem o mesmo pool, # o segundo herdaria a sessao autenticada do primeiro: mesmo `search_path`, # mesmas GUCs de sessao, mesmo usuario efetivo no servidor. # # Todo o isolamento de P2.1 — database por tenant, role por tenant, CONNECT # revogado — seria contornado por baixo, sem erro nenhum e sem log. # # O PgBouncer separa por (database, user), e e por isso que cada tenant tem o # proprio par. Conferido em 2026-09-18 com `SHOW POOLS`: seis clientes de dois # tenants produziram seis conexoes de servidor, tres em cada database, sem # cruzamento. # # ── A LISTA DE DATABASES NAO E GERADA SOZINHA, E ISSO E PROPOSITAL ─────────── # # Cada tenant novo acrescenta uma linha aqui. Um `*` generico funcionaria e # tiraria a fronteira: qualquer database da instancia viraria alcancavel pelo # bouncer, inclusive `postgres` e `template1`. A lista explicita e a mesma ideia # do `endurecer_instancia.sql` — o que nao esta escrito nao existe. # # O provisionamento automatico da fase 5 (card 163) e quem passa a escrever esta # linha; ate la, e passo do runbook. # # ── userlist.txt NAO FICA AQUI ─────────────────────────────────────────────── # # Ele carrega os hashes SCRAM dos roles. Vem de um SealedSecret, gerado no # servidor a partir do proprio `pg_authid` — do mesmo jeito que as outras # credenciais deste tenant. Ver `scripts/gerar_credenciais_plataforma.sh`. # ============================================================================= apiVersion: v1 kind: ConfigMap metadata: name: pgbouncer-conf namespace: plataforma-prod data: pgbouncer.ini: | [databases] ; Uma linha por tenant. Sem `*`: ver o cabecalho deste arquivo. athleticmap_plataforma = host=pg-compartilhado port=5432 dbname=athleticmap_plataforma athleticmap_escolinha = host=pg-compartilhado port=5432 dbname=athleticmap_escolinha athleticmap_piloto = host=pg-compartilhado port=5432 dbname=athleticmap_piloto athleticmap_demo = host=pg-compartilhado port=5432 dbname=athleticmap_demo athleticmap_acme = host=pg-compartilhado port=5432 dbname=athleticmap_acme athleticmap_escolinha-de-teste = host=pg-compartilhado port=5432 dbname=athleticmap_escolinha-de-teste athleticmap_escolinha-de-teste-2 = host=pg-compartilhado port=5432 dbname=athleticmap_escolinha-de-teste-2 athleticmap_zico-0 = host=pg-compartilhado port=5432 dbname=athleticmap_zico-0 athleticmap_teste5 = host=pg-compartilhado port=5432 dbname=athleticmap_teste5 [pgbouncer] listen_addr = 0.0.0.0 listen_port = 6432 auth_type = scram-sha-256 auth_file = /etc/pgbouncer/userlist.txt ; TRANSACTION, e nao session: com session pooling a conexao de servidor fica ; presa ao cliente ate ele desconectar, e um runtime com pool de 20 seguraria ; 20 conexoes reais o dia inteiro. O ganho do bouncer viria a zero. pool_mode = transaction ; 400 clientes contra ate 15 servidores por pool. A conta esta no ; 12-postgres-compartilhado.yaml: 18 tenants x 15 = 270, e o ; max_connections do Postgres e 300. max_client_conn = 400 default_pool_size = 10 min_pool_size = 0 reserve_pool_size = 5 reserve_pool_timeout = 3 ; VAZIO de proposito. O padrao (`DISCARD ALL`) e para session pooling; em ; transaction mode o PgBouncer ja garante que nada de sessao atravessa, e ; rodar DISCARD a cada transacao custa uma ida ao servidor por transacao. server_reset_query = ; O driver JDBC envia `extra_float_digits` no startup, e o PgBouncer recusa ; parametro que nao conhece. Sem esta linha, TODA conexao Java falha com ; "unsupported startup parameter" — e a mensagem nao diz que o culpado e o ; bouncer. ignore_startup_parameters = extra_float_digits admin_users = postgres stats_users = postgres log_connections = 0 log_disconnections = 0 --- apiVersion: apps/v1 kind: Deployment metadata: name: pgbouncer namespace: plataforma-prod spec: # Pode ter mais de uma replica: o PgBouncer nao guarda estado entre conexoes. # Mas cada replica tem o PROPRIO pool — duas replicas dobram as conexoes de # servidor, e a conta de max_connections precisa ser refeita antes de subir a # segunda. replicas: 1 selector: matchLabels: { app: pgbouncer } template: metadata: labels: { app: pgbouncer } spec: containers: - name: pgbouncer # A tag e `v1.25.2-p0`, e NAO `1.25.2`: essa segunda nao existe no Docker Hub # e o pod ficaria em ImagePullBackOff. Achado pelo ensaio local # (scripts/verificar_migracao_compartilhado.sh) antes do primeiro deploy. image: docker.io/edoburu/pgbouncer:v1.25.2-p0 ports: [{ containerPort: 6432 }] volumeMounts: - { name: conf, mountPath: /etc/pgbouncer/pgbouncer.ini, subPath: pgbouncer.ini } - { name: userlist, mountPath: /etc/pgbouncer/userlist.txt, subPath: userlist.txt } resources: requests: { cpu: 50m, memory: 32Mi } limits: { cpu: 500m, memory: 128Mi } readinessProbe: tcpSocket: { port: 6432 } initialDelaySeconds: 5 periodSeconds: 10 volumes: - name: conf configMap: { name: pgbouncer-conf } - name: userlist secret: { secretName: pgbouncer-userlist } --- apiVersion: v1 kind: Service metadata: name: pgbouncer namespace: plataforma-prod spec: selector: { app: pgbouncer } ports: [{ port: 6432, targetPort: 6432 }]