Aanleiding
Een audittabel met ruim 1,6 miljard rijen moest uit de productiedatabase, maar wel opvraagbaar blijven: bij een vraag van security of een externe partij moet je binnen redelijke tijd kunnen zeggen wie wat wanneer heeft gedaan. De klassieke route was een aparte herstelserver met pg_restore van een dump van honderden gigabytes, en dat kostte dagen.
Het archief
- Per jaar in chunks: 2.849 brokken over drie jaar, elk jaar met een
manifest.json,SHA256SUMSen een eigen herstelscript. - Versleuteld opgeslagen via rclone crypt in S3.
- Verificatie bij het maken: het aantal geïmporteerde rijen is gelijk aan het aantal gescande rijen, en de checksums van de bron zijn gevalideerd. Dat staat in het manifest, zodat je het later kunt aantonen.
Herstellen als Job in Kubernetes
De nieuwe route: een Kubernetes Job die een PostgreSQL-pod met een groot volume opzet, de gevraagde jaren uit S3 haalt, de checksums controleert en importeert. Een resume-ledger houdt bij welke chunks al binnen zijn, zodat een onderbroken import verder kan waar hij was.
Na de vraag zijn de pod, het volume (1,5 TB) en het PV weer verwijderd. Er blijft niets online staan dat er niet hoeft te staan.
De herstelkit bundelt het script, de handleiding en drie routes: de Job (aanbevolen), stap voor stap met de hand, en de klassieke herstelserver.
Fouten die ik in de oude kit vond
- Een kubeconfig-context die niet meer bestond.
- De crypt-configuratie verwachtte objecten op
.encrypted, terwijl ze op.bineindigden. Met dezelfde wachtwoorden en het juiste suffix werkte alles. - Oudere dumps bevatten JSONB-indexen die bij een lookup niemand nodig heeft, maar die het herstel uren langer maken. De kit slaat ze nu over.
De query-valkuil
De tabel heeft een B-tree-index op één veld uit de JSON-snapshot en een op de tijd, maar geen GIN-index op de hele snapshot. Dat maakt een groot verschil:
-- parallel seq scan over 1,6 miljard rijen, cost ~98.000.000
WHERE snapshot @> '{"ClientId": "..."}'
-- index scan, cost ~19.000
WHERE snapshot->>'ClientId' = '...'
Het tijdsveld is timestamp without time zone: gebruik half-open ranges (>= begin AND < eind) zonder tijdzoneconversie, anders valt de index weg of mis je randgevallen.
Les
Een archief is pas een archief als je kunt aantonen dat het compleet is, én als iemand anders het kan terughalen zonder jou. Manifests, checksums en een herstelkit met handleiding zijn belangrijker dan de opslag zelf.