Naar de inhoud

Blog ·

De migratie die nooit gedraaid heeft

Een migrator die succes meldt en toch een bestand overslaat is erger dan een die crasht. Hoe tijdstempelvolgorde stil migraties laat vallen.

Door Provibr

Een schemamigrator die crasht is een goede middag. Je krijgt een stacktrace, een regelnummer en een database die nog in een toestand verkeert die je begrijpt.

De slechte middag is die waarop de migrator afdrukt dat het schema bij is, met exitcode nul eindigt, en ongelijk heeft. Wij hebben die middag meer dan eens meegemaakt, om meer dan één reden, en de ergste keer eindigde het met productiecode die een kolom las die niet bestond.

Dit is wat we geleerd hebben, opgeschreven voor iedereen die Postgres-migraties uit een bestandsmap draait, ongeacht welk gereedschap die bestanden gemaakt heeft.

De beslissing valt op één getal

De meeste bestandsgebaseerde migrators houden een tabel bij van wat er is toegepast. De onze ook: een schema, een tabel, een rij per migratie met een hash en een aanmaaktijdstempel.

De vergissing is aannemen dat die tabel een verzameling is die de migrator raadpleegt. Dat is hij niet. Lees je de code, dan vind je een query die precies één rij selecteert, de nieuwste op tijdstempel, en daarna één enkele vergelijking per migratiebestand: draai hem als het laatst toegepaste tijdstempel lager is dan het tijdstempel van dit bestand.

Daar volgen meteen drie dingen uit, en geen ervan zit in iemands mentale model.

De hash speelt geen rol in de beslissing. Hij wordt geschreven wanneer een migratie draait en daarna nooit meer gelezen om iets te bepalen. Een hash in de tabel die niet meer overeenkomt met enig bestand op schijf betekent meestal dat dat bestand na toepassing nog bewerkt is, wat slordig is maar onschuldig. Het is niet het gereedschap dat iets opmerkt.

Alleen de nieuwste rij telt. Niet de verzameling toegepaste bestanden, niet het aantal. Eén rij.

De volgorde loopt op tijdstempel, niet op bestandsnaam. De nummers vooraan je bestandsnamen zijn voor mensen. Legt je gereedschap een volgordewaarde vast in een manifest, dan is die waarde de waarheid en is het nummer versiering.

Zet dat bij elkaar en je krijgt de eigenschap die middagen kost: een migratie waarvan het tijdstempel niet groter is dan het nieuwste toegepaste tijdstempel zal op die database nooit meer draaien. Niet bij deze deploy, niet bij de volgende, niet na tien pogingen. Er komt geen fout, want vanuit het gezichtspunt van het gereedschap is er niets mis.

Hoe je er werkelijk een krijgt

Het klinkt als iets dat een zorgvuldig team niet kan overkomen. Zo is het ons twee keer overkomen, op manieren die niets met slordigheid te maken hadden.

Twee branches, hetzelfde nummer. Twee mensen voegen ieder tegelijk een migratie toe. Beide gereedschappen delen het volgende vrije nummer uit, beide krijgen hetzelfde, beide schrijven een tijdstempel. De branch die als tweede samengevoegd wordt krijgt het volgende nummer, wat de voor de hand liggende oplossing is en niet de hele oplossing: is zijn tijdstempel nog steeds lager dan het al toegepaste, dan veranderde het hernoemen de bestandsnaam en niet de beslissing. De migratie wordt overgeslagen. Het symptoom was bij ons een enum-type waar eenvoudigweg een waarde aan ontbrak, op een database waar elk ander object aanwezig en correct was.

Met de hand gezette tijdstempels die niet op volgorde stonden. De onze zijn ronde getallen die met de hand gezet worden in plaats van door een echte klok, wat ze leesbaar maakt en makkelijk fout te doen. Eén migratie kreeg een stempel vroeger dan twee die al toegepast waren. De migrator meldde succes. In productie zou dat een typefout op elke integratierij zijn geweest, en dat is niet het soort ding dat je van een klant wilt horen.

Beide gevallen delen een vorm die het benoemen waard is. De boekhouding hangt aan een volgordewaarde, niet aan identiteit. Een grootboek dat op identiteit werkt ("heb ik deze migratie toegepast?") kan niets overslaan, in welke volgorde dingen ook binnenkomen. Een grootboek dat op volgorde werkt ("hoe ver ben ik gekomen?") kan dat wel, en het is een ontwerp dat de meeste migrators delen omdat het het gewone pad terugbrengt tot het lezen van één rij.

De teller is niet de controle

Na het eerste incident voegden we de voor de hand liggende bewaking toe: kijk na het migreren naar het aantal rijen in de migratietabel en bevestig dat het gestegen is.

Die bewaking is nodig en niet genoeg, wat we ontdekten door hem te zien slagen terwijl juist datgene ontbrak wat we nodig hadden. Een migratieronde bracht het aantal rijen van achtennegentig naar honderdvier. Zes migraties gingen erin. Die ene waar het ons om ging zat er niet bij. De teller ging omhoog, alles meldde succes, en elke ingelogde pagina in het product gaf een 500 op een kolom die niet bestond.

Een aantal vertelt je niets over welke migraties gedraaid hebben. Wil je weten of een object bestaat, dan is de enige eerlijke controle de database vragen of dat object bestaat: de tabellen opsommen, de tabel beschrijven, de enum-waarden opvragen. Wij vergelijken de tabellijst van de omgeving die we net gemigreerd hebben met die van de omgeving die we vertrouwen.

Er is een variant van deze mislukking die niets met tijdstempels te maken heeft en dezelfde groene uitvoer geeft. Onze migrator reist mee in een van onze service-images. Iemand herbouwde alleen de andere service, draaide de migratie vanuit het verouderde image, en kreeg een opgewekt "het schema is bij" van een container die nog nooit van de twee migraties in kwestie gehoord had. Het gereedschap loog niet. Het beantwoordde een vraag over de bestanden die het had, en het had oude bestanden.

Alles of niets, in één transactie

Eén detail in de uitvoering van onze migrator is het weten waard omdat het verandert hoe je herstelt: elke openstaande migratie draait binnen één enkele transactie, met de boekhoudkundige invoeging voor elk van hen erin.

Dat is goed nieuws en een beperking. Het goede nieuws is dat een mislukking halverwege een reeks van zeven je achterlaat met nul van de zeven toegepast, in plaats van met drieënhalf. Er is geen half gemigreerde toestand om over na te denken, en dat is precies wat je wilt op het moment dat je het minst in staat bent om na te denken.

De beperking is dat één opdracht die Postgres niet binnen een transactieblok kan uitvoeren de hele reeks meesleurt, en dat een zeer lange migratie sloten vasthoudt voor de hele reeks in plaats van voor zijn eigen duur. Geen van beide heeft ons tot nu toe gebeten. Van allebei wil je gelezen hebben voordat het gebeurt.

Er een repareren zonder de volgende erger te maken

Stel dat je hem vindt: een migratie in je map die deze database nooit gedraaid heeft. Het instinct is het manifest te repareren, het tijdstempel te verlagen of te verhogen tot het gereedschap hem oppakt, en opnieuw te draaien.

Verlaag hem niet, en wees voorzichtig met verhogen.

De reden is dezelfde regel die het probleem veroorzaakte. Verlaag je een tijdstempel op een migratie die elders al toegepast is, dan heb je niets laten draaien; je hebt een bestand gemaakt waarvan de stempel nu onder de hoogwaterlijn ligt op de databases waar hij wél gedraaid heeft, en de volgende migratie die je schrijft met een stempel tussen die twee wordt daar overgeslagen. Je repareerde de ene omgeving en scherpte de volgende mislukking in een andere.

Wat wel werkt is de SQL zelf toepassen, in één transactie, en er daarna voor zorgen dat de boekhouding de werkelijkheid weergeeft. De migrator krijgt het niet voor je gedaan, want van zijn kant valt er niets te doen.

Wat we nu doen

Vijf regels, allemaal goedkoop, allemaal opgeschreven omdat we ze ieder één keer niet gevolgd hebben.

  1. Deployen en migreren zijn één handeling, geen twee. Het gat ertussen is een venster waarin nieuwe code een oud schema leest. Het onze kostte ons een beheerpagina voor elke beheerder en een facturatietaak die een hele middag stil faalde op kolommen die er nog niet waren.
  2. Bouw alles voordat je migreert, niet alleen de service die je gewijzigd hebt. De migrator reist in een image, en een image dat je niet herbouwd hebt bevat de migraties die je niet toegevoegd hebt.
  3. Controleer de volgordewaarden, niet de bestandsnamen, telkens wanneer twee mensen tegelijk hebben zitten werken. Een bestand hernoemen verandert niets aan wat het gereedschap vergelijkt.
  4. Controleer het object, niet de teller. Vraag na een migratie die een kolom toevoegt om die kolom. Dat kost één commando en het is de enige controle die niet vacuüm kan slagen.
  5. Verlaag nooit een volgordewaarde die ergens al toegepast is. Het lijkt een plaatselijke reparatie en het is een landmijn op afstand.

De ongemakkelijke draad door dit alles heen is dat het gereedschap zich elke keer correct gedroeg. Het crashte nooit, beschadigde nooit iets, loog nooit over wat het gedaan had. Het beantwoordde een nauwere vraag dan die wij dachten te stellen, en drukte een zin af die over die nauwere vraag waar was.

Dat is de klasse van mislukkingen waartegen het loont gewoontes te bouwen, in migraties en overal elders. Gereedschap dat luidruchtig faalt leert je wat het doet. Gereedschap dat nauw slaagt leert je niets tot de dag dat het ertoe doet.

Begin met één host en één agent

Maak een account aan, installeer de agent met één commando en koppel je eerste Proxmox-host. De proefperiode loopt 14 dagen en vraagt geen betaalgegevens.