Postgres Autovacuum Tunen voor Rails: Stop Table Bloat en Transaction ID Wraparound in Productie
Een klant belde me vorig jaar op een zondagochtend omdat hun productie Rails-applicatie plotseling geen schrijfopdrachten meer accepteerde. Postgres logde een melding die ze nog nooit hadden gezien: “database is not accepting commands to avoid wraparound data loss”. Hun grootste tabel was opgeblazen tot meer dan 400 GB op disk, terwijl er maar zo’n 18 GB aan levende rijen in zat. Autovacuum had de race negen maanden lang stilletjes verloren en niemand had het gemerkt totdat de database zelf op de rem ging staan.
Na negentien jaar Rails kan ik je met overtuiging zeggen: Postgres autovacuum tunen is de meest onderschatte hefboom voor productie Rails-performance. De meeste teams blijven voor altijd op de defaults zitten, die defaults zijn geschreven voor laptops uit 2005, en op een dag verslindt een drukke tabel je hele middag. Deze post is het draaiboek dat ik wou dat die klant had gehad.
Wat Postgres Autovacuum Eigenlijk Doet
Postgres gebruikt MVCC — multi-version concurrency control. Wanneer je een rij UPDATE, past Postgres deze niet op zijn plek aan. Het schrijft een nieuwe tuple en markeert de oude als dood. Wanneer je een rij DELETE, wordt die rij alleen gevlagd. De ruimte wordt niet teruggewonnen. Leesqueries slaan dode tuples over, maar betalen wel de kosten om eroverheen te lopen.
Autovacuum is het achtergrondproces dat:
- Ruimte terugwint van dode tuples zodat de tabel niet ongelimiteerd groeit.
- De planner-statistieken bijwerkt die queryplannen aansturen (
ANALYZE). - Zeer oude transactie-ID’s bevriest om transaction ID wraparound te voorkomen — de apocalyptische 32-bits teller van Postgres die, als hij overloopt, je database read-only zet totdat je het oplost.
Als autovacuum gezond is, denk je er nooit aan. Als het achterloopt, krijg je drie smaken pijn: tabellen die meerdere keren groter worden dan hun levende data (bloat), queryplannen die de mist in gaan omdat statistieken oud zijn, en de wraparound-noodmodus die mijn klant onderuit haalde.
Waarom Postgres Autovacuum Tunen Belangrijk Is voor Rails
Rails-applicaties zijn er bijzonder goed in om de standaard autovacuum-instellingen te verwarren, om drie redenen.
Ten eerste houdt Active Record van UPDATE. Een typische User.find(1).update(last_seen_at: Time.current) is een volledige rij-update die een dode tuple produceert, zelfs als er maar één kolom verandert. Vermenigvuldig dat met een session-tracking middleware en je hebt een hete tabel die miljoenen dode tuples per dag produceert.
Ten tweede hebben Rails-applicaties vaak een paar enorme tabellen (events, audits, jobs, sessions) en veel kleine. Default autovacuum-thresholds schalen op percentage van tabelgrootte, wat betekent dat hele grote tabellen bijna nooit worden gevacuumd, terwijl piepkleine tabellen constant worden gevacuumd.
Ten derde versterken background job systemen dit. Als je Solid Queue of Sidekiq draait, is de jobs-tabel constant in beweging — elke enqueue is een INSERT, elke voltooide job is een DELETE. Zonder tuning is de jobs-tabel het eerste wat opzwelt.
Postgres autovacuum tunen voor Rails is voorbij een bepaalde schaal niet optioneel. Het lijkt alleen optioneel tot de avond dat het dat niet meer is.
Hoe Je Ziet Of Autovacuum Achterloopt
Voordat je iets tuned, meet. De twee views die ertoe doen, leven in de system catalog. Zet deze in een Rake-task of een Rails runner-script.
# lib/tasks/postgres_health.rake
namespace :db do
desc "Show table bloat and last vacuum stats"
task vacuum_status: :environment do
sql = <<~SQL
SELECT
schemaname,
relname AS table_name,
n_live_tup AS live_rows,
n_dead_tup AS dead_rows,
ROUND(n_dead_tup::numeric / NULLIF(n_live_tup, 0), 2) AS dead_ratio,
last_autovacuum,
last_autoanalyze
FROM pg_stat_user_tables
WHERE n_live_tup > 1000
ORDER BY n_dead_tup DESC
LIMIT 20;
SQL
ActiveRecord::Base.connection.execute(sql).each do |row|
puts row.inspect
end
end
end
Een gezonde tabel heeft een dead_ratio onder ongeveer 0,2 en een last_autovacuum binnen de laatste dag of twee. Als je een tabel van 2 GB ziet met een dead ratio van 5 en last_autovacuum twee maanden geleden, heb je je probleem gevonden.
Voor specifiek wraparound-risico bewaak je de leeftijd van de oudste niet-bevroren transactie:
ActiveRecord::Base.connection.execute(<<~SQL).to_a
SELECT
relname,
age(relfrozenxid) AS xid_age,
pg_size_pretty(pg_table_size(oid)) AS size
FROM pg_class
WHERE relkind = 'r'
ORDER BY age(relfrozenxid) DESC
LIMIT 10;
SQL
Postgres triggert anti-wraparound autovacuum bij autovacuum_freeze_max_age (default 200 miljoen). De harde stop is bij 2 miljard. Als je grootste tabel een xid_age ten noorden van een miljard laat zien, zit je in de gevarenzone — plan een handmatige VACUUM FREEZE tijdens lage traffic en begin agressief te tunen.
De Defaults Die Je Pijn Gaan Doen
Postgres komt met deze autovacuum-thresholds:
autovacuum_vacuum_threshold = 50
autovacuum_vacuum_scale_factor = 0.2
autovacuum_analyze_threshold = 50
autovacuum_analyze_scale_factor = 0.1
autovacuum_vacuum_cost_limit = -1 (gebruikt vacuum_cost_limit = 200)
autovacuum_vacuum_cost_delay = 2ms (Postgres 12+)
autovacuum_max_workers = 3
De gevaarlijke is autovacuum_vacuum_scale_factor = 0.2. Dat betekent dat autovacuum een tabel niet eens overweegt te vacuumen totdat 20% ervan dood is. Op een tabel van 10 GB is dat 2 GB aan dode tuples voordat er iets gebeurt. Op een tabel van 100 GB is dat 20 GB aan dode tuples — en dan duurt de vacuum zelf uren, blokkeert hij andere autovacuum-workers en wordt hij waarschijnlijk afgebroken door je deploy-pipeline.
autovacuum_vacuum_cost_limit = 200 is ook te conservatief voor elke moderne SSD. Het throttelt de vacuum-doorvoer zo agressief dat autovacuum op een drukke tabel nooit kan inhalen.
Een Verstandige Baseline voor Rails Productie
Hier is de configuratie die ik in vrijwel elke Rails Postgres-instance boven de 50 GB neerzet. Zet hem in postgresql.conf of de parametergroep van je managed database:
# Draai meer workers, parallel
autovacuum_max_workers = 6
# Word vaker wakker
autovacuum_naptime = 30s
# Veel agressievere doorvoer op SSD
autovacuum_vacuum_cost_limit = 2000
autovacuum_vacuum_cost_delay = 10ms
# Verlaag de scale factor globaal zodat grote tabellen eerder worden gevacuumd
autovacuum_vacuum_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.02
# Vries proactief in om noodanti-wraparound werk te voorkomen
autovacuum_freeze_max_age = 400000000
vacuum_freeze_min_age = 50000000
Dit is een startpunt, geen eindbestemming. De echte winst zit in tunen per tabel.
Per-Tabel Autovacuum Tunen Vanuit Rails Migrations
De fout die ik het vaakst zie, is dat teams alles globaal proberen te tunen. De juiste stap is om je drie of vier heetste tabellen te identificeren en ze hun eigen autovacuum-policy te geven. Dit kan vanuit een gewone Rails-migratie — geen reden om Active Record te verlaten.
class TuneAutovacuumForJobs < ActiveRecord::Migration[8.0]
def up
execute <<~SQL
ALTER TABLE solid_queue_jobs SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_cost_limit = 4000,
autovacuum_vacuum_cost_delay = 5
);
SQL
end
def down
execute <<~SQL
ALTER TABLE solid_queue_jobs RESET (
autovacuum_vacuum_scale_factor,
autovacuum_analyze_scale_factor,
autovacuum_vacuum_cost_limit,
autovacuum_vacuum_cost_delay
);
SQL
end
end
Een 1% scale factor op een jobs-tabel met veel churn betekent dat autovacuum veel eerder begint en veel sneller klaar is. Elke individuele run doet minder werk, houdt locks korter vast en blijft uit de buurt van foreground-traffic.
Voor append-only-achtige tabellen (events, audit logs) is de analyze threshold belangrijker dan de vacuum threshold. Er worden constant nieuwe rijen toegevoegd, de planner heeft verse statistieken nodig, maar er zijn weinig dode tuples om terug te winnen:
class TuneAutovacuumForEvents < ActiveRecord::Migration[8.0]
def up
execute <<~SQL
ALTER TABLE events SET (
autovacuum_analyze_scale_factor = 0.005,
autovacuum_vacuum_scale_factor = 0.1,
autovacuum_freeze_max_age = 200000000
);
SQL
end
end
Voor tabellen waar je letterlijk alleen in invoegt en nooit update of verwijdert, kun je bloat-gedreven vacuum effectief uitschakelen en alleen freeze-gedreven vacuum laten draaien.
Letten Op Vacuum Die Geen Voortgang Kan Maken
De meest sluwe falingsmodus is autovacuum die continu draait maar nooit ruimte terugwint. Dit gebeurt wanneer er een lange transactie is die een snapshot vasthoudt. Postgres kan een dode tuple niet terugwinnen als een open transactie deze nog zou kunnen zien.
De gebruikelijke verdachten in een Rails-app:
- Een Sidekiq-job die een transactie heeft geopend en vastzit op een externe API-call.
- Een Rails console-sessie die iemand vergeten is (dit is de klassieker).
- Een lange analytics-query gestart vanaf een read replica.
- Een
pg_dumpdie op de primary draait in plaats van een replica.
Vind ze met:
SELECT
pid,
now() - xact_start AS xact_age,
state,
query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start ASC
LIMIT 10;
Alles ouder dan een paar minuten verdient een blik. Alles ouder dan een uur is bijna altijd fout. Ik voeg een Prometheus-alert toe die afgaat bij een open transactie ouder dan 15 minuten, want dat ene signaal vangt meer incidenten op dan welke andere Postgres-metric ik ook bewaak. Combineer dit met de observability patronen die ik voor Rails 8 behandelde en je vangt bloat voordat het jou vangt.
Handmatige VACUUM en Waarom Je Het Soms Nodig Hebt
Autovacuum is bewust beleefd. Het gebruikt cost-based throttling, geeft voorrang aan andere queries en houdt nooit de locks vast die nodig zijn om bestanden op disk daadwerkelijk te krimpen. Voor echt herstel van een bloat-incident heb je handmatige interventie nodig.
Alleen voor statistieken:
ANALYZE VERBOSE users;
Voor terugwinning van dode tuples zonder writes te blokkeren:
VACUUM (VERBOSE, ANALYZE) users;
Voor het krimpen van het bestand op disk (dit vereist een exclusive lock):
VACUUM FULL users;
VACUUM FULL is de nucleaire optie — het herschrijft de hele tabel en blokkeert alle reads en writes voor de duur. Op een tabel van 200 GB betekent dit uren downtime. Draai geen VACUUM FULL op een live productietabel zonder onderhoudsvenster.
Het moderne antwoord is pg_repack, dat dezelfde fysieke compactie tegelijk met normale traffic uitvoert. Installeer de extensie, en vanuit een Rake-task:
namespace :db do
desc "Repack a bloated table without downtime"
task :repack, [:table] => :environment do |_, args|
system("pg_repack --no-superuser-check --table=#{args[:table]} myapp_production")
end
end
Draai het vanaf een worker box, niet vanaf je applicatieservers. Het gebruikt ongeveer de grootte van de tabel aan extra schijfruimte tijdens het werk, dus zorg dat je marge hebt.
Het Geheel: Een Kwartaalritueel voor Postgres Health
De teams die nooit een autovacuum-incident hebben, hebben geen geluk. Ze hebben een ritueel. Dat van mij ziet er zo uit:
- Wekelijks: controleer de bloat-query hierboven. Alles met een dead ratio boven 1,0 wordt onderzocht.
- Maandelijks: bekijk
pg_stat_user_tablesvoor tabellen waarlast_autovacuumouder is dan de typische churn van die tabel zou suggereren. - Per kwartaal: beoordeel per-tabel autovacuum-instellingen tegen de werkelijke workload. Promoot nieuw-hete tabellen naar per-tabel tuning.
- Bij elke deploy: monitor op transacties die langer openblijven dan verwacht door background jobs.
Gecombineerd met verstandige Postgres connection pooling via PgBouncer en gedisciplineerde database indexing strategieën is Postgres autovacuum tunen wat een Rails-app een decennium lang op dezelfde database laat draaien. Sla het over en je krijgt uiteindelijk dat zondagochtend-telefoontje.
Veelgestelde Vragen
Hoe vaak zou autovacuum moeten draaien op een Rails Postgres-database?
Het hangt af van tabel-churn, maar voor een tabel met veel writes zoals een jobs queue of sessions table wil je dat autovacuum elke paar minuten draait. Voor voornamelijk-lezen tabellen is een keer per dag prima. De juiste metric is niet frequentie maar achterstand — als n_dead_tup blijft groeien tussen runs, loop je achter.
Wat is het verschil tussen VACUUM en VACUUM FULL?
Gewone VACUUM wint ruimte terug binnen het bestaande tabelbestand zodat toekomstige inserts deze kunnen hergebruiken, maar geeft geen schijfruimte terug aan het besturingssysteem. VACUUM FULL herschrijft de tabel vanaf nul, geeft schijfruimte terug en vereist een exclusive lock die alle reads en writes blokkeert. Gebruik pg_repack in plaats van VACUUM FULL in productie.
Kan ik autovacuum uitschakelen op een tabel met veel verkeer?
Technisch ja, praktisch nooit. Autovacuum uitschakelen betekent dat je verantwoordelijk wordt voor handmatig vacuumen en bevriezen, en een gemiste freeze leidt tot wraparound-shutdown. De juiste stap is per-tabel tuning om autovacuum agressiever te maken, niet uitzetten.
Waarom blijft Postgres autovacuum afgebroken worden?
Autovacuum geeft voorrang aan elke operatie die een conflicterende lock nodig heeft — typisch ALTER TABLE, CREATE INDEX of DROP TABLE. Rails migraties zijn de gebruikelijke oorzaak. Als je autovacuum-cancellaties rond deploy-tijden ziet clusteren, plan dan zware schema-wijzigingen in een apart venster van autovacuum-kritische tabellen, of gebruik waar mogelijk CONCURRENTLY-varianten.
Hulp nodig bij het diagnosticeren of oplossen van Postgres-performance in productie Rails? TTB Software is gespecialiseerd in Rails infrastructuur, database tuning en fractional CTO-opdrachten. We doen dit al negentien jaar.
About the Author
Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation