Database Migraties Die Je Niet om 3 Uur 's Nachts Wakker Maken
Database Migraties Die Je Niet om 3 Uur ‘s Nachts Wakker Maken
Je telefoon trilt. 3:17. De deployment ging om middernacht live, alles zag er prima uit, en nu zit je database muurvast. Net zo vast als een fiets bij Amsterdam Centraal.
Ik ken het gevoel. Na negentien jaar Rails deployments heb ik geleerd dat de migraties die er het simpelst uitzien, vaak het hardst bijten.
De Migratie Die Onschuldig Lijkt
Een klassieker:
add_index :orders, :customer_id
Eén regel. Netjes. Wat kan er misgaan?
Op een tabel met 50 miljoen rijen lockt Postgres de hele tabel terwijl die index wordt gebouwd. Afhankelijk van je hardware duurt dat ergens tussen de 30 seconden en een paar minuten. Al die tijd kan niemand orders inserten, updaten of verwijderen. Je checkout flow? Dood. De klantenservice-wachtrij? Groeiend.
Indexes Bouwen Zonder Drama
Postgres heeft een CONCURRENTLY optie voor index creatie. Het duurt langer, maar het blokkeert geen writes:
class AddCustomerIdIndexToOrders < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :orders, :customer_id, algorithm: :concurrently
end
end
Die disable_ddl_transaction! is cruciaal. Concurrent index builds kunnen niet binnen een transactie draaien, dus moet je Rails vertellen om deze migratie niet in een transactie te wrappen.
Een addertje: als de migratie halverwege faalt, blijft er een ongeldige index achter. Check altijd op achterblijvers:
SELECT indexrelid::regclass, indisvalid
FROM pg_index
WHERE NOT indisvalid;
Het Expand en Contract Patroon
Een kolom hernoemen lijkt simpel genoeg:
rename_column :users, :email, :email_address
Het probleem? Je draaiende applicatie verwacht nog steeds email. Zodra die migratie draait, explodeert elk request dat users.email probeert te lezen.
De veiligere route kost drie deployments:
Deploy 1: Expand
class AddEmailAddressToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :email_address, :string
end
end
Voeg een callback toe aan je model om beide kolommen in sync te houden:
before_save :sync_email_columns
def sync_email_columns
self.email_address = email if email_changed?
self.email = email_address if email_address_changed?
end
Deploy 2: Migreer
Update je applicatie code om overal email_address te gebruiken. Draai een backfill script om bestaande data te kopiëren. Neem de tijd—er is niks stuk.
Deploy 3: Contract
class RemoveEmailFromUsers < ActiveRecord::Migration[7.1]
def change
remove_column :users, :email, :string
end
end
Ja, drie deployments voor een kolom hernoemen. Maar weet je wat sneller is dan drie zorgvuldige deployments? Niet om 4 uur ‘s nachts een backup moeten terugzetten terwijl je CTO in je nek hijgt.
Lock Timeouts: Je Vangnet
Zelfs met alle voorzorgsmaatregelen glippen er dingen doorheen. Een migratie die prima werkt op staging met 10.000 rijen kan stikken op productie met 10 miljoen.
Stel een lock timeout in bij je migraties:
class SafeMigration < ActiveRecord::Migration[7.1]
def change
execute "SET lock_timeout = '5s'"
# je migratie hier
end
end
Als de migratie niet binnen 5 seconden een lock kan krijgen, faalt hij in plaats van eindeloos te wachten (terwijl er een rij geblokkeerde queries achter hem opstapelt).
Nog beter: configureer dit globaal in je database.yml voor migraties:
production:
lock_timeout: 5000
statement_timeout: 60000
De Strong Migrations Gem
Andrew Kane’s strong_migrations gem vangt gevaarlijke migraties af voordat ze productie raken. Hij schreeuwt tegen je voor:
- Indexes toevoegen zonder
CONCURRENTLY - Kolommen toevoegen met default values (voor Postgres 11 herschreef dit de hele tabel)
- Kolom types wijzigen op manieren die tabellen locken
- Kolommen verwijderen die je code mogelijk nog gebruikt
Installeer hem. Configureer hem. Luister naar hem.
# Gemfile
gem "strong_migrations"
De gem houdt je niet tegen om riskante dingen te doen—soms moet dat echt. Maar het dwingt je om het risico expliciet te erkennen, wat betekent dat je die migratie eerder zelf om 3 uur ‘s nachts inplant, met een kop koffie, in plaats van wakker geschud te worden door PagerDuty.
Grote Tabellen Backfillen
Een nieuwe kolom die gevuld moet worden met bestaande data? De verleidelijke aanpak:
User.update_all(full_name: "#{first_name} #{last_name}")
Op een grote tabel is dit één gigantische transactie die rijen lockt en je transaction log laat opzwellen.
Doe het in batches:
User.in_batches(of: 1000) do |batch|
batch.update_all("full_name = first_name || ' ' || last_name")
sleep(0.1) # even ademen
end
Die sleep lijkt misschien raar, maar het geeft je database ruimte om normaal verkeer te handelen tussen batches door. Zonder dat voer je in feite een denial-of-service aanval tegen jezelf uit.
Voor echt grote tabellen kun je overwegen om backfills als background jobs te draaien in plaats van migraties. Migraties moeten snel en reversible zijn. Een backfill die 6 uur duurt is geen van beide.
Migraties Testen Tegen Productie Data
Als je team feature flags gebruikt om rollouts te beheren, kun je die combineren met migraties: deploy eerst de migratie, schakel dan geleidelijk de code in die van het nieuwe schema afhangt.
Je staging database heeft 500 gebruikers. Productie heeft er 2 miljoen. Die migratie die 0.3 seconden duurt op staging? Die kan 45 minuten duren op productie.
Opties:
-
Geanonimiseerde productie dumps: Kopieer productie data structuur en volume naar staging, met gevoelige velden geschrobd. Duur qua storage en onderhoud, maar accuraat.
-
Query plans: Check voor het draaien het query plan van de migratie op productie (read-only).
EXPLAIN ANALYZElaat zien of Postgres een full table scan plant. -
Vermenigvuldigingsschattingen: Als staging 500 rijen heeft en de migratie 0.3 seconden duurt, kan productie met 2 miljoen rijen grofweg 1.200 seconden (20 minuten) duren. Deze wiskunde is wildly imprecies, maar beter dan geen schatting.
Als Het Toch Misgaat
Soms, ondanks alle voorzorgsmaatregelen, veroorzaakt een migratie alsnog problemen. Heb een rollback plan klaar:
- Kan de migratie worden teruggedraaid? Rails maakt dit makkelijk voor de meeste DDL changes.
- Als je een kolom toevoegt, kun je hem snel droppen?
- Als je een kolom verwijdert, heb je de data ergens gebackupt?
- Wie heeft productie database toegang om 3 uur ‘s nachts? (Schrijf hun telefoonnummers op voordat je ze nodig hebt.)
Documenteer je runbook. Het moment dat je in paniek bent is het slechtste moment om rollback procedures uit te zoeken.
De Cultuur Shift
Zero-downtime deployments vereisen meer dan technische trucjes. Ze vereisen geduld. Die kolom hernoemen kost drie deploys in plaats van één. Die index toevoeging moet een aparte migratie zijn van de feature code.
Dit voelt eerst langzamer. Het ís eerst langzamer. Maar je haalt die tijd in—en meer—door geen productie incidenten te hebben. Door niet om 3 uur ‘s nachts te debuggen. Door geen boze klanten te hebben.
Slaap lekker. Je database komt er wel.
Veelgestelde Vragen
Kan ik zero-downtime migraties draaien op MySQL, of werkt dit alleen op PostgreSQL?
De meeste technieken in dit artikel werken op zowel PostgreSQL als MySQL. Concurrent index creatie (CONCURRENTLY) is PostgreSQL-specifiek, maar MySQL 8.0+ ondersteunt online DDL voor de meeste index-operaties met ALGORITHM=INPLACE. Het expand-en-contract patroon en lock timeouts zijn op beide databases toepasbaar.
Hoe weet ik of een migratie veilig is om zonder downtime te draaien?
Gebruik de strong_migrations gem als eerste verdedigingslinie — die markeert automatisch bekende gevaarlijke patronen. Test daarnaast de migratie tegen een dataset die qua volume overeenkomt met productie, en controleer het query plan met EXPLAIN ANALYZE. Als de migratie een exclusieve lock op een grote tabel vereist, is hij niet veilig zonder aanpassing.
Moet ik migraties draaien tijdens deployment of apart?
Voor zero-downtime deploys draai je migraties vóór het deployen van de nieuwe applicatiecode. Zo blijft de oude code draaien tegen het bijgewerkte schema. Je migratie moet backward-compatible zijn met de huidige code — het expand-en-contract patroon garandeert dit.
Wat is de veiligste manier om een kolom uit een productietabel te verwijderen?
Stop eerst alle applicatiecode die de kolom leest of schrijft (deploy die wijziging). Wacht dan tot actieve queries zijn afgerond. Draai daarna de migratie om de kolom te droppen. Rails’ ignored_columns instelling laat je ActiveRecord vertellen om een kolom niet meer te gebruiken voordat hij fysiek verwijderd is.
Hulp nodig bij het moderniseren van je deployment pipeline? TTB Software is gespecialiseerd in Rails DevOps en zero-downtime deployments. We doen dit al negentien jaar—we hebben alle fouten gemaakt zodat jij dat niet hoeft.
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