Naar de inhoud

Blog ·

Twee bestellingen, één machine

Een dedicated server is precies één keer te verkopen. De hele pool vergrendelen kostte stil een reservering, en de reparatie was per machine vergrendelen.

Door Provibr

Een VPS-plan verkoopt met opzet meer dan er is en niemand ligt er wakker van. Een dedicated machine kan dat niet: er is er één van, en geloven twee bestellingen allebei dat zij hem hebben, dan krijgt één klant op maandagochtend te horen dat het ding waar hij vrijdag voor betaalde van iemand anders is.

De eis is één zin lang. Reserveer vóór neveneffecten, en geef capaciteit na een crash of een mislukking transactioneel weer vrij. Het bouwen ervan leverde twee metingen op die het ontwerp veranderden, en één daarvan kostte ons een machine die er gewoon vrij stond.

De scheidsrechter is een index

De regel dat twee bestellingen nooit dezelfde machine kunnen vasthouden staat in een partiële unieke index op het asset-id, beperkt tot reserveringen in de stand vastgehouden. Niet in de applicatie, niet in een slot dat de code neemt, niet in een commentaarregel.

Dat is bewust dezelfde vorm als de bezettingsindex op rackunits. De applicatielaag levert een leesbare weigering vóór de botsing, en de database beslist wie er werkelijk wint. Maar één van die twee kan gezaghebbend zijn, en dat moet degene zijn die niet te omzeilen is door een codepad dat iemand volgend jaar toevoegt.

Vastgehouden is de enige stand die iets vasthoudt, en dat staat als tabel naast de index en niet als booleaanse kolom, want een boolean naast de status is een tweede waarheid die alleen klopt zolang elke schrijver eraan denkt. Een test toetst de tabel tegen de index.

Er zijn vier standen en twee daarvan zijn met opzet een paar: vastgehouden, omgezet, verlopen, geannuleerd. De verleidelijke vereenvoudiging is één vrijgegeven voor die laatste twee, en die zou de vraag "heeft iemand dit afgeblazen, of is het blijven liggen" onbeantwoordbaar maken, precies de vraag die je krijgt als een klant belt over een bestelling die niet doorging. Een sweep die stil dezelfde stand schrijft als een mens wist het verschil tussen een besluit en een timeout.

De hele pool vergrendelen kostte een reservering

De eerste versie selecteerde de kandidaat-machines en vergrendelde ze in één query met FOR UPDATE SKIP LOCKED, dus met overslaan van wat al vastzat. Dat leest goed en het is fout, en alleen een gelijktijdigheidsproef liet het zien.

Acht gelijktijdige bestellingen op drie geschikte machines horen drie reserveringen op te leveren. Er kwamen er twee. De eerste transactie greep alle drie de rijen, gebruikte er één en hield de andere twee vast tot haar commit; iedereen daarna sloeg alle drie over en vond niets. De pool was niet uitgeput maar gegijzeld.

De reparatie is per kandidaat vergrendelen: pak de eerste, en heeft iemand anders hem, ga door naar de volgende. Dezelfde proef opnieuw: drie reserveringen op drie verschillende machines en vijf eerlijke weigeringen. Twee bestellingen op één machine geven één reservering en één weigering.

Die reparatie brengt haar eigen risico mee, en dat is de lengte van de lus. Bij honderd gelijktijdige bestellingen zou de laatste transactie honderd kandidaten aflopen voordat zij wint, en een lange transactie houdt alle andere op. De scan stopt daarom na vijfentwintig kandidaten, en daarboven is "probeer het zo weer" het eerlijke antwoord.

Let erop dat overslaan en mislukken los geteld worden. Een kandidaat die door iemand anders vastgehouden werd is overgeslagen en dat is geen poging, want er ging niets mis. Een kandidaat die een insert-race verloor is een poging, en dat zijn er hoogstens vijf.

Een uniciteitsschending vergiftigt de transactie

Dit is de valstrik waar de herhaallus omheen gebouwd is, en het is makkelijk om code te schrijven die er correct uitziet en bij de eerste echte botsing omvalt.

Binnen een PostgreSQL-transactie breekt een schending van een unieke sleutel de transactie af. Elke volgende statement wordt dan geweigerd met een andere fout, "de huidige transactie is afgebroken", tot iemand terugrolt. Een lus die de schending vangt en de volgende kandidaat probeert werkt dus helemaal niet: de insert van de tweede kandidaat faalt om een reden die niets met die tweede kandidaat te maken heeft.

Elke poging draait daarom binnen een savepoint, wat in onze querybouwer simpelweg een geneste transactie is. Terugrollen tot het savepoint, de buitenste transactie is weer gezond, volgende kandidaat.

Er hoort een detail bij over hoe de schending gevangen wordt. Onze querybouwer verpakt driverfouten in een eigen fouttype en hangt de echte aan de oorzaak, dus een code toetsen op het bovenste niveau ziet niets. Hij moet de oorzaakketen aflopen, en hij moet ook naar de constraintnaam kijken, want deze tabel heeft twee unieke constraints en die betekenen het tegenovergestelde: de een zegt dat de machine vergeven is, de ander zegt dat deze bestelling al een reservering heeft en hier is hij.

De weigering moet zeggen welke soort het is

De allocator bouwt de kandidatenlijst twee keer, en het verschil tussen die twee uitkomsten is een mededeling aan een klant.

De eerste ronde is de volledige lijst, met een reden bij elke machine die afviel: verkeerde site, verkeerde fabrikant, niet gereed, enzovoort. De tweede ronde is wat er op dat moment werkelijk vast te pakken viel. Leverde de volledige lijst een winnaar op en de vastpakbare niet, dan was er iets te verkopen en was iemand een fractie eerder. Dat is een andere zin dan "er is niets": de een betekent probeer het zo weer, de ander betekent koop meer ijzer. Ze verdienen verschillende knoppen.

De bevroren reden voor de gekozen machine gaat de reserveringsrij in, want een maand later staat elk asset er anders bij en is niet meer te reconstrueren waarom de allocator juist deze pakte. De rij zegt het dan zelf.

Naar de idempotentiesleutel wordt twee keer gevraagd, en die tweede keer is nog iets dat alleen een gelijktijdigheidsproef vond. Vijf gelijktijdige verzoeken met dezelfde sleutel, op vier machines, leverden één reservering en vier weigeringen op, waaronder een van een transactie die niets kon vastpakken en daardoor nooit bij de insert kwam en dus ook nooit bij de herhaaltak, terwijl er op dat moment gewoon een reservering met haar sleutel bestond. Vlak vóór de weigering nog een keer vragen lost dat op, en het werkt omdat elke statement op ons isolatieniveau een verse momentopname neemt, dus een broer die net commit is wél zichtbaar.

De garantie die dit alles overleeft is smal en het opschrijven waard: vijf verzoeken met dezelfde sleutel geven één reservering en vier keer "iemand was sneller". De verliezers zien de winnaar nog niet, want die had op dat moment nog niet gecommit. Er is geen versie hiervan waarin dat anders kan zonder op elkaar te wachten, en wachten maakt van elke drukke minuut een wachtrij.

De reservering wordt de binding

Is de bestelling rond, dan moet de machine van gereserveerd naar verkocht. De voor de hand liggende implementatie geeft de reservering vrij en schrijft dan de dienstbinding, en zij bevat een venster van enkele milliseconden waarin de machine vrij is. Op een drukke dag wordt dat venster gevonden.

De conversie schrijft daarom de binding en zet de reservering op omgezet in één transactie, en de rij blijft vastgehouden tot de binding bestaat. Op elk moment van die transactie ziet een tweede bestelling óf de reservering óf de binding, en allebei sluiten hem uit. Er is geen vrij moment omdat geen enkele statement er een maakt. Dezelfde vorm als het promoveren van een rackreservering tot een plaatsing, één laag hoger.

De sweep is een schoonmaker en geen bewaker

Een reservering verloopt standaard na vijftien minuten, en een worker veegt de verlopen exemplaren elke minuut op.

Dat getal is niet de vier uur die een rackreservering krijgt, en het verschil zit in waar de twee op wachten. Een rackreservering wacht op een mens met een kar: iemand moet naar de zaal lopen, rails monteren, kabels omprikken. Een reservering wacht op een afrekening: betaalscherm, fraudecontrole, bevestiging. Intussen is de duurste voorraad die een hoster heeft onverkoopbaar. Vijftien minuten is ruim voor een afgebroken betaling die iemand hervat, en kort genoeg dat een storm van afgebroken bestellingen de schappen geen halve dag leeg laat staan.

Het belangrijke deel is wáár de deadline afgedwongen wordt. Hij wordt in de conversie getoetst, niet door de sweep. Was de sweep de enige plek, dan zou een reservering die tussen twee rondes in verliep alsnog omzetten, en een bewaker die elke minuut één keer langskomt bewaakt niets. De sweep maakt de administratie kloppend en geeft de machine terug aan de voorraad; dat is schoonmaakwerk.

Hij heeft bovendien geen feature-gate, en dat is een bewuste uitzondering. Er staat een rolloutschakelaar over de hele datacentermodule, en hem hier herhalen zou betekenen dat de module uitzetten machines voor altijd gereserveerd laat staan voor bestellingen die niemand meer kan zien. Een schakelaar die als neveneffect een timeout uitzet is geen schakelaar maar een lek.

Wat wij zouden opschrijven

  1. Zet de scheidsrechter in de database. De applicatie hoort de leesbare weigering te leveren; de index hoort de winnaar aan te wijzen.
  2. Vergrendel per kandidaat. Een verzameling vergrendelen houdt rijen vast die je niet gebruikt, en het symptoom is een gemiste verkoop en geen fout.
  3. Begrens de scan. Per kandidaat vergrendelen ruilt een gegijzelde pool in voor een lange lus, en die lus heeft ook een grens nodig.
  4. Een uniciteitsschending breekt de transactie af. Opnieuw proberen zonder savepoint levert een fout op die het verkeerde ding noemt.
  5. Kijk naar de constraintnaam. Eén tabel heeft vaak meerdere uniciteiten, en ze betekenen voor de gebruiker meestal iets anders.
  6. "Niets beschikbaar" en "iemand was sneller" zijn twee mededelingen. De kandidatenlijst twee keer bouwen is hoe je ze uit elkaar houdt.
  7. Promoveer, geef niet vrij om opnieuw te schrijven. Elke volgorde met een vrij moment erin wordt op een dag gevonden.
  8. Dwing de deadline af op het punt van gebruik. Een periodieke sweep is huishouding, en huishouding is geen regel.

Voor de duidelijkheid over wat dit is: het datacenterlogistiekwerk zit achter een rolloutschakelaar die standaard uitstaat, met een instelling per organisatie erbovenop, dus hiervan is vandaag niets te koop. Twee dingen zijn onbewezen. De verloopsweep is als functie gemeten en niet als draaiende worker. En een rauw verzoek aan de server-actions met de poort dicht is niet geprobeerd.

Uit het platformwerk van Provibr. Datzelfde promoveren in plaats van herschrijven, toegepast op rackunits, staat in de verhuisreservering wordt de plaatsing, en waarom vrije units in een rack niet hetzelfde zijn als verkoopbare capaciteit staat in rackruimte is geen capaciteit.

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.