PostgreSQL 16 fa temps que funciona a srv-tramontana, des del Mòdul 5. Està instal·lat, té el seu apt-mark hold i la seva fixació de versions perquè no salti de versió major sense decidir-ho, escolta a 10.0.2.15:5432 i ufw només ho permet des de 10.0.2.0/24. Tot això està ben fet.
I tanmateix la configuració és la que va portar el paquet, que està calculada per arrencar en qualsevol màquina, inclosa una amb 512 MB de RAM. A srv-tramontana significa que PostgreSQL fa servir 128 MB de memòria compartida dels 3,8 GB disponibles, que ordena en disc el que cabria en memòria, i que es pensa que el sistema té una memòria cau de disc molt menor de la que té — cosa que canvia els plans d'execució que tria.
A 08-01 vas deixar una pista concreta: el p99 de $upstream_response_time és d'1,18 s, i les dues rutes més lentes són informes. Quan el temps de l'aplicació puja i el de la xarxa no, la culpable gairebé mai no és l'aplicació: és una consulta. Aquesta lliçó va a buscar això, i el que hi ha a sota — perquè la base de dades és on viu el negoci real de Tramontana, i perdre-la és un problema d'una altra categoria que perdre el servidor web.
Contingut
- Objectiu, requisits previs i estat de partida
- Arquitectura de processos i fitxers de PostgreSQL
- Ajust de memòria amb fórmules raonades
- Pàgines enormes: la connexió amb 07-03
- Connexions: per què una agrupació i no un número més gran
- Autenticació: pg_hba.conf camp a camp
- TLS a les connexions
- Rols i permisos amb mínim privilegi
- Còpies: lògica, física, WAL i recuperació a un punt en el temps
- Manteniment: buidatge, wraparound i reindexació
- Diagnòstic: on se'n va el temps
- Rèplica en flux
- Automatització amb Ansible
- Operació diària
Objectiu, requisits previs i estat de partida
Objectiu. Deixar PostgreSQL ajustat a la màquina, amb accés restringit i xifrat, un rol d'aplicació sense privilegis innecessaris, còpies que permetin recuperar a un instant concret dins del RPO de 4 hores, manteniment automàtic verificat i diagnòstic instrumentat.
Requisits previs: PostgreSQL 16 instal·lat (05-03), LVM i /srv/tramontana/backups xifrat amb LUKS (05-04), restic amb retenció GFS (05-08), pass i systemd-creds (06-05), el tallafocs de 06-03, i Ansible operatiu (07-06).
Estat de partida, mesurat abans de tocar res:
$ psql --version
psql (PostgreSQL) 16.3 (Ubuntu 16.3-0ubuntu0.24.04.1)
$ sudo -u postgres psql -c "SELECT name, setting, unit, source FROM pg_settings
WHERE name IN ('shared_buffers','effective_cache_size','work_mem',
'maintenance_work_mem','max_connections','wal_buffers');"
name | setting | unit | source
----------------------+---------+------+----------
effective_cache_size | 524288 | 8kB | default
maintenance_work_mem | 65536 | kB | default
max_connections | 100 | | default
shared_buffers | 16384 | 8kB | default
wal_buffers | 512 | 8kB | default
work_mem | 4096 | kB | defaultTraduït: 128 MB de shared_buffers, 4 GB d'effective_cache_size (que casualment no està malament, però per accident), 4 MB de work_mem i source = default a tot, és a dir, ningú no ha ajustat mai res.
# Mida actual de la base de dades i de les seves taules mes grans
$ sudo -u postgres psql -d tramontana -c "
SELECT relname, pg_size_pretty(pg_total_relation_size(c.oid)) AS total,
n_live_tup AS files
FROM pg_class c JOIN pg_stat_user_tables s ON s.relid = c.oid
ORDER BY pg_total_relation_size(c.oid) DESC LIMIT 5;"
relname | total | files
-------------+---------+--------
reserves | 412 MB | 186420
disponibles | 168 MB | 891200
hostes | 54 MB | 92310
cases | 96 kB | 5
auditoria | 1204 MB | 512880Dues dades rellevants: la base de dades completa ronda els 1,8 GB, és a dir, hi cap sencera en memòria si es configura bé; i la taula auditoria és la més gran de totes, cosa que ja suggereix una política de retenció pendent.
Arquitectura de processos i fitxers de PostgreSQL
PostgreSQL fa servir un model de procés per connexió, no de fils. Això explica gairebé tot el seu comportament de memòria i és el que fa important l'apartat de l'agrupació de connexions.
$ ps -eo pid,ppid,user,comm --forest | grep -A9 'postgres$' | head -12
1121 1 postgres postgres
1210 1121 postgres \_ postgres: checkpointer
1211 1121 postgres \_ postgres: background writer
1213 1121 postgres \_ postgres: walwriter
1214 1121 postgres \_ postgres: autovacuum launcher
1215 1121 postgres \_ postgres: logical replication launcher
3402 1121 postgres \_ postgres: svc_tramontana tramontana 127.0.0.1(51422) idle
3403 1121 postgres \_ postgres: svc_tramontana tramontana 127.0.0.1(51424) SELECT| Procés | Què fa | Per què t'importa |
|---|---|---|
| postmaster | Procés pare; accepta connexions i llança backends | Si mor, tot cau |
| backend | Un per connexió; executa les consultes | Cadascun consumeix memòria pròpia: és el motiu de l'agrupació |
| checkpointer | Aboca pàgines brutes a disc periòdicament | Un checkpoint agressiu produeix pics d'E/S |
| background writer | Va escrivint pàgines brutes a poc a poc | Suavitza els pics del checkpointer |
| walwriter | Escriu el registre d'escriptura anticipada | És el que garanteix la durabilitat |
| autovacuum launcher | Llança treballadors de neteja | Sense ell, la base de dades es degrada sola |
I on viu cada cosa a Ubuntu, que segueix el disseny de Debian amb diversos clusters possibles:
$ sudo -u postgres psql -c "SHOW data_directory; SHOW config_file; SHOW hba_file;"
data_directory
---------------------------
/var/lib/postgresql/16/main
config_file
-------------------------------------------
/etc/postgresql/16/main/postgresql.conf
hba_file
-------------------------------------------
/etc/postgresql/16/main/pg_hba.conf
$ sudo ls -1 /var/lib/postgresql/16/main/ | head -8
base # les dades: un subdirectori per base de dades
global # cataleg compartit entre bases
pg_wal # el registre d escriptura anticipada
pg_stat # estadistiques del planificador
pg_tblspc # enllacos a tablespaces externs
postgresql.auto.conf # el que escriu ALTER SYSTEM
PG_VERSION
postmaster.pid| Ruta | Què és | Compte |
|---|---|---|
PGDATA = /var/lib/postgresql/16/main |
Totes les dades | Mai no es toca a mà amb el servidor arrencat |
pg_wal/ |
Registre d'escriptura anticipada | Si s'omple, PostgreSQL s'atura. No esborrar mai fitxers a mà |
/etc/postgresql/16/main/postgresql.conf |
Configuració | A Debian/Ubuntu viu fora de PGDATA |
postgresql.auto.conf |
El que escriu ALTER SYSTEM |
Té prioritat sobre postgresql.conf: font de confusió |
pg_hba.conf |
Qui es pot connectar i com | S'aplica amb reload, en ordre de dalt a baix |
Aquella fila de postgresql.auto.conf mereix un advertiment: si algú va executar alguna vegada ALTER SYSTEM SET work_mem = '64MB', aquell valor guanya encara que editis postgresql.conf. Quan un paràmetre no pren el valor que esperes, pg_settings.source et diu d'on ve.
Seguint la convenció de drop-ins del curs, la configuració pròpia no s'escriu editant el fitxer principal, sinó en un fitxer a part inclòs al final:
$ sudo cp /etc/postgresql/16/main/postgresql.conf \
/etc/postgresql/16/main/postgresql.conf.bak-$(date +%F)
$ echo "include_dir = 'conf.d'" | sudo tee -a /etc/postgresql/16/main/postgresql.conf
$ sudo -u postgres mkdir -p /etc/postgresql/16/main/conf.dAjust de memòria amb fórmules raonades
La màquina té 3,8 GB de RAM i 2 vCPU. Cal repartir aquella memòria entre PostgreSQL, l'aplicació Tramontana, Nginx i el sistema. Aquestes són les fórmules de partida —de partida, no dogmes— i el seu raonament.
| Paràmetre | Fórmula habitual | Valor per a 3,8 GB | Què controla |
|---|---|---|---|
shared_buffers |
25 % de la RAM | 960 MB | Memòria cau de pàgines pròpia de PostgreSQL |
effective_cache_size |
50-75 % de la RAM | 2560 MB | El que el planificador es pensa que hi ha a la memòria cau |
work_mem |
RAM ÷ (connexions × 3) | 8 MB | Memòria per operació d'ordenació o hash |
maintenance_work_mem |
5-10 % de la RAM | 256 MB | Per a VACUUM, CREATE INDEX, ALTER TABLE |
wal_buffers |
1/32 de shared_buffers, màx. 16 MB |
16 MB | Memòria intermèdia del WAL abans d'escriure'l |
I ara el raonament de cadascun, que és el que distingeix ajustar de copiar números:
shared_buffers = 960 MB (25 %). PostgreSQL manté la seva pròpia memòria cau de pàgines a més de la del sistema operatiu. Per això no es posa al 80 % com en altres motors: es produiria un doble emmagatzematge, amb les mateixes pàgines en dos llocs i menys memòria total útil. El 25 % és el punt d'equilibri contrastat empíricament. Amb 1,8 GB de dades, gairebé la meitat de la base de dades viurà permanentment aquí.
effective_cache_size = 2560 MB (67 %). Aquest paràmetre no reserva res: és una pista per al planificador sobre quanta memòria hi ha disponible entre shared_buffers i la memòria cau del sistema. Si el deixes baix, el planificador es pensa que llegir del disc és car i evita els índexs, preferint recorreguts seqüencials. És un dels ajustos amb major impacte per unitat d'esforç, i no costa ni un byte de memòria.
work_mem = 8 MB, i per què és el paràmetre perillós. És memòria per operació, no per connexió. Una consulta amb dues ordenacions i un hash join pot fer servir tres vegades work_mem. Amb 100 connexions i consultes de tres operacions:
És a dir, més de la meitat de la RAM del servidor, per sobre dels 960 MB de shared_buffers. Aquella aritmètica és la raó per la qual pujar work_mem alegrement provoca que l'OOM killer mati PostgreSQL — el mateix mecanisme que vas veure a 07-02. Amb PgBouncer limitant a 25 connexions reals, el pitjor cas baixa a 600 MB, que sí que és assumible. Primer l'agrupació, després work_mem.
I l'elegant: work_mem es pot pujar només per a la consulta que ho necessita, sense tocar el global.
-- A la sessio de l informe mensual, no a tot el servidor
BEGIN;
SET LOCAL work_mem = '128MB';
SELECT casa, sum(import) FROM reserves WHERE data >= '2026-01-01' GROUP BY casa;
COMMIT;maintenance_work_mem = 256 MB. Només el fan servir operacions de manteniment, i com a màxim autovacuum_max_workers alhora. Pujar-lo accelera enormement el VACUUM i la creació d'índexs, amb un risc molt menor que work_mem.
El fitxer complet:
# /etc/postgresql/16/main/conf.d/10-tramontana.conf
# Ajust per a srv-tramontana: 3,8 GB RAM, 2 vCPU, SSD, PostgreSQL 16
# Base de referencia presa el 2026-08-18. Mesurar abans i despres.
# ---------- Memoria ----------
shared_buffers = 960MB
effective_cache_size = 2560MB
work_mem = 8MB
maintenance_work_mem = 256MB
wal_buffers = 16MB
# ---------- Connexions ----------
# Es BAIXA de 100 a 60. Amb PgBouncer al davant, 60 sobren, i cada
# connexio reservada costa memoria encara que estigui ociosa.
max_connections = 60
superuser_reserved_connections = 3
# ---------- Registre d escriptura anticipada ----------
wal_level = replica # necessari per a replica i PITR
max_wal_size = 2GB # menys checkpoints, pics mes suaus
min_wal_size = 256MB
checkpoint_completion_target = 0.9 # reparteix l escriptura en el temps
archive_mode = on
archive_command = '/usr/local/bin/arxivar_wal.sh %p %f'
archive_timeout = 900s # forca un segment cada 15 min
# ---------- Planificador (SSD) ----------
# random_page_cost per defecte es 4.0, calibrat per a discos giratoris
# on un salt aleatori costava molt mes que una lectura sequencial.
# En SSD la diferencia es minima: 1.1 reflecteix la realitat i fa que el
# planificador faci servir indexs on toca.
random_page_cost = 1.1
effective_io_concurrency = 200 # el SSD aten moltes peticions alhora
# ---------- Paral·lelisme (2 vCPU: contencio) ----------
max_worker_processes = 2
max_parallel_workers = 2
max_parallel_workers_per_gather = 1
# ---------- Registre ----------
log_destination = 'stderr'
logging_collector = off # deixa que systemd/journald ho reculli
log_line_prefix = '%m [%p] %q%u@%d '
log_min_duration_statement = 250ms # registra les consultes lentes
log_checkpoints = on
log_connections = off # sorollos amb una agrupacio al davant
log_lock_waits = on # esperes de blocatge: simptoma important
log_temp_files = 0 # qualsevol fitxer temporal = work_mem curt
log_autovacuum_min_duration = 1s
# ---------- Estadistiques ----------
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.max = 5000
pg_stat_statements.track = top$ sudo -u postgres psql -c "SELECT pg_reload_conf();"
# shared_buffers i shared_preload_libraries requereixen REINICI
$ sudo systemctl restart postgresql@16-main
$ sudo -u postgres psql -c "SELECT name, setting, unit, source, pending_restart
FROM pg_settings WHERE name IN ('shared_buffers','work_mem','random_page_cost');"
name | setting | unit | source | pending_restart
-----------------+---------+------+-------------------------------------+-----------------
random_page_cost| 1.1 | | configuration file | f
shared_buffers | 122880 | 8kB | configuration file | f
work_mem | 8192 | kB | configuration file | fLa columna pending_restart és la que et diu si un canvi està aplicat o només escrit. source = configuration file confirma que el drop-in s'està llegint.
Mesurar l'efecte, que és la meitat de la feina:
# L index d encerts de la memoria cau compartida. Es mesura ABANS i
# DESPRES, deixant passar unes hores de transit real.
$ sudo -u postgres psql -d tramontana -c "
SELECT round(100.0 * sum(blks_hit) / nullif(sum(blks_hit + blks_read), 0), 2)
AS encert_pct
FROM pg_stat_database WHERE datname = 'tramontana';"
encert_pct
------------
99.42Abans de l'ajust aquell número era 91,80 %. Sembla una millora petita i no ho és: passar del 91,8 % al 99,4 % significa que de cada 100 accessos a pàgina, els que van al disc cauen de 8,2 a 0,6, és a dir, una reducció de gairebé el 93 % de les lectures físiques. Per sota del 95 % cal sospitar; per sota del 90 % hi ha un problema clar de memòria o de consultes que escombren taules senceres.
Pàgines enormes: la connexió amb 07-03
A 07-03 vas desactivar les pàgines enormes transparents (THP) o les vas deixar en madvise, i la lliçó va deixar anotat que PostgreSQL n'era una de les raons. Aquí tens el detall.
El problema no són les pàgines enormes en si, que són útils: amb pàgines de 2 MB en lloc de 4 KB, la TLB del processador cobreix 512 vegades més memòria i les fallades de traducció s'esfondren. El problema és el «transparent»: el nucli intenta fusionar pàgines i desfragmentar memòria de forma síncrona, dins del context del procés que demana memòria. Per a PostgreSQL, amb el seu shared_buffers de gairebé un gigabyte i els seus processos de vida curta, això produeix pauses impredictibles de desenes o centenars de mil·lisegons en consultes que haurien de trigar dos.
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never
$ cat /sys/kernel/mm/transparent_hugepage/defrag
always defer defer+madvise [madvise] nevermadvise és exactament la configuració correcta: les pàgines enormes només es fan servir on el programa les demana explícitament amb madvise(MADV_HUGEPAGE), i no s'imposen a tothom.
El millor de tots dos mons és fer servir pàgines enormes explícites per a shared_buffers:
# 1. Quantes en necessita PostgreSQL (arrenca, pregunta i surt)
$ sudo -u postgres /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/main \
-C shared_memory_size_in_huge_pages
495
# 2. Reservar-les amb marge, de forma persistent
$ echo 'vm.nr_hugepages = 520' | \
sudo tee /etc/sysctl.d/71-postgresql-hugepages.conf
$ sudo sysctl --system
# 3. Demanar a PostgreSQL que les faci servir
$ echo "huge_pages = try" | \
sudo tee -a /etc/postgresql/16/main/conf.d/10-tramontana.conf
$ sudo systemctl restart postgresql@16-main
# 4. Verificar
$ grep -E 'HugePages_Total|HugePages_Free' /proc/meminfo
HugePages_Total: 520
HugePages_Free: 25huge_pages = try i no on: amb on, si les pàgines reservades no basten, PostgreSQL no arrenca. Amb try arrenca igualment fent servir pàgines normals, que és el comportament que vols en un servidor que ha de tornar després d'un reinici sense supervisió.
I l'advertiment important: la memòria de vm.nr_hugepages queda reservada i no disponible per a res més. Reservar 520 pàgines de 2 MB són 1040 MB que la resta del sistema ja no veurà. Amb 3,8 GB cal ser precís, i per això el pas 1 no s'estima: es pregunta.
Connexions: per què una agrupació i no un número més gran
A 07-02 hi va haver un incident que convé recordar amb precisió: l'aplicació té max_connexions=80 a /etc/tramontana/app.conf i PostgreSQL tenia max_connections=100. Sota càrrega, l'aplicació obria les seves 80, més les connexions d'informe_reserves.sh, més les de manteniment, més les tres reservades per a superusuari... i apareixia FATAL: sorry, too many clients already.
La reacció instintiva —pujar max_connections a 300— és exactament l'equivocada, per tres motius mesurables:
| Cost de cada connexió | Detall |
|---|---|
| Memòria | Un procés backend ronda 5-10 MB de memòria pròpia, ociós o no |
work_mem potencial |
Cada connexió pot reclamar diversos work_mem alhora |
| Contenció | Més processos competint per 2 vCPU: més canvis de context, més contenció de blocatges lleugers |
Amb 2 vCPU, el nombre de consultes que es poden executar realment alhora és 2. Tres-centes connexions no donen més feina feta: donen més processos esperant, més memòria consumida i un rendiment que cau en pujar la concurrència. És el mateix fenomen de saturació del mètode USE de 05-07.
La regla de partida és connexions ≈ (2 × nuclis) + fusos de disc efectius, que aquí dona unes 5-10 connexions actives. En necessitem moltes més obertes perquè l'aplicació les manté ocioses entre peticions. Aquí és on entra l'agrupació de connexions (pool).
PgBouncer en mode transacció
| Mode | Quan torna la connexió a l'agrupació | Multiplexació | Restriccions |
|---|---|---|---|
session |
En desconnectar el client | Cap | Cap |
transaction |
En acabar cada transacció | Alta | Sense SET de sessió, sense sentències preparades de sessió, sense LISTEN |
statement |
Després de cada sentència | Màxima | Prohibeix les transaccions multisentència |
El mode transacció és el correcte per a una aplicació web: entre peticions, la connexió torna a l'agrupació i una altra petició l'aprofita. Cent clients d'aplicació poden compartir vint-i-cinc connexions reals perquè gairebé cap no està executant res en un instant donat.
Les restriccions cal verificar-les amb el Luis abans d'activar-lo, perquè són reals: en mode transacció, un SET search_path fora d'una transacció es perd, i les sentències preparades a nivell de sessió fallen llevat que s'activi max_prepared_statements.
; /etc/pgbouncer/pgbouncer.ini
[databases]
; L aplicacio es connecta a PgBouncer creient-se que es PostgreSQL
tramontana = host=127.0.0.1 port=5432 dbname=tramontana
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
unix_socket_dir = /var/run/postgresql
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
; Consulta d autenticacio delegada: evita duplicar contrasenyes
auth_user = pgbouncer_auth
pool_mode = transaction
; --- El dimensionament, que es la decisio central ---
; max_client_conn: quants clients d aplicacio acceptem (barats)
max_client_conn = 200
; default_pool_size: connexions REALS a PostgreSQL per parell usuari/bd.
; Amb 2 vCPU, 25 es folgat. Aquest es el numero que protegeix el servidor.
default_pool_size = 25
; Marge temporal per a pics, amb avis al registre
reserve_pool_size = 5
reserve_pool_timeout = 3
; Tancar connexions de servidor ocioses molt de temps
server_idle_timeout = 600
; Reciclar connexions cada hora: evita fuites de memoria acumulades
server_lifetime = 3600
; Si un client espera mes d aixo per una connexio, error clar
query_wait_timeout = 20
logfile = /var/log/postgresql/pgbouncer.log
pidfile = /var/run/postgresql/pgbouncer.pid
admin_users = operador
stats_users = operador, monitorEl canvi a l'aplicació és una línia de /etc/tramontana/app.conf — recordant que el fitxer té chattr +i i cal treure'l abans:
$ sudo chattr -i /etc/tramontana/app.conf
$ sudo cp /etc/tramontana/app.conf /etc/tramontana/app.conf.bak-$(date +%F)
$ sudo sed -i 's/^db_port=5432/db_port=6432/' /etc/tramontana/app.conf
$ sudo diff -u /etc/tramontana/app.conf.bak-$(date +%F) /etc/tramontana/app.conf
--- /etc/tramontana/app.conf.bak-2026-08-18
+++ /etc/tramontana/app.conf
@@ -2,7 +2,7 @@
db_host=127.0.0.1
-db_port=5432
+db_port=6432
db_name=tramontana
$ sudo chattr +i /etc/tramontana/app.conf
$ sudo systemctl restart tramontanaI la comprovació que el problema de 07-02 està resolt:
$ psql -h 127.0.0.1 -p 6432 -U operador -d pgbouncer -c "SHOW POOLS;"
database | user | cl_active | cl_waiting | sv_active | sv_idle | maxwait
-------------+---------------+-----------+------------+-----------+---------+---------
tramontana | svc_tramontana| 63 | 0 | 4 | 21 | 0Seixanta-tres clients d'aplicació connectats, quatre connexions reals treballant i cap esperant. maxwait = 0 és la mètrica clau: així que sigui més gran que zero de forma sostinguda, l'agrupació s'està quedant curta.
| Abans | Després |
|---|---|
| 80 connexions directes, memòria de 80 processos | 25 com a màxim |
too many clients already sota càrrega |
maxwait visible i controlat |
max_connections = 100 |
max_connections = 60, amb folgança real |
Autenticació: pg_hba.conf camp a camp
pg_hba.conf (host-based authentication) decideix qui es pot connectar, a què, des d'on i com. S'avalua de dalt a baix i guanya la primera línia que coincideix, cosa que significa que una línia permissiva a dalt anul·la totes les restrictives de sota.
| Camp | Valors | Notes |
|---|---|---|
TIPUS |
local, host, hostssl, hostnossl |
local = sòcol Unix; hostssl exigeix TLS |
BASE_DE_DADES |
nom, all, replication |
replication és una pseudobase per a rèpliques |
USUARI |
nom, all, +grup |
+ significa «membre del rol» |
ADREÇA |
CIDR, samenet, buit a local |
Com més estret, millor |
MÈTODE |
scram-sha-256, peer, cert, trust, reject |
Veure taula següent |
| Mètode | Com autentica | Veredicte |
|---|---|---|
scram-sha-256 |
Repte-resposta; la contrasenya no viatja | El correcte per a connexions de xarxa |
md5 |
Obsolet i feble | Migrar a SCRAM |
peer |
Comprova l'usuari Unix del sòcol local | Ideal per a tasques locals de manteniment |
ident |
Consulta a un servidor ident remot | No fer-lo servir: confia en la màquina remota |
cert |
Certificat de client TLS | Excel·lent per a servei a servei |
trust |
Accepta qualsevol sense comprovar res | Mai, veure a sota |
reject |
Denega explícitament | Útil per tallar abans d'una regla general |
Per què trust mai, ni tan sols «temporalment» i ni tan sols a 127.0.0.1: significa que qualsevol procés que pugui obrir un sòcol al port entra com l'usuari que declari ser, inclòs postgres. En un servidor on corre una aplicació web, n'hi ha prou amb una vulnerabilitat de tipus SSRF —aconseguir que l'aplicació faci una petició a 127.0.0.1:5432— per tenir control total de la base de dades. I el que es posa «temporalment» un dimarts continua allà dos anys després.
# /etc/postgresql/16/main/pg_hba.conf
# L ordre IMPORTA: guanya la primera coincidencia.
# --- 1. Manteniment local per socol Unix ---
# 'peer' compara l usuari del sistema amb el rol demanat: 'sudo -u postgres
# psql' funciona sense contrasenya, i ningu mes no pot fer-se passar per ell.
local all postgres peer
local all all peer
# --- 2. PgBouncer i l aplicacio, des del bucle local ---
# Nomes el rol d aplicacio, nomes la seva base de dades, amb SCRAM.
host tramontana svc_tramontana 127.0.0.1/32 scram-sha-256
host tramontana pgbouncer_auth 127.0.0.1/32 scram-sha-256
# --- 3. Monitoracio (08-06), nomes lectura d estadistiques ---
host postgres monitor 127.0.0.1/32 scram-sha-256
# --- 4. Replicacio (apartat 12), amb TLS OBLIGATORI ---
# 'hostssl' rebutja la connexio si no va xifrada; el WAL porta
# les dades completes i no pot viatjar en clar.
hostssl replication replicador 10.0.2.16/32 scram-sha-256
# --- 5. Acces administratiu des de la xarxa interna, xifrat ---
hostssl tramontana operador 10.0.2.0/24 scram-sha-256
# --- 6. Denegacio explicita de tota la resta ---
# Redundant (el comportament per defecte ja es denegar) pero deixa
# constancia de la intencio i produeix un missatge clar al registre.
host all all 0.0.0.0/0 reject
host all all ::/0 reject$ sudo -u postgres psql -c "SELECT pg_reload_conf();"
$ sudo -u postgres psql -c "SELECT line_number, type, database, user_name,
address, auth_method, error FROM pg_hba_file_rules WHERE error IS NOT NULL;"
(0 rows)pg_hba_file_rules és una vista que valida el fitxer sense recarregar-lo: et diu si hi ha línies amb errors abans que trenquin l'accés. És l'equivalent de nginx -t per a pg_hba.conf, i cal fer-la servir sempre — «no tanquis mai la porta per la qual estàs entrant» també s'aplica aquí.
I la verificació que les regles fan el que et penses:
# Ha de funcionar
$ PGPASSWORD=$(pass tramontana/db) psql -h 127.0.0.1 -p 5432 \
-U svc_tramontana -d tramontana -c 'SELECT 1;' >/dev/null && echo OK
OK
# NO ha de funcionar: rol d aplicacio contra una altra base de dades
$ PGPASSWORD=$(pass tramontana/db) psql -h 127.0.0.1 -U svc_tramontana \
-d postgres -c 'SELECT 1;'
psql: error: FATAL: no pg_hba.conf entry for host "127.0.0.1", user
"svc_tramontana", database "postgres", no encryptionTLS a les connexions
Amb l'aplicació i PgBouncer a la mateixa màquina, el trànsit va pel bucle local i el xifratge aporta poc. Però la rèplica de 10.0.2.16 i els accessos administratius des de la xarxa interna sí que ho necessiten: el WAL conté totes les dades en clar.
# Ubuntu genera un certificat autosignat i l enllaca en instal·lar
$ sudo ls -l /var/lib/postgresql/16/main/server.crt
lrwxrwxrwx 1 postgres postgres 36 -> /etc/ssl/certs/ssl-cert-snakeoil.pemPer a trànsit intern entre servidors propis, el correcte no és un certificat de Let's Encrypt —que exigeix un nom públic i validació externa— sinó una CA interna amb les eines d'openssl de 06-05:
# CA interna (es genera una vegada, en una maquina segura, NO al servidor)
$ openssl req -new -x509 -days 3650 -nodes -out ca-tramontana.crt \
-keyout ca-tramontana.key -subj "/CN=CA interna Tramontana"
# Certificat de servidor per a la base de dades
$ openssl req -new -nodes -out bd.csr -keyout bd.key \
-subj "/CN=srv-tramontana.intern"
$ openssl x509 -req -in bd.csr -days 825 -CA ca-tramontana.crt \
-CAkey ca-tramontana.key -CAcreateserial -out bd.crt
$ sudo install -o postgres -g postgres -m 0600 bd.key /etc/postgresql/16/main/
$ sudo install -o postgres -g postgres -m 0644 bd.crt /etc/postgresql/16/main/# conf.d/20-tls.conf
ssl = on
ssl_cert_file = '/etc/postgresql/16/main/bd.crt'
ssl_key_file = '/etc/postgresql/16/main/bd.key'
ssl_ca_file = '/etc/postgresql/16/main/ca-tramontana.crt'
ssl_min_protocol_version = 'TLSv1.2'
ssl_prefer_server_ciphers = on$ sudo -u postgres psql -c "SELECT pid, ssl, version, cipher, client_addr
FROM pg_stat_ssl JOIN pg_stat_activity USING (pid) WHERE ssl;"
pid | ssl | version | cipher | client_addr
------+-----+---------+------------------------+-------------
8812 | t | TLSv1.3 | TLS_AES_256_GCM_SHA384 | 10.0.2.16El detall que la majoria de la gent passa per alt: al client, sslmode=require xifra però no verifica el certificat, així que no protegeix d'un intermediari. Només verify-full comprova la cadena i el nom.
sslmode |
Xifra | Verifica CA | Verifica nom |
|---|---|---|---|
disable |
No | — | — |
require |
Sí | No | No |
verify-ca |
Sí | Sí | No |
verify-full |
Sí | Sí | Sí |
Rols i permisos amb mínim privilegi
És el mateix principi de 05-01 i 05-02, aplicat dins de la base de dades. Un rol d'aplicació no ha de poder crear taules, alterar l'esquema, llegir altres bases de dades ni, per descomptat, ser SUPERUSER.
-- Executat com a postgres: sudo -u postgres psql -d tramontana
-- 1. Rol d aplicacio: nomes es pot connectar i treballar amb dades
CREATE ROLE svc_tramontana WITH LOGIN
PASSWORD 's-injecta-des-de-pass'
NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT
CONNECTION LIMIT 30;
-- 2. Tancar l esquema public, que a PostgreSQL 15+ ja ve restringit,
-- pero conve ser explicit. Abans de la 15, QUALSEVOL usuari podia
-- crear objectes a 'public'.
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON DATABASE tramontana FROM PUBLIC;
-- 3. Esquema propi de l aplicacio
CREATE SCHEMA IF NOT EXISTS app AUTHORIZATION postgres;
-- 4. Permisos acotats: connectar-se, fer servir l esquema, i DML sobre
-- les taules existents. NO CREATE: no pot alterar l esquema.
GRANT CONNECT ON DATABASE tramontana TO svc_tramontana;
GRANT USAGE ON SCHEMA app TO svc_tramontana;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app
TO svc_tramontana;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA app TO svc_tramontana;
-- 5. I per a les taules FUTURES, que es el que gairebe tothom oblida:
-- sense aixo, la taula que crei la proxima migracio sera inaccessible
-- per a l aplicacio i el desplegament fallara en produccio.
ALTER DEFAULT PRIVILEGES IN SCHEMA app
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO svc_tramontana;
ALTER DEFAULT PRIVILEGES IN SCHEMA app
GRANT USAGE ON SEQUENCES TO svc_tramontana;
-- 6. Rol de nomes lectura per a informes (informe_reserves.sh) i per a
-- la monitoracio de 08-06
CREATE ROLE lector_informes WITH LOGIN PASSWORD 'una-altra-de-diferent'
NOSUPERUSER NOCREATEDB NOCREATEROLE CONNECTION LIMIT 5;
GRANT CONNECT ON DATABASE tramontana TO lector_informes;
GRANT USAGE ON SCHEMA app TO lector_informes;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO lector_informes;
ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT SELECT ON TABLES TO lector_informes;
-- 7. Rol de monitoracio: rol predefinit, sense ser superusuari
CREATE ROLE monitor WITH LOGIN PASSWORD 'una-altra-mes';
GRANT pg_monitor TO monitor;
-- 8. Rol de replicacio (apartat 12)
CREATE ROLE replicador WITH LOGIN REPLICATION PASSWORD 'i-una-altra';Verificar és tan important com concedir — comprovar que el prohibit està prohibit:
$ PGPASSWORD=$(pass tramontana/db) psql -h 127.0.0.1 -U svc_tramontana \
-d tramontana -c "CREATE TABLE prova(id int);"
ERROR: permission denied for schema app
$ PGPASSWORD=$(pass tramontana/db) psql -h 127.0.0.1 -U svc_tramontana \
-d tramontana -c "SELECT * FROM pg_shadow;"
ERROR: permission denied for table pg_shadow
$ sudo -u postgres psql -c "\du" | grep -E 'svc_tramontana|lector'
lector_informes | 5 connections | {}
svc_tramontana | No inheritance, 30 connections | {}La contrasenya, per descomptat, no s'escriu al SQL: s'injecta des de pass com a 06-05.
$ sudo -u postgres psql -d tramontana <<SQL
ALTER ROLE svc_tramontana PASSWORD '$(pass tramontana/db)';
SQL
# I s esborra de l historial de psql, que guarda TOT el que s ha teclejat
$ shred -u ~/.psql_history 2>/dev/null || trueCòpies: lògica, física, WAL i recuperació a un punt en el temps
copia_tramontana.sh fa avui un pg_dump. És correcte i no és suficient, i aquí tens per què.
pg_dump (lògica) |
pg_basebackup (física) |
|
|---|---|---|
| Què copia | Sentències SQL per reconstruir | Els fitxers de PGDATA byte a byte |
| Portable entre versions majors | Sí | No |
| Restauració selectiva d'una taula | Sí | No, és tot o res |
| Temps de restauració d'1,8 GB | ~10 min (reconstrueix índexs) | ~2 min (copia fitxers) |
| Permet PITR | No | Sí, amb arxivat de WAL |
| Granularitat del punt de recuperació | L'instant de l'abocament | Qualsevol instant |
| Cost al servidor | Alt: llegeix i serialitza tot | Moderat: E/S seqüencial |
Totes dues són necessàries, i no són alternatives: la lògica et salva d'una migració de versió major i permet recuperar una taula concreta; la física amb WAL és l'única que compleix un RPO de 4 hores sense fer abocaments cada quatre hores.
L'arxivat de WAL
El registre d'escriptura anticipada conté tots els canvis, en ordre. Si guardes una còpia física i tots els segments de WAL des d'aleshores, pots reproduir la història fins a qualsevol instant.
#!/usr/bin/env bash
#
# /usr/local/bin/arxivar_wal.sh - Arxiva un segment de WAL
#
# PostgreSQL l invoca com: arxivar_wal.sh %p %f
# %p = ruta relativa al segment; %f = nomes el nom
#
# CONTRACTE CRITIC:
# - Ha de retornar 0 NOMES si el segment esta segur i verificat.
# - Si retorna != 0, PostgreSQL HO REINTENTA indefinidament i NO esborra
# el segment. Es el correcte: millor omplir pg_wal que perdre dades.
# - MAI no ha de sobreescriure un fitxer existent amb contingut diferent.
#
set -euo pipefail
readonly ORIGEN="$1"
readonly NOM="$2"
readonly DESTI="/srv/tramontana/backups/wal"
umask 077
# Ja arxivat i identic: exit idempotent (07-06)
if [[ -f "${DESTI}/${NOM}" ]]; then
if cmp -s "$ORIGEN" "${DESTI}/${NOM}"; then
exit 0
fi
logger -t arxivar_wal "ERROR: ${NOM} ja existeix amb contingut DIFERENT"
exit 1
fi
# Copia atomica: escriure a temporal i reanomenar. Si el proces mor a
# mitges, no queda un segment truncat que sembli valid.
tmp="${DESTI}/.${NOM}.$$"
trap 'rm -f "$tmp"' EXIT
cp "$ORIGEN" "$tmp"
sync -f "$tmp" # forcar a disc ABANS de reanomenar
mv "$tmp" "${DESTI}/${NOM}"
sync "$DESTI"
exit 0$ sudo install -o root -g root -m 0755 arxivar_wal.sh /usr/local/bin/
$ sudo -u postgres mkdir -p /srv/tramontana/backups/wal
$ sudo -u postgres psql -c "SELECT pg_switch_wal();" # forcar un segment
$ sudo -u postgres psql -c "SELECT archived_count, last_archived_wal,
last_archived_time, failed_count, last_failed_wal FROM pg_stat_archiver;"
archived_count | last_archived_wal | last_archived_time | failed_count
----------------+--------------------------+-------------------------------+--------------
47 | 000000010000000000000031 | 2026-08-18 12:41:08.221+02 | 0failed_count és la mètrica que cal vigilar sense descans: si l'arxivat falla, pg_wal creix fins a omplir el disc i PostgreSQL s'atura. Va directa a la monitoració de 08-06.
La còpia física
$ sudo -u postgres pg_basebackup \
-h /var/run/postgresql -U postgres \
-D /srv/tramontana/backups/base/$(date +%F) \
-Ft -z -Xs -P -c fast --manifest-checksums=SHA256
1843712/1843712 kB (100%), 1/1 tablespace| Opció | Què fa |
|---|---|
-Ft -z |
Format tar comprimit: un fitxer per tablespace |
-Xs |
Inclou el WAL generat durant la còpia (flux) |
-c fast |
Checkpoint immediat: comença ja, amb un pic d'E/S |
--manifest-checksums |
Manifest verificable amb pg_verifybackup |
$ sudo -u postgres pg_verifybackup /srv/tramontana/backups/base/2026-08-18
backup successfully verifiedI la integració amb restic de 05-08, que és el que la porta fora del servidor:
# Fragment afegit a copia_tramontana.sh
copiar_postgresql() {
local desti="${TRAMONTANA_BACKUP_DIR}/base/$(date +%F)"
log "iniciant copia base de PostgreSQL"
sudo -u postgres pg_basebackup -h /var/run/postgresql -U postgres \
-D "$desti" -Ft -z -Xs -c fast --manifest-checksums=SHA256 \
|| morir 74 "pg_basebackup ha fallat"
sudo -u postgres pg_verifybackup "$desti" \
|| morir 65 "la copia base NO verifica: no es puja"
log "copia base verificada: $(formatar_bytes "$(du -sb "$desti" | cut -f1)")"
restic backup "$desti" "${TRAMONTANA_BACKUP_DIR}/wal" \
--tag postgresql --tag base \
|| morir 74 "restic ha fallat en pujar la copia"
}El pg_verifybackup abans de pujar és deliberat: pujar una còpia corrupta consumeix espai i, pitjor encara, dona una falsa sensació de seguretat. La regla de 05-08 es compleix aquí: una còpia no verificada no és una còpia.
Recuperació a un punt en el temps, amb el procediment complet
Aquest és el procediment que cal tenir escrit abans de necessitar-lo, i provat. L'escenari: a les 14:32 algú executa DELETE FROM app.reserves WHERE data < '2026-08-01' sense el WHERE correcte i esborra 40.000 files. Es detecta a les 14:51.
# ===== ES FA A srv-tramontana-proves, MAI sobre produccio =====
# Restaurar sobre el servidor viu destrueix l unica copia de les dades
# posteriors a l incident. Es restaura a part i s extreu el que s ha perdut.
# 1. Aturar PostgreSQL a la maquina de recuperacio i buidar PGDATA
$ sudo systemctl stop postgresql@16-main
$ sudo -u postgres mv /var/lib/postgresql/16/main \
/var/lib/postgresql/16/main.trencat-$(date +%F)
$ sudo -u postgres mkdir -m 0700 /var/lib/postgresql/16/main
# 2. Descomprimir l ultima copia base ANTERIOR a l incident
$ sudo -u postgres tar -xzf /srv/tramontana/backups/base/2026-08-18/base.tar.gz \
-C /var/lib/postgresql/16/main
# 3. Dir a PostgreSQL d on treu el WAL i fins on reproduir
$ sudo -u postgres tee /var/lib/postgresql/16/main/postgresql.auto.conf <<'EOF'
restore_command = 'cp /srv/tramontana/backups/wal/%f %p'
# Reproduir fins JUST ABANS del DELETE. Un segon de marge.
recovery_target_time = '2026-08-18 14:31:55+02'
recovery_target_action = 'pause'
EOF
# 'pause' i no 'promote': el servidor s atura a l instant objectiu i
# espera. Aixi pots MIRAR les dades abans de confirmar. Si t has passat,
# reinicies amb un altre objectiu sense haver destruit res.
# 4. El senyal que activa el mode recuperacio (PostgreSQL >= 12)
$ sudo -u postgres touch /var/lib/postgresql/16/main/recovery.signal
# 5. Arrencar i observar
$ sudo systemctl start postgresql@16-main
$ sudo journalctl -u postgresql@16-main -f
LOG: starting point-in-time recovery to 2026-08-18 14:31:55+02
LOG: restored log file "000000010000000000000031" from archive
LOG: restored log file "000000010000000000000032" from archive
LOG: recovery stopping before commit of transaction 84412, time 2026-08-18 14:32:04+02
LOG: pausing at the end of recovery
HINT: Execute pg_wal_replay_resume() to promote.
# 6. VERIFICAR abans de confirmar res
$ sudo -u postgres psql -d tramontana -c \
"SELECT count(*) FROM app.reserves WHERE data < '2026-08-01';"
count
-------
40218
# 7. Confirmar: promoure el servidor recuperat
$ sudo -u postgres psql -c "SELECT pg_wal_replay_resume();"
$ sudo -u postgres psql -c "SELECT pg_is_in_recovery();"
pg_is_in_recovery
-------------------
f
# 8. Extreure NOMES el que s ha perdut i portar-ho a produccio
$ sudo -u postgres pg_dump -d tramontana -t app.reserves \
--data-only --where="data < '2026-08-01'" > /tmp/recuperades.sql
$ scp /tmp/recuperades.sql [email protected]:/tmp/
$ ssh [email protected] \
"sudo -u postgres psql -d tramontana -1 -f /tmp/recuperades.sql"Cinc decisions d'aquell procediment que cal entendre:
- Es recupera en una altra màquina. Restaurar a sobre de producció destrueix les transaccions posteriors a l'incident, que són legítimes i no són en cap altra banda.
recovery_target_action = 'pause'. Permet inspeccionar abans de comprometre's. Ambpromote, si t'has passat d'instant, cal començar de zero.- L'objectiu es fixa un segon abans, no a l'instant exacte: la marca temporal de l'incident poques vegades es coneix amb precisió de mil·lisegons.
- S'extreu només el que s'ha perdut. Substituir la base sencera perdria 19 minuts de reserves reals.
psql -1embolcalla la importació en una transacció: o entra tot o no entra res.
I el resultat que va al manual d'operació, amb números:
| Mètrica | Valor mesurat |
|---|---|
| RPO real aconseguit | Segons (amb archive_timeout=900s, pitjor cas 15 min) |
| Temps de restauració d'1,8 GB | 4 min 20 s |
| Temps total del procediment amb verificació | ~25 min |
| RTO acordat | 2 h |
El RPO passa de 4 hores a minuts sense cost addicional, i això és una notícia que mereix un paràgraf a l'informe per a la Marta.
Manteniment: buidatge, wraparound i reindexació
PostgreSQL fa servir control de concurrència multiversió (MVCC): un UPDATE no modifica la fila, escriu una versió nova i marca la vella com a morta. Un DELETE només marca. Les versions mortes continuen ocupant espai fins que algú les neteja.
$ sudo -u postgres psql -d tramontana -c "
SELECT relname, n_live_tup AS vives, n_dead_tup AS mortes,
round(100.0*n_dead_tup/nullif(n_live_tup+n_dead_tup,0),1) AS pct_mortes,
last_autovacuum
FROM pg_stat_user_tables WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;"
relname | vives | mortes | pct_mortes | last_autovacuum
-------------+--------+--------+------------+-------------------------------
disponibles | 891200 | 412880 | 31.7 | 2026-08-16 03:12:41+02
reserves | 186420 | 18122 | 8.9 | 2026-08-18 04:02:11+02Un 31,7 % de tuples mortes a disponibles és inflament (bloat): un terç d'aquella taula és brossa que es llegeix a cada recorregut seqüencial, ocupa shared_buffers i fa que els índexs apuntin a pàgines gairebé buides. I el last_autovacuum de fa dos dies indica que autovacuum no està seguint el ritme d'aquella taula.
| Operació | Què fa | Bloqueja |
|---|---|---|
VACUUM |
Marca l'espai mort com a reutilitzable | No: conviu amb la càrrega |
VACUUM FULL |
Reescriu la taula i retorna espai al SO | Sí, ACCESS EXCLUSIVE: ningú no llegeix ni escriu |
ANALYZE |
Recalcula les estadístiques del planificador | No |
REINDEX |
Reconstrueix índexs inflats | Sí, llevat de CONCURRENTLY |
VACUUM FULL en producció és un error clàssic: sobre una taula de 400 MB triga minuts, durant els quals l'aplicació no la pot tocar. Si cal recuperar espai en calent, l'eina és pg_repack.
Ajustar autovacuum on cal
Els valors per defecte disparen la neteja quan les tuples mortes superen el 20 % de la taula. En una taula de 900.000 files això són 180.000 tuples mortes abans de moure un dit, i una neteja cara cada vegada.
-- Ajust PER TAULA, que es com es fa: no es canvia el global per una
-- taula problematica.
ALTER TABLE app.disponibles SET (
autovacuum_vacuum_scale_factor = 0.02, -- netejar al 2 %, no al 20 %
autovacuum_vacuum_threshold = 1000,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_cost_delay = 2 -- mes agressiu
);
-- La taula d auditoria nomes creix: no necessita el mateix tractament,
-- necessita una politica de retencio.
ALTER TABLE app.auditoria SET (autovacuum_vacuum_scale_factor = 0.1);# conf.d/30-manteniment.conf — ajustos globals
autovacuum_max_workers = 2 # amb 2 vCPU, no mes
autovacuum_naptime = 30s
autovacuum_vacuum_cost_limit = 1000 # per defecte 200: massa lentEl wraparound, i per què és una emergència
Aquesta és la fallada més greu que pot patir una base de dades PostgreSQL mal mantinguda, i la que menys gent coneix fins que la pateix.
Cada transacció rep un identificador de 32 bits. Són uns 4.000 milions, i s'esgoten. PostgreSQL resol la circularitat tractant els identificadors com un cercle on 2.000 milions queden «al passat» i 2.000 milions «al futur». Perquè allò funcioni, les files molt antigues s'han de marcar com a congelades (frozen): visibles per a tothom, sense importar el comptador. D'això se n'encarrega VACUUM.
Si VACUUM no arriba a fer-ho —perquè autovacuum està desactivat, o perquè una transacció oberta des de fa dies el bloqueja— PostgreSQL avisa, després avisa més fort, i finalment es nega a acceptar escriptures:
ERROR: database is not accepting commands to avoid wraparound data loss in database "tramontana" HINT: Stop the postmaster and vacuum that database in single-user mode.
La base de dades queda de només lectura i l'única sortida és aturar el servei i netejar en mode monousuari, cosa que en una base gran pot durar hores. És una caiguda total, i per això es vigila:
$ sudo -u postgres psql -c "
SELECT datname, age(datfrozenxid) AS edat_xid,
round(100.0*age(datfrozenxid)/2000000000, 1) AS pct_cap_al_limit
FROM pg_database ORDER BY age(datfrozenxid) DESC LIMIT 3;"
datname | edat_xid | pct_cap_al_limit
------------+----------+------------------
tramontana | 48212104 | 2.4
postgres | 12088311 | 0.6Un 2,4 % és una situació perfectament sana. La regla operativa:
pct_cap_al_limit |
Situació | Acció |
|---|---|---|
| < 25 % | Normal | Cap |
| 25-50 % | Vigilar | Revisar autovacuum i transaccions llargues |
| 50-75 % | Avís | VACUUM FREEZE manual planificat |
| > 75 % | Crític | Intervenir ja |
Les dues causes gairebé sempre són les mateixes, i totes dues es detecten en una consulta:
$ sudo -u postgres psql -c "
SELECT pid, state, age(backend_xid) AS edat,
now()-xact_start AS duracio, left(query,50) AS consulta
FROM pg_stat_activity
WHERE backend_xid IS NOT NULL ORDER BY age(backend_xid) DESC LIMIT 3;"
pid | state | edat | duracio | consulta
------+---------------------+---------+-----------------+---------------------------
4471 | idle in transaction | 8812044 | 3 days 04:12:09 | BEGIN; SELECT * FROM app...idle in transaction durant tres dies és l'enemic: una transacció oberta impedeix congelar tot el que ve després i bloqueja la neteja de tota la base de dades. Sol ser una aplicació que va obrir una transacció i no la va tancar. La defensa és preventiva:
# conf.d/30-manteniment.conf
idle_in_transaction_session_timeout = 300s # matar despres de 5 min ociosa
statement_timeout = 60s # cap consulta mes d 1 min
lock_timeout = 10s # no esperar blocatges eternamentstatement_timeout = 60s és global; els informes que legítimament triguen més el pugen a la seva sessió amb SET LOCAL statement_timeout.
Diagnòstic: on se'n va el temps
Reprenem el fil de 08-01: el p99 de $upstream_response_time era 1,18 s i les rutes lentes eren informes.
pg_stat_statements: la consulta que més temps consumeix
$ sudo -u postgres psql -d tramontana -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"
$ sudo -u postgres psql -d tramontana -c "
SELECT round(total_exec_time::numeric,0) AS ms_total, calls,
round(mean_exec_time::numeric,1) AS ms_mitjana,
round(100.0*shared_blks_hit/nullif(shared_blks_hit+shared_blks_read,0),1) AS encert,
left(query, 60) AS consulta
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;"
ms_total | calls | ms_mitjana | encert | consulta
-----------+-------+------------+--------+--------------------------------------------
4128200 | 1088 | 3794.3 | 41.2 | SELECT casa, sum(import) FROM app.reserves
881400 | 92104 | 9.6 | 99.8 | SELECT * FROM app.disponibles WHERE casa =
412900 | 1204 | 342.9 | 98.1 | SELECT * FROM app.reserves WHERE hoste_idLa primera fila és la culpable, i la columna encert de 41,2 % ho confirma: aquella consulta llegeix del disc més de la meitat del que necessita. Ordena per temps total, no per temps mitjà: una consulta de 10 ms executada 92.000 vegades consumeix més servidor que una de 4 segons executada 1.000, i és un error comú optimitzar la lenta i espectacular en lloc de la freqüent.
EXPLAIN (ANALYZE, BUFFERS)
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT casa, sum(import) FROM app.reserves
WHERE data >= '2026-01-01' GROUP BY casa; HashAggregate (cost=48122.11..48122.16 rows=5 width=40)
(actual time=3781.442..3781.449 rows=5 loops=1)
Group Key: casa
Buffers: shared hit=812 read=41208
-> Seq Scan on reserves (cost=0.00..47188.20 rows=186782 width=18)
(actual time=0.031..3402.118 rows=186420 loops=1)
Filter: (data >= '2026-01-01'::date)
Rows Removed by Filter: 0
Buffers: shared hit=812 read=41208
Planning Time: 0.184 ms
Execution Time: 3781.512 msCom es llegeix això, que és una habilitat que s'entrena:
| Element | Què diu | Aquí |
|---|---|---|
cost= |
Estimació del planificador | 48122 unitats arbitràries |
actual time= |
Temps real: primera fila..última | 3,78 s |
rows= estimat vs rows= actual |
Si difereixen molt, falten estadístiques | 186782 vs 186420: bé |
Buffers: shared hit / read |
Pàgines de memòria cau / de disc | 41208 de disc: el problema |
Seq Scan |
Recorregut seqüencial complet | La taula sencera per no filtrar res |
Rows Removed by Filter: 0 |
El filtre no descarta res | El WHERE és inútil aquí |
Diagnòstic: el WHERE data >= '2026-01-01' no descarta cap fila perquè totes les reserves són posteriors. La consulta llegeix 41.208 pàgines de disc —322 MB— per agregar cinc grups. I sum(import) obliga a llegir les files completes, així que un índex sobre data no ajudaria: el planificador continuaria preferint el recorregut seqüencial.
La solució correcta aquí no és un índex, sinó un índex que cobreixi la consulta sencera:
-- Index de cobertura: conte tot el que la consulta necessita, aixi
-- que es respon SENSE tocar la taula (Index Only Scan).
CREATE INDEX CONCURRENTLY idx_reserves_data_casa_import
ON app.reserves (data) INCLUDE (casa, import);
ANALYZE app.reserves; HashAggregate (actual time=182.401..182.409 rows=5 loops=1)
Group Key: casa
Buffers: shared hit=1204 read=3811
-> Index Only Scan using idx_reserves_data_casa_import on reserves
(actual time=0.048..96.221 rows=186420 loops=1)
Index Cond: (data >= '2026-01-01'::date)
Heap Fetches: 0
Buffers: shared hit=1204 read=3811
Execution Time: 182.478 msDe 3.781 ms a 182 ms: vint vegades més ràpid, i les pàgines llegides de disc cauen de 41.208 a 3.811. Heap Fetches: 0 confirma que no es toca la taula en absolut.
CREATE INDEX CONCURRENTLY és obligatori en producció: la versió normal bloqueja les escriptures de la taula mentre construeix. CONCURRENTLY triga més i no bloqueja, a canvi que si falla deixa un índex invàlid que cal eliminar.
Consultes lentes al registre
Amb log_min_duration_statement = 250ms, les lentes queden al journal:
$ sudo journalctl -u postgresql@16-main --since today | grep -oP 'duration: \K[0-9.]+' | \
sort -rn | head -3
3812.402
1204.118
890.331
$ sudo journalctl -u postgresql@16-main --since today | grep 'temporary file'
LOG: temporary file: path "base/pgsql_tmp/pgsql_tmp4471.0", size 18874368
STATEMENT: SELECT ... ORDER BY data DESCAquell missatge de fitxer temporal és or: significa que una ordenació no va cabre a work_mem i es va fer en disc. 18 MB amb work_mem = 8 MB. La resposta és pujar work_mem només en aquella sessió, no globalment.
Índexs que falten i que sobren
# Taules amb molts recorreguts sequencials sobre volum gran
$ sudo -u postgres psql -d tramontana -c "
SELECT relname, seq_scan, seq_tup_read, idx_scan,
seq_tup_read/nullif(seq_scan,0) AS files_per_recorregut
FROM pg_stat_user_tables
WHERE seq_scan > 100 AND seq_tup_read/nullif(seq_scan,0) > 10000
ORDER BY seq_tup_read DESC;"
relname | seq_scan | seq_tup_read | idx_scan | files_per_recorregut
----------+----------+--------------+----------+----------------------
reserves | 1088 | 202744960 | 91204 | 186346
# Indexs que ningu no fa servir: ocupen espai i alenteixen cada INSERT
$ sudo -u postgres psql -d tramontana -c "
SELECT indexrelname, pg_size_pretty(pg_relation_size(indexrelid)) AS mida, idx_scan
FROM pg_stat_user_indexes WHERE idx_scan < 50
AND indexrelid NOT IN (SELECT conindid FROM pg_constraint)
ORDER BY pg_relation_size(indexrelid) DESC;"
indexrelname | mida | idx_scan
----------------------+-------+----------
idx_reserves_telefon | 22 MB | 0Un índex sense fer servir no és neutre: ocupa 22 MB de disc i de memòria cau, i cada INSERT, UPDATE i DELETE l'ha d'actualitzar. Abans d'eliminar-lo cal confirmar que les estadístiques cobreixen un període representatiu —un índex fet servir només al tancament mensual semblarà inútil el dia 12.
Rèplica en flux
Reprenem 07-07, on va quedar decidit: replicació asíncrona (el RPO de 4 h la fa més que suficient) i promoció manual (amb dos nodes no hi ha quòrum, i l'automàtica produeix dades divergents).
# ===== Al PRIMARI (10.0.2.15) =====
# wal_level = replica i archive_mode = on ja hi son.
$ sudo -u postgres psql -c "
SELECT slot_name FROM pg_create_physical_replication_slot('replica_16');"Una ranura de replicació (replication slot) fa que el primari conservi els segments de WAL que la rèplica encara no ha consumit, encara que la rèplica porti hores aturada. És el que garanteix que una rèplica que cau de nit pugui recuperar-se al matí sense recrear-la sencera.
I porta el risc simètric, que cal conèixer: una ranura d'una rèplica que no torna mai omple pg_wal fins a aturar el primari. Per això s'acota:
# conf.d/40-replicacio.conf al PRIMARI
max_wal_senders = 3
max_replication_slots = 3
wal_keep_size = 1GB
# Limit de seguretat: si la ranura acumula mes de 8 GB, s invalida.
# Millor perdre la replica que aturar el primari.
max_slot_wal_keep_size = 8GB# ===== A la REPLICA (10.0.2.16) =====
$ sudo systemctl stop postgresql@16-main
$ sudo -u postgres rm -rf /var/lib/postgresql/16/main/*
$ sudo -u postgres PGPASSWORD=$(pass tramontana/replicador) pg_basebackup \
-h 10.0.2.15 -U replicador -D /var/lib/postgresql/16/main \
-R -P -Xs -C -S replica_16 \
-d "sslmode=verify-full sslrootcert=/etc/postgresql/ca-tramontana.crt"
1843712/1843712 kB (100%), 1/1 tablespace| Opció | Què fa |
|---|---|
-R |
Escriu postgresql.auto.conf i standby.signal: la rèplica queda llesta |
-S replica_16 |
Fa servir la ranura creada |
-Xs |
Rep el WAL mentre copia: no es perd res |
sslmode=verify-full |
El WAL viatja xifrat i verificat |
$ sudo -u postgres cat /var/lib/postgresql/16/main/postgresql.auto.conf
primary_conninfo = 'user=replicador passfile=''/var/lib/postgresql/.pgpass''
host=10.0.2.15 port=5432 sslmode=verify-full ...'
primary_slot_name = 'replica_16'
$ sudo systemctl start postgresql@16-main
$ sudo -u postgres psql -c "SELECT pg_is_in_recovery();"
pg_is_in_recovery
-------------------
tI la vigilància, des del primari:
$ sudo -u postgres psql -x -c "
SELECT client_addr, state, sync_state,
pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS retard_bytes,
write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
-[ RECORD 1 ]+----------------
client_addr | 10.0.2.16
state | streaming
sync_state | async
retard_bytes | 0 bytes
write_lag | 00:00:00.002
replay_lag | 00:00:00.004retard_bytes és quantes dades es perdrien si el primari caigués ara: la mètrica més important de tot l'apartat, i va a la monitoració de 08-06 al costat de l'antiguitat de l'última còpia.
La rèplica serveix a més per a una cosa immediata i valuosa: hot_standby = on permet consultes de només lectura sobre ella, així que els informes pesants —els de 3,7 segons— es poden executar allà sense tocar el primari.
# A la replica, perque les consultes llargues no es tallin per conflicte
# amb la reproduccio del WAL
$ echo "max_standby_streaming_delay = 300s" | \
sudo tee -a /etc/postgresql/16/main/conf.d/40-replicacio.confI el recordatori que mai no sobra, ja enunciat a 07-07: la rèplica no és una còpia de seguretat. El DELETE de l'apartat 9 arriba a la rèplica en quatre mil·lisegons. Per desfer-lo hi ha el PITR; la rèplica protegeix de la fallada de maquinari, no de l'error humà.
Automatització amb Ansible
# ~/tramontana-infra/roles/bd/defaults/main.yml
---
bd_version: 16
bd_ram_mb: 3800
# Les formules viuen a les variables: canvia la RAM i tot es recalcula
bd_shared_buffers_mb: "{{ (bd_ram_mb * 0.25) | int }}"
bd_effective_cache_mb: "{{ (bd_ram_mb * 0.67) | int }}"
bd_work_mem_mb: 8
bd_maintenance_work_mem_mb: "{{ (bd_ram_mb * 0.07) | int }}"
bd_max_connections: 60
bd_random_page_cost: 1.1 # SSD
bd_log_min_duration_ms: 250
bd_archive_dir: /srv/tramontana/backups/wal
bd_pgbouncer_pool_size: 25
bd_pgbouncer_max_clients: 200
bd_xarxes_permeses:
- { xarxa: '127.0.0.1/32', bd: tramontana, rol: svc_tramontana, tipus: host }
- { xarxa: '10.0.2.0/24', bd: tramontana, rol: operador, tipus: hostssl }
- { xarxa: '10.0.2.16/32', bd: replication, rol: replicador, tipus: hostssl }# ~/tramontana-infra/roles/bd/tasks/main.yml
---
- name: Comprovar que la RAM declarada coincideix amb la real
ansible.builtin.assert:
that: (ansible_memtotal_mb - bd_ram_mb) | abs < 400
fail_msg: >-
bd_ram_mb ({{ bd_ram_mb }}) no coincideix amb la RAM real
({{ ansible_memtotal_mb }} MB). Els calculs de memoria serien
incorrectes i podrien deixar el servidor sense memoria.
- name: Instal·lar PostgreSQL i PgBouncer
ansible.builtin.apt:
name:
- "postgresql-{{ bd_version }}"
- "postgresql-contrib-{{ bd_version }}"
- pgbouncer
- python3-psycopg2 # necessari per als moduls postgresql_*
state: present
tags: [paquets]
- name: Bloquejar la versio major (politica de 05-03)
ansible.builtin.dpkg_selections:
name: "postgresql-{{ bd_version }}"
selection: hold
- name: Directori d arxivat de WAL
ansible.builtin.file:
path: "{{ bd_archive_dir }}"
state: directory
owner: postgres
group: postgres
mode: '0700'
- name: Instal·lar l script d arxivat de WAL
ansible.builtin.copy:
src: arxivar_wal.sh
dest: /usr/local/bin/arxivar_wal.sh
owner: root
group: root
mode: '0755'
- name: Activar include_dir a postgresql.conf
ansible.builtin.lineinfile:
path: "/etc/postgresql/{{ bd_version }}/main/postgresql.conf"
line: "include_dir = 'conf.d'"
regexp: '^#?\s*include_dir\s*='
backup: true
notify: Reiniciar postgresql
- name: Desplegar l ajust calculat
ansible.builtin.template:
src: 10-tramontana.conf.j2
dest: "/etc/postgresql/{{ bd_version }}/main/conf.d/10-tramontana.conf"
owner: postgres
group: postgres
mode: '0640'
backup: true
notify: Reiniciar postgresql
- name: Desplegar pg_hba.conf
ansible.builtin.template:
src: pg_hba.conf.j2
dest: "/etc/postgresql/{{ bd_version }}/main/pg_hba.conf"
owner: postgres
group: postgres
mode: '0640'
backup: true
notify: Recarregar postgresql
- name: Forcar els handlers abans de verificar
ansible.builtin.meta: flush_handlers
# --- Verificacio: un pg_hba invalid deixa el servidor inaccessible ---
- name: Verificar que pg_hba.conf no te errors
community.postgresql.postgresql_query:
db: postgres
login_unix_socket: /var/run/postgresql
query: "SELECT count(*) AS errors FROM pg_hba_file_rules WHERE error IS NOT NULL"
become: true
become_user: postgres
register: hba
failed_when: hba.query_result[0].errors | int > 0
- name: Crear els rols amb contrasenya des de Vault
community.postgresql.postgresql_user:
name: "{{ item.nom }}"
password: "{{ item.password }}"
role_attr_flags: "{{ item.flags }}"
conn_limit: "{{ item.limit | default(omit) }}"
state: present
become: true
become_user: postgres
loop:
- { nom: svc_tramontana, password: "{{ vault_bd_app }}",
flags: 'LOGIN,NOSUPERUSER,NOCREATEDB,NOCREATEROLE', limit: 30 }
- { nom: replicador, password: "{{ vault_bd_replicador }}",
flags: 'LOGIN,REPLICATION' }
- { nom: monitor, password: "{{ vault_bd_monitor }}", flags: 'LOGIN' }
no_log: true # les contrasenyes no surten per pantalla
tags: [rols]
- name: Concedir pg_monitor al rol de monitoracio
community.postgresql.postgresql_membership:
group: pg_monitor
target_roles: monitor
state: present
become: true
become_user: postgres
- name: Configurar PgBouncer
ansible.builtin.template:
src: pgbouncer.ini.j2
dest: /etc/pgbouncer/pgbouncer.ini
owner: postgres
group: postgres
mode: '0640'
backup: true
notify: Reiniciar pgbouncer
- name: Verificar que l arxivat de WAL funciona
community.postgresql.postgresql_query:
db: postgres
login_unix_socket: /var/run/postgresql
query: "SELECT failed_count, last_archived_time FROM pg_stat_archiver"
become: true
become_user: postgres
register: arxivador
failed_when: arxivador.query_result[0].failed_count | int > 0
tags: [verificar]# roles/bd/handlers/main.yml
---
- name: Recarregar postgresql
community.postgresql.postgresql_query:
db: postgres
login_unix_socket: /var/run/postgresql
query: "SELECT pg_reload_conf()"
become: true
become_user: postgres
- name: Reiniciar postgresql
# Un reinici talla les connexions. Nomes es dispara quan canvia un
# parametre que ho requereix, i en produccio s executa en finestra.
ansible.builtin.systemd:
name: "postgresql@{{ bd_version }}-main"
state: restarted
- name: Reiniciar pgbouncer
ansible.builtin.systemd:
name: pgbouncer
state: restartedL'assert inicial mereix atenció: sense ell, aplicar el rol a una màquina amb menys RAM de la declarada produiria un shared_buffers més gran que la memòria física i PostgreSQL no arrencaria. És la mena de fallada que Ansible converteix en global si no es comprova.
Operació diària
#!/usr/bin/env bash
# Fragment per integrar a revisio_salut.sh: bloc de base de dades
comprovar_base_dades() {
local estat=0
# 1. Connexions esperant a l agrupacio
local esperant
esperant="$(psql -h 127.0.0.1 -p 6432 -U operador -d pgbouncer -tAc \
"SHOW POOLS" | awk -F'|' '$1=="tramontana"{print $4}')"
if (( esperant > 0 )); then
error "hi ha $esperant clients esperant connexio"; estat=1
fi
# 2. Retard de la replica (bytes que es perdrien ara mateix)
local retard
retard="$(sudo -u postgres psql -tAc \
"SELECT coalesce(max(pg_wal_lsn_diff(sent_lsn,replay_lsn)),0)::bigint
FROM pg_stat_replication")"
if (( retard > 104857600 )); then # 100 MB
error "la replica va $(formatar_bytes "$retard") per darrere"; estat=1
fi
# 3. Fallades d arxivat de WAL: si falla, pg_wal creix sense limit
local fallades
fallades="$(sudo -u postgres psql -tAc "SELECT failed_count FROM pg_stat_archiver")"
if (( fallades > 0 )); then
error "l arxivat de WAL ha fallat $fallades vegades"; estat=2
fi
# 4. Wraparound
local pct
pct="$(sudo -u postgres psql -tAc \
"SELECT round(100.0*max(age(datfrozenxid))/2000000000) FROM pg_database")"
if (( pct > 75 )); then
error "wraparound al ${pct}%: EMERGENCIA"; estat=2
elif (( pct > 50 )); then
error "wraparound al ${pct}%"; estat=1
fi
# 5. Transaccions obertes eternament
local zombis
zombis="$(sudo -u postgres psql -tAc \
"SELECT count(*) FROM pg_stat_activity
WHERE state='idle in transaction' AND now()-xact_start > interval '10 min'")"
if (( zombis > 0 )); then
error "$zombis transaccions ocioses de mes de 10 min"; estat=1
fi
(( estat == 0 )) && log "base de dades correcta"
return "$estat"
}Què es mira i quan:
| Freqüència | Comprovació | Llindar d'alerta |
|---|---|---|
| Continu (08-06) | Retard de la rèplica | > 100 MB |
| Continu | pg_stat_archiver.failed_count |
> 0 |
| Continu | Clients esperant a PgBouncer | > 0 sostingut |
| Continu | Espai lliure a pg_wal |
< 20 % |
| Diari | Índex d'encert de la memòria cau | < 95 % |
| Diari | Transaccions idle in transaction llargues |
> 10 min |
| Setmanal | Tuples mortes per taula | > 20 % |
| Setmanal | Top 5 de pg_stat_statements |
Canvis a la classificació |
| Mensual | Edat del comptador de transaccions | > 50 % |
| Mensual | Índexs sense fer servir i mida de la BD | Creixement inesperat |
| Trimestral | Assaig complet de PITR | S'ha de completar en < 30 min |
Aquella última fila és la més important de la taula i la que més s'incompleix. Una còpia que no s'ha restaurat mai no és una còpia: és un fitxer. L'assaig trimestral entra al simulacre semestral que vas proposar a 07-07.
Errors Comuns i Consells
- Pujar
max_connectionsen lloc de posar una agrupació. Cada connexió costa memòria i contenció. Amb 2 vCPU, 300 connexions donen menys feina feta que 25. - Pujar
work_memglobalment. És memòria per operació i per connexió: multiplica-ho abans de tocar-ho, o l'OOM killer s'encarregarà de recordar-t'ho. - Posar
shared_buffersal 80 %. Es duplica l'emmagatzematge amb la memòria cau del sistema i hi ha menys memòria útil que amb el 25 %. - Deixar
random_page_cost = 4en un SSD. El planificador evita índexs que hauria de fer servir, i les consultes es degraden sense causa aparent. - Editar
postgresql.confquanpostgresql.auto.confté el mateix paràmetre. Guanya el segon. Consultapg_settings.source. - Fer servir
trustapg_hba.conf, encara que sigui «temporalment» a localhost. És control total de la base de dades per a qualsevol procés local, i el temporal dura anys. - Recarregar
pg_hba.confsense mirarpg_hba_file_rules. Pots quedar-te fora de la teva pròpia base de dades. - Creure que
sslmode=requireverifica el certificat. No ho fa. Nomésverify-full. - Donar
SUPERUSERal rol de l'aplicació «perquè no doni problemes». Una injecció SQL passa de llegir dades a executar ordres al servidor. - Oblidar
ALTER DEFAULT PRIVILEGES. L'aplicació funciona fins que una migració crea una taula nova, i aleshores falla en producció. - Confondre rèplica amb còpia de seguretat. Un
DELETEes replica en mil·lisegons. Per desfer-lo cal PITR. VACUUM FULLen producció. Blocatge exclusiu: ningú no llegeix ni escriu mentre dura. Fes servirpg_repack.- Ignorar el wraparound fins que la base deixa d'acceptar escriptures. Vigila
age(datfrozenxid)i mata les transaccions ocioses. - No acotar
max_slot_wal_keep_size. Una rèplica caiguda omplepg_wali atura el primari. Millor perdre la rèplica. CREATE INDEXsenseCONCURRENTLYen producció. Bloqueja les escriptures de la taula mentre construeix.- Optimitzar per
mean_exec_time. Ordena pertotal_exec_time: la consulta de 10 ms executada 92.000 vegades costa més que la de 4 s executada 1.000. - Consell de mètode. Abans de canviar un paràmetre, apunta el valor actual i la mètrica que esperes moure. Un ajust sense mesura prèvia és indistingible de la superstició.
Exercicis
Exercici 1
revisio_salut.sh avisa a les 03:14 que l'espai de /var/lib/postgresql està al 91 % i pujant. Diagnostica la causa, explica el mecanisme, resol-ho sense perdre dades i proposa la prevenció definitiva.
Exercici 2
Dissenya i documenta el procediment complet d'assaig trimestral de recuperació a un punt en el temps, en forma de manual d'operació accionable per algú que no siguis tu, amb els seus criteris d'èxit.
Exercici 3
La Marta pregunta si val la pena la rèplica de PostgreSQL de la qual es va parlar a 07-07, ara que ja hi ha còpies amb recuperació al minut. Redacta la resposta.
Solucions
Solució 1
Diagnòstic. El primer és saber què creix, no quant:
$ sudo du -sh /var/lib/postgresql/16/main/* | sort -rh | head -4
9.8G /var/lib/postgresql/16/main/pg_wal
1.6G /var/lib/postgresql/16/main/base
2.1M /var/lib/postgresql/16/main/global
$ ls /var/lib/postgresql/16/main/pg_wal/*.ready 2>/dev/null | wc -l
612pg_wal amb 9,8 GB i 612 fitxers .ready a archive_status/. Un .ready és un segment que PostgreSQL vol arxivar i no ha aconseguit arxivar. La causa és immediata:
$ sudo -u postgres psql -c "SELECT archived_count, failed_count,
last_failed_wal, last_failed_time FROM pg_stat_archiver;"
archived_count | failed_count | last_failed_wal | last_failed_time
----------------+--------------+--------------------------+-------------------------------
4128 | 2044 | 000000010000000000000A31 | 2026-08-18 03:12:55.118+02
$ sudo journalctl -t arxivar_wal --since "6 hours ago" | tail -2
arxivar_wal[8812]: cp: cannot create regular file
'/srv/tramontana/backups/wal/.000000010000000000000A31.8812': No space left on deviceEl mecanisme, que és el que cal entendre. El LV lv-backups de 15 GiB, xifrat amb LUKS, s'ha omplert. arxivar_wal.sh retorna un codi diferent de zero, i aquí entra en joc el contracte de l'arxivat: PostgreSQL no esborra un segment fins que l'ordre d'arxivat confirma l'èxit. És un comportament correcte i deliberat —perdre un segment trencaria la cadena de PITR i amb ella totes les còpies posteriors— però produeix un efecte en cascada:
lv-backups ple -> arxivar_wal.sh falla -> els segments no s esborren -> pg_wal creix -> s omple /var/lib -> PostgreSQL S ATURA
Si /var/lib/postgresql s'omple del tot, PostgreSQL entra en mode pànic i s'atura. Amb el 91 % i pujant, queden hores, no dies.
I un segon sospitós que cal descartar sempre:
$ sudo -u postgres psql -c "SELECT slot_name, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retingut
FROM pg_replication_slots;"
slot_name | active | retingut
------------+--------+----------
replica_16 | t | 12 MBLa ranura està activa i només reté 12 MB: no és la causa. Si active = f amb diversos GB retinguts, la causa seria una rèplica caiguda.
Resolució, en ordre i sense perdre dades:
# --- PAS 1: espai immediat al desti d arxivat ---
# Pujar a restic el que ja esta arxivat i alliberar el mes antic
$ sudo restic backup /srv/tramontana/backups/wal --tag wal-urgent
$ sudo restic check --read-data-subset=5% # verificar ABANS d esborrar
# Nomes aleshores, esborrar els WAL anteriors a la copia base mes antiga
# que volem conservar. pg_archivecleanup calcula quins sobren: MAI no
# s esborren a ma per data.
$ sudo -u postgres pg_archivecleanup -d /srv/tramontana/backups/wal \
000000010000000000000900
pg_archivecleanup: removing file "000000010000000000000412"
...
pg_archivecleanup: 1288 files removed
$ df -h /srv/tramontana/backups
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg--dades-lv--backups 15G 4.2G 9.9G 30% /srv/tramontana/backups
# --- PAS 2: drenar la cua de .ready ---
$ sudo -u postgres psql -c "SELECT pg_switch_wal();"
$ sleep 60
$ ls /var/lib/postgresql/16/main/archive_status/*.ready | wc -l
0
$ sudo du -sh /var/lib/postgresql/16/main/pg_wal
412M /var/lib/postgresql/16/main/pg_wal
$ sudo -u postgres psql -c "SELECT pg_stat_reset_shared('archiver');"El que NO es fa, i és la temptació del moment:
| Acció temptadora | Conseqüència |
|---|---|
rm de fitxers de pg_wal |
Trenca la cadena de PITR i pot impedir l'arrencada. Mai |
archive_mode = off |
Alleuja avui i elimina la capacitat de PITR. És rendir-se |
archive_command = '/bin/true' |
Descarta segments silenciosament: PITR trencat sense avisar |
| Ampliar el LV sense més | Legítim, però sense arreglar la causa es repeteix en tres mesos |
Aquella tercera fila és especialment traïdora: tot sembla funcionar, failed_count es queda a zero, i el problema només apareix el dia que cal restaurar.
Prevenció definitiva, en quatre mesures:
# 1. Retencio automatica de WAL, setmanal, despres de verificar restic
$ cat /home/operador/scripts/purgar_wal.sh
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/comuns.sh"
readonly WAL_DIR=/srv/tramontana/backups/wal
readonly BASE_DIR=/srv/tramontana/backups/base
main() {
requereix_comanda pg_archivecleanup
requereix_comanda restic
# No purgar mai per sota de la copia base mes antiga que es conserva
local base_mes_antiga
base_mes_antiga="$(find "$BASE_DIR" -maxdepth 1 -type d -name '20*' | sort | head -1)" \
|| morir 75 "no hi ha cap copia base: NO es purga res"
[[ -n "$base_mes_antiga" ]] || morir 75 "no hi ha copia base; avortant"
# I nomes si restic te el contingut segur
restic check --read-data-subset=2% >/dev/null \
|| morir 65 "restic no verifica: no es purga res"
local wal_inicial
wal_inicial="$(awk '/^START WAL LOCATION/{print $6}' \
"${base_mes_antiga}/backup_label" | tr -d ')')"
log "purgant WAL anterior a $wal_inicial"
pg_archivecleanup -d "$WAL_DIR" "$wal_inicial"
}
main "$@"# 2. Alerta ABANS del problema, no quan ja no hi ha sortida
# /etc/systemd/system/vigilar-wal.service (executat cada 15 min)
[Service]
Type=oneshot
ExecStart=/home/operador/scripts/vigilar_wal.sh# vigilar_wal.sh: dos llindars, dos nivells
# - .ready > 20 -> avis: l arxivat s esta endarrerint
# - us de lv-backups > 75 % -> avis; > 90 % -> critic
# - failed_count > 0 -> critic immediat# 3. Xarxa de seguretat al propi PostgreSQL
# Limita quant WAL pot acumular una ranura abans d invalidar-la.
max_slot_wal_keep_size = 8GB# 4. Ampliar lv-backups amb marge (05-04), ara amb dades
$ sudo lvextend -L +10G /dev/vg-dades/lv-backups
$ sudo cryptsetup resize backups-xifrat
$ sudo resize2fs /dev/mapper/backups-xifratI les tres lliçons de mètode, que van al quadern de guàrdia:
- Una fallada d'arxivat és un incident de disponibilitat, encara que el símptoma sigui d'espai: la cadena acaba amb PostgreSQL aturat.
failed_count > 0ha de ser una alerta crítica des de la primera fallada, no quan el disc estigui al 91 %. És un exemple perfecte d'alerta de causa (08-06) que sí que mereix existir, perquè el seu símptoma triga hores a aparèixer i aleshores ja és tard.- La monitoració havia d'haver avisat als 4.128 arxivats i 1 fallada, no a les 2.044 fallades. Aquest incident és justificació directa per a la lliçó 08-06.
Solució 2
Manual d'operació: Assaig trimestral de recuperació a un punt en el temps
Document: RB-BD-02 · Versió: 1.0 · Data: 2026-08-18 Responsable: Operacions · Periodicitat: trimestral (març, juny, setembre, desembre) Durada estimada: 60 minuts · Risc per a producció: cap si se segueixen els passos Ubicació: còpia impresa a l'arxivador d'operacions i a
~/tramontana-infra/docs/. No es guarda únicament asrv-tramontana.0. Per què existeix aquest document
Una còpia que no s'ha restaurat mai no és una còpia: és un fitxer del qual suposem coses. Aquest assaig verifica que la cadena completa —còpia base, arxivat de WAL,
restic, i el procediment— funciona abans de necessitar-la. També mesura el temps real, que és la dada que sosté el RTO acordat de 2 hores.1. Requisits previs (5 min)
# Comprovació Ordre Criteri 1.1 Màquina de proves engegada virsh list --allsrv-tramontana-provesactiva1.2 Mateixa versió major de PostgreSQL ssh ... psql --version16.x a totes dues 1.3 Espai a proves df -h /var/lib/postgresql≥ 3 × mida de la BD 1.4 Repositori restic accessible restic snapshots --tag base | tail -3Almenys 2 còpies base 1.5 Frase de pas de restic disponible pass restic/tramontanaEs recupera 1.6 Finestra avisada — Marta informada per correu Si 1.4 o 1.5 fallen, l'assaig s'atura i es declara incident. No poder accedir a les còpies és exactament l'escenari que aquest assaig ha de descobrir.
2. Triar l'objectiu (5 min)
L'assaig ha de recuperar a un instant arbitrari dins de les últimes 24 hores, no al de la còpia base — recuperar a la còpia base no prova el WAL, que és la meitat del mecanisme.
# Instant objectiu: ahir a les 15:00 $ OBJECTIU="$(date -d 'yesterday 15:00' '+%Y-%m-%d %H:%M:%S%:z')" # Dada de control: una fila que existeixi en aquell moment i que es # pugui verificar despres. S anota AQUI, abans de comencar. $ sudo -u postgres psql -d tramontana -tAc \\ "SELECT id, import FROM app.reserves WHERE creat_el < '$OBJECTIU' ORDER BY creat_el DESC LIMIT 1;" 1023|412.50Anotar: objectiu
2026-08-17 15:00:00+02, control: reserva 1023, import 412,50 €.3. Restauració (25 min)
# 3.1 Marcar l inici: el cronometre comenca AQUI $ INICI=$(date +%s) # 3.2 A srv-tramontana-proves $ ssh [email protected] $ sudo systemctl stop postgresql@16-main $ sudo -u postgres rm -rf /var/lib/postgresql/16/main $ sudo -u postgres mkdir -m 0700 /var/lib/postgresql/16/main # 3.3 Portar la copia base ANTERIOR a l objectiu des de restic $ restic restore latest --tag base --target /tmp/rest --host srv-tramontana $ sudo -u postgres tar -xzf /tmp/rest/srv/tramontana/backups/base/*/base.tar.gz \\ -C /var/lib/postgresql/16/main # 3.4 I els WAL $ restic restore latest --tag wal --target /tmp/rest # 3.5 Configurar la recuperacio $ sudo -u postgres tee /var/lib/postgresql/16/main/postgresql.auto.conf <<EOF restore_command = 'cp /tmp/rest/srv/tramontana/backups/wal/%f %p' recovery_target_time = '$OBJECTIU' recovery_target_action = 'pause' EOF $ sudo -u postgres touch /var/lib/postgresql/16/main/recovery.signal # 3.6 Arrencar i seguir el proces $ sudo systemctl start postgresql@16-main $ sudo journalctl -u postgresql@16-main -fSortida esperada (si no apareix
recovery stopping before..., l'assaig ha fallat):LOG: starting point-in-time recovery to 2026-08-17 15:00:00+02 LOG: restored log file "0000000100000000000009F1" from archive LOG: recovery stopping before commit of transaction 91204, time 2026-08-17 15:00:03+02 LOG: pausing at the end of recovery4. Verificació (10 min) — els criteris d'èxit
# Criteri Ordre Llindar 4.1 El servidor va arribar a l'objectiu journalctl | grep 'recovery stopping'Apareix, amb hora ≈ objectiu 4.2 La dada de control existeix i coincideix SELECT import FROM app.reserves WHERE id=1023412,50 4.3 No hi ha dades posteriors a l'objectiu SELECT count(*) FROM app.reserves WHERE creat_el > '$OBJECTIU'0 4.4 Integritat estructural SELECT count(*) FROM app.reservesCoherent amb producció 4.5 Sense errors de suma de verificació journalctl | grep -ci 'checksum|corrupt'0 4.6 Temps total echo $(( $(date +%s) - INICI ))< 1800 s El criteri 4.3 és el que valida de debò el PITR: si hi hagués dades posteriors a l'objectiu, la recuperació no es va aturar on tocava i el mecanisme no serveix per desfer un esborrat.
$ sudo -u postgres psql -d tramontana -c " SELECT (SELECT import FROM app.reserves WHERE id=1023) AS control, (SELECT count(*) FROM app.reserves WHERE creat_el > '$OBJECTIU') AS posteriors, (SELECT count(*) FROM app.reserves) AS total;" control | posteriors | total ---------+------------+-------- 412.50 | 0 | 1859125. Neteja (5 min)
$ sudo systemctl stop postgresql@16-main $ sudo -u postgres rm -rf /var/lib/postgresql/16/main /tmp/rest $ sudo virsh snapshot-revert srv-tramontana-proves neta # des de l amfitrioMai no es deixa la màquina de proves amb una còpia de les dades de producció: conté dades personals reals de clients i quedaria fora de l'abast de les mesures de seguretat de producció. És un requisit del RGPD, no una mania.
6. Registre del resultat
S'anota a
~/tramontana-infra/docs/assajos-pitr.md, encara que l'assaig surti perfecte:| Data | Objectiu | Temps | Criteris | Incidencies | |------------|---------------------|--------|-----------|--------------------------------| | 2026-08-18 | 2026-08-17 15:00+02 | 24m11s | 6/6 OK | Cap | | 2026-06-14 | 2026-06-13 11:00+02 | 41m02s | 5/6 | 4.6 fallada: restic lent per xarxa|7. Si alguna cosa falla
Fallada Causa probable Acció requested recovery stop point is before consistent recovery pointLa còpia base és posterior a l'objectiu Fer servir una còpia base anterior could not restore file ... from archiveFalta un segment de WAL: cadena trencada Incident greu: revisar pg_stat_archiveri la purgaLa recuperació no s'atura i arriba al final recovery_target_timemal formatat o zona horàriaRevisar el format amb +02explícitTemps > 30 min Xarxa, xifratge LUKS o descompressió Analitzar i revisar el RTO amb la Marta Errors de suma de verificació Corrupció a la còpia Incident greu: provar una altra còpia i revisar el maquinari 8. Escalat
Si l'assaig falla a 4.2, 4.3 o 4.5, es declara incident de severitat alta el mateix dia: significa que avui no podríem recuperar les dades. S'avisa la Marta i s'atura qualsevol altra feina fins a resoldre-ho.
Dues notes de disseny del manual d'operació: està escrit perquè l'executi algú que no el va redactar —cada ordre és copiable i cada criteri té un llindar numèric—, i inclou què fer quan falla, que és la part que gairebé tots els manuals d'operació ometen i l'única que cal el dia dolent.
Solució 3
Val la pena la rèplica de la base de dades? Per a: Marta Vidal · De: Operacions de sistemes · 18 d'agost de 2026
Resposta breu: sí, però no pel motiu pel qual se sol muntar, i l'ordre importa. La recomano, amb una inversió moderada, i sobretot per un benefici que no és l'evident.
Primer, una bona notícia. La feina d'aquesta setmana ha millorat la nostra capacitat de recuperació molt més del previst:
Abans Ara Dades que podríem perdre en un desastre Fins a 4 hores Menys de 15 minuts Podem desfer un esborrat accidental? No: només tornar a la còpia de la nit Sí, al segon anterior Temps de restauració completa ~2 hores estimades 24 minuts, mesurats Aquell salt no ha costat diners: és una tècnica que guarda contínuament el registre de canvis de la base de dades, de manera que podem «rebobinar» a qualsevol instant. Ho he assajat i funciona.
Aleshores, per a què la rèplica? Perquè resol un problema diferent, i convé no confondre'ls:
Problema Ho resolen les còpies? Ho resol la rèplica? Algú esborra dades per error Sí, al segon anterior No: l'esborrat es copia en mil·lisegons Corrupció lògica de dades Sí No S'espatlla el disc del servidor Sí, en 24 minuts Sí, en 5-15 minuts Els informes pesants alenteixen el web No Sí, i avui mateix Cal actualitzar el servidor No Sí: es treballa sobre un mentre l'altre atén El proveïdor té una caiguda general No Només si està en una altra ubicació I aquí està l'argument principal, que no és el de l'avaria. Avui tenim consultes d'informes que triguen gairebé quatre segons i competeixen amb les reserves dels clients per la mateixa màquina. Amb una rèplica, aquells informes s'executen a la còpia, i el web deixa de notar-los. És una millora de rendiment immediata i perceptible, no una assegurança per a un dia que potser no arribarà.
Què protegeix i què no protegeix la rèplica, en una línia cada cosa:
- Protegeix que s'espatlli el maquinari del servidor principal.
- Protegeix el rendiment del web davant dels informes pesants.
- Permet actualitzar sense finestra de manteniment.
- No protegeix d'un esborrat per error, ni de dades corruptes: això es copia immediatament. Per a això hi ha les còpies, que continuen sent igual de necessàries.
- No protegeix si el problema és de l'aplicació o de la xarxa.
- No s'activa sola. Recomano expressament que el canvi de servidor sigui manual, i t'explico per què al punt següent.
Per què manual, encara que soni pitjor. Amb només dos servidors existeix un risc que al sector s'anomena «cervell dividit»: si es talla la comunicació entre tots dos però tots dos continuen vius, cadascun es pensa que l'altre ha caigut i tots dos comencen a acceptar reserves. El resultat són dues bases de dades amb informació diferent i incompatible, i reconciliar-les pot ser impossible: reserves duplicades sobre la mateixa casa i la mateixa nit. Evitar-ho automàticament exigeix un mínim de cinc màquines. Amb dues, la decisió la pren una persona en cinc o deu minuts, i aquells minuts són un preu petit davant d'aquell risc.
El que costaria:
Concepte Cost Una màquina més Equivalent al servidor actual Posada en marxa 2-3 dies, ja automatitzada amb la resta Manteniment addicional ~1 h al mes Canvi a l'aplicació Cap per a l'avaria; petit per dirigir els informes a la rèplica La meva recomanació, per ordre de prioritat:
- Ja fet, cost zero: les còpies amb rebobinat al segon i l'assaig trimestral que les verifica. Això era l'urgent i ja està.
- Aquest trimestre: la rèplica, justificada sobretot pel rendiment dels informes i, de passada, per poder actualitzar sense tallar el servei. Amb canvi manual.
- No recomano, avui: el canvi automàtic de servidor. Exigeix cinc màquines per ser segur, i amb dues crearia un risc més gran que el que evita.
- Pendent, i ho porto a part: la taula d'auditoria ocupa ja més que totes les dades de reserves juntes. Necessitem decidir quant temps guardem aquell històric, i és una decisió teva i no meva, perquè té implicacions legals de protecció de dades.
Una última cosa que vull deixar per escrit. Amb la rèplica continuaríem necessitant exactament les mateixes còpies de seguretat que avui. És la confusió més habitual en aquest terreny i la que provoca les pèrdues de dades més greus: tenir una còpia en viu de les dades dona una sensació de seguretat que no es correspon amb la realitat, perquè copia fidelment també els errors. La rèplica és per a les avaries; les còpies són per als errors. Calen totes dues.
Conclusió
PostgreSQL ja no corre amb la configuració que va portar el paquet. Has repartit els 3,8 GB amb fórmules que saps justificar: shared_buffers al 25 % perquè hi ha una segona memòria cau a sota, effective_cache_size al 67 % perquè no reserva res i canvia els plans que tria l'optimitzador, work_mem al valor que resisteix multiplicar-se per connexions i per operacions, i random_page_cost a 1,1 perquè el disc és un SSD i el valor per defecte descriu un món de discos giratoris. I has tancat el cercle amb 07-03: les pàgines enormes en madvise, amb reserva explícita calculada preguntant-ho al mateix servidor en lloc d'estimar-la.
Has resolt l'incident que va quedar obert a 07-02, i de la manera correcta: no pujant max_connections, sinó baixant-lo a 60 i posant PgBouncer en mode transacció al davant, on 63 clients d'aplicació comparteixen quatre connexions reals. Has tancat pg_hba.conf línia a línia, sabent que guanya la primera coincidència i que trust no es posa mai, ni a 127.0.0.1 ni «temporalment». Has creat un rol d'aplicació que no pot crear taules, i ho has verificat intentant-ho. I saps que sslmode=require xifra però no verifica res.
Sobretot, la base de dades ja es pot recuperar. La còpia física amb pg_basebackup, l'arxivat de WAL amb un script el contracte del qual entens —retornar zero només si el segment està segur—, i un procediment de recuperació a un punt en el temps que has executat de principi a fi, amb recovery_target_action = 'pause' per poder mirar abans de comprometre't. El RPO ha passat de 4 hores a minuts sense gastar un euro, i l'assaig trimestral està escrit perquè l'executi algú que no siguis tu. Coneixes el VACUUM, l'inflament, el wraparound del comptador de transaccions i per què una transacció idle in transaction de tres dies és capaç de tombar una base de dades sencera. I saps llegir un EXPLAIN (ANALYZE, BUFFERS), que és el que va convertir una consulta de 3.781 ms en una de 182 ms.
A 08-03 l'escenari canvia completament, i a propòsit. Construiràs un servidor de mitjans per a casa teva: Jellyfin sobre el teu propi maquinari, amb emmagatzematge redundant, acceleració per maquinari per a la transcodificació, comparticions per als dispositius de la família i accés des de fora. És el projecte on comproves que res del que has après era «cosa de servidors d'empresa»: els mateixos UUID a fstab, la mateixa unitat de systemd endurida, les mateixes claus a keyrings, les mateixes còpies verificades i el mateix smartctl vigilant discos. Amb dues diferències que a casa importen i a la feina no: el consum elèctric i el soroll. I amb un advertiment que convé llegir abans de començar, sobre quin contingut és legítim tenir-hi.
Curs de Linux: De Principiant a Administrador de Sistemes
Mòdul 1: Introducció a Linux
- Què és Linux?
- Història de Linux
- Distribucions de Linux
- Instal·lant Linux
- Primer Contacte amb el Sistema
- Estructura del Sistema de Fitxers de Linux
Mòdul 2: Comandes Bàsiques de Linux
- Introducció a la Línia de Comandes
- Obtenir Ajuda i Documentació del Sistema
- Navegant pel Sistema de Fitxers
- Operacions amb Fitxers i Directoris
- Visualització i Edició de Fitxers
- Enllaços Durs i Simbòlics
- Permisos i Propietat dels Fitxers
Mòdul 3: Habilitats Avançades en la Línia de Comandes
- L'Entorn del Shell: Variables, Àlies i Historial
- Ús de Comodins i Expressions Regulars
- Cerca de Fitxers i Contingut: find, locate i grep
- Canonades i Redirecció
- Processament de Text: cut, sort, uniq, sed i awk
- Gestió de Processos
- Programació de Tasques amb Cron
- Comandes de Xarxa
Mòdul 4: Scripting en Shell
- Introducció al Scripting en Shell
- Variables i Tipus de Dades
- Entrada, Sortida i Arguments d'un Script
- Estructures de Control
- Funcions i Biblioteques
- Depuració i Gestió d'Errors
- Scripts de Producció: Bones Pràctiques
Mòdul 5: Administració del Sistema
- Gestió d'Usuaris i Grups
- sudo i Permisos Especials
- Gestió de Paquets
- Gestió de Discs
- systemd i la Gestió de Serveis
- Registres del Sistema: journald i syslog
- Monitoratge del Sistema i Optimització del Rendiment
- Còpies de Seguretat i Restauració
Mòdul 6: Xarxes i Seguretat
- Configuració de Xarxes
- SSH i Accés Remot
- Tallafocs i Seguretat Perimetral
- Sistemes de Detecció d'Intrusions
- Gestió de Secrets i Certificats TLS
- Assegurant Sistemes Linux
Mòdul 7: Temes Avançats
- El Procés d'Arrencada i la Recuperació del Sistema
- Diagnòstic Avançat: strace, perf i eBPF
- Optimització del Nucli de Linux
- Virtualització amb Linux
- Contenidors de Linux i Docker
- Automatització amb Ansible
- Alta Disponibilitat i Balanceig de Càrrega
