# ============================================================================= # 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_`, `r_`), 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 }]