35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Database Migraties Die Je Niet om 3 Uur 's Nachts Wakker Maken

Database Migraties Die Je Niet om 3 Uur 's Nachts Wakker Maken

Roger Heykoop
DevOps, Ruby on Rails
Praktische technieken voor zero-downtime Rails database migraties. Stop met tabellen locken, begin met doorslapen.

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:

  1. Geanonimiseerde productie dumps: Kopieer productie data structuur en volume naar staging, met gevoelige velden geschrobd. Duur qua storage en onderhoud, maar accuraat.

  2. Query plans: Check voor het draaien het query plan van de migratie op productie (read-only). EXPLAIN ANALYZE laat zien of Postgres een full table scan plant.

  3. 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.

#rails #postgresql #database #migraties #devops #deployment #zero-downtime
R

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 Touch

Share this article

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