Files
athletic-map-deploy/tenants/plataforma/12-postgres-compartilhado.yaml
T

147 lines
5.7 KiB
YAML

# =============================================================================
# P4.7 — UMA instancia de Postgres para todos os tenants, com PgBouncer na
# frente.
#
# Hoje cada tenant tem o proprio pod de Postgres. Sao cinco instancias servindo
# bancos de poucos megabytes cada, e cada uma paga um processo, um volume e um
# ciclo de backup proprio.
#
# O modelo alvo ja estava decidido e preparado em P2.1: um DATABASE e um ROLE por
# tenant na mesma instancia (`athleticmap_<tenant>`, `r_<tenant>`), com schema por
# contexto DENTRO do database. Sao duas fronteiras diferentes — schema separa
# CONTEXTO, database separa CLIENTE (ADR-0002 §2).
#
# ── O QUE FOI VERIFICADO LOCALMENTE, E O QUE NAO FOI ─────────────────────────
#
# Verificado em 2026-09-18, com Postgres 16 + PgBouncer 1.25 em containers, dois
# tenants provisionados pelos scripts de P2.1:
#
# 1. role de um tenant no database de outro -> "permission denied" ✔
# 2. role certo com a senha do vizinho -> "SASL authentication failed" ✔
# 3. role certo no proprio database -> conecta ✔
# 4. enumeracao de tenants pelo catalogo -> enxerga 1, o proprio ✔
# 5. pools do PgBouncer -> um por (database, role),
# SEM mistura de credencial ✔
#
# TUDO ISSO ATRAVES DO PGBOUNCER, que e o ponto: transaction pooling reaproveita
# conexao de servidor entre clientes, e um pool compartilhado entre roles faria
# um tenant herdar a sessao do outro.
#
# NAO verificado: comportamento sob carga real, e a migracao dos dados dos
# tenants que ja existem. Os dois dependem de deploy.
#
# ── O DIMENSIONAMENTO, QUE E CONTA E NAO CHUTE ───────────────────────────────
#
# Alvo do card: ~18 tenants.
#
# por tenant: 1 database, 1 role, 1 pool
# por pool: default_pool_size 10 + reserve_pool_size 5 = ate 15
# 18 tenants: 18 x 15 = 270 conexoes de servidor no pior caso
# + superusuario: 3 reservadas (superuser_reserved_connections)
# + folga: ~27
# -----------------------------------------------------------------
# max_connections: 300
#
# `shared_buffers` em 512Mi = 25% do limite de 2Gi, que e a regra de bolso do
# proprio Postgres. `effective_cache_size` em 1536Mi (75%) diz ao planejador
# quanta pagina ele pode esperar encontrar em cache — nao reserva memoria, so
# muda a escolha entre varredura sequencial e indice.
#
# `work_mem` em 4MB, e nao mais: ele e POR OPERACAO de ordenacao, nao por
# conexao. Com 300 conexoes e consultas com dois sorts, 16MB viraria 9,6 GB no
# pior caso — e o limite do pod e 2Gi.
# =============================================================================
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pg-compartilhado
namespace: plataforma-prod
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources:
requests:
storage: 50Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: pg-compartilhado-conf
namespace: plataforma-prod
data:
# Passados por `-c` no comando do postgres, e nao por arquivo: o entrypoint da
# imagem oficial reescreve o postgresql.conf na primeira subida, e um arquivo
# montado seria ignorado ou sobrescrito, sem aviso.
parametros: >-
-c max_connections=300
-c superuser_reserved_connections=3
-c shared_buffers=512MB
-c effective_cache_size=1536MB
-c work_mem=4MB
-c maintenance_work_mem=128MB
-c wal_compression=on
-c log_min_duration_statement=1000
-c log_connections=off
-c log_disconnections=off
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: pg-compartilhado
namespace: plataforma-prod
spec:
replicas: 1
strategy:
# `Recreate`, e nao RollingUpdate: sao dois processos disputando o MESMO
# PersistentVolume. O segundo nao sobe — o Postgres recusa iniciar sobre um
# diretorio ja travado — e o rollout fica preso com o pod novo em erro e o
# velho ainda de pe, ate alguem olhar.
type: Recreate
selector:
matchLabels: { app: pg-compartilhado }
template:
metadata:
labels: { app: pg-compartilhado }
spec:
containers:
- name: postgres
image: postgres:16
args: ["postgres"]
command: ["sh", "-c", "exec docker-entrypoint.sh postgres $PARAMETROS"]
env:
- name: PARAMETROS
valueFrom:
configMapKeyRef: { name: pg-compartilhado-conf, key: parametros }
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef: { name: pg-compartilhado-admin, key: password }
# `PGDATA` num subdiretorio: o local-path monta o volume na raiz, e o
# Postgres recusa inicializar num diretorio que ja tem `lost+found`.
- { name: PGDATA, value: /var/lib/postgresql/data/pgdata }
ports: [{ containerPort: 5432 }]
volumeMounts:
- { name: dados, mountPath: /var/lib/postgresql/data }
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { cpu: "2", memory: 2Gi }
readinessProbe:
exec: { command: ["pg_isready", "-U", "postgres"] }
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
exec: { command: ["pg_isready", "-U", "postgres"] }
initialDelaySeconds: 60
periodSeconds: 30
volumes:
- name: dados
persistentVolumeClaim: { claimName: pg-compartilhado }
---
apiVersion: v1
kind: Service
metadata:
name: pg-compartilhado
namespace: plataforma-prod
spec:
selector: { app: pg-compartilhado }
ports: [{ port: 5432, targetPort: 5432 }]