Blog ·
Waarom een Proxmox-200 geen uitkomst is
Proxmox beantwoordt een kloonverzoek met een UPID en gaat aan de slag. Hoe je dat kenmerk leest, eerlijk pollt, en klaar van geslaagd onderscheidt.
Door Provibr
Je POST een kloonverzoek naar een Proxmox-node. Je krijgt HTTP 200 terug en een tekenreeks die op ruis lijkt. Op dat ogenblik is precies één ding waar: het verzoek is aangenomen. Er is geen schijf gekopieerd, er bestaat geen machine, en er is ook nog niets mislukt.
Bijna elke muterende aanroep in de Proxmox-API werkt zo, en het is een volstrekt redelijk ontwerp voor bewerkingen die minuten duren. Het wordt pas een probleem wanneer de aanroepende code de 200 als een uitkomst behandelt, en dat is het standaardgedrag van elke HTTP-client die ooit geschreven is.
Zo lezen wij die taken, en dit zijn de delen die scherper bleken dan ze eruitzien.
De tekenreeks is een kenmerk, en hij heeft structuur
Wat je terugkrijgt is een UPID: een met dubbelepunten gescheiden kenmerk dat één draaiende taak benoemt. Hij draagt, naast andere velden, de node waarop de taak draait, het soort taak, het ding waarop hij werkt, en de gebruiker die hem gestart heeft.
Drie daarvan zijn dragend.
De node is degene die je om de status moet vragen. Een taak hoort bij de node die hem draait, en een clusterbrede vraag is niet dezelfde vraag. Neem de node uit het kenmerk en niet uit de variabele die je toevallig bij de hand had, want die twee kunnen verschillen en het geval waarin ze verschillen is precies het geval dat je niet getest hebt.
Het soort vertelt je wat er werkelijk gestart is, en dat is niet altijd wat je bij naam gevraagd hebt. Een backup die via het node-endpoint gestart wordt komt terug met tasksoort vzdump, niet met iets dat de vorm van een gast heeft.
Het doelwit is het gastkenmerk bij gastbewerkingen, en het kan leeg zijn. Dat is geen beschadiging. Een taak mag legitiem over een node gaan in plaats van over een gast, dus code die dit veld ontleedt moet "geen gast" teruggeven in plaats van een uitzondering te gooien of naar nul terug te vallen.
Onze parser is bewust smal in wat hij afwijst: hij eist het voorvoegsel, hij eist minstens acht met dubbelepunten gescheiden velden, en hij eist dat de node en het soort niet leeg zijn. Al het andere mag ontbreken, want het formaat moet blijven werken voor tasksoorten die we nooit gezien hebben.
Klaar is niet hetzelfde als geslaagd
Vraag een node om de status van een taak en je krijgt twee velden die twee verschillende vragen beantwoorden. Het ene zegt of de taak nog draait of gestopt is. Het andere, alleen aanwezig zodra hij gestopt is, zegt OK of draagt een fouttekst.
Dat als twee losse uitspraken schrijven in plaats van als één is geen muggenzifterij. Eén enkele isDone() die ze op één hoop gooit levert de fout op waarbij een mislukte kloon als een voltooide kloon gerapporteerd wordt, en de machine die niet bestaat een rij in je database krijgt. Wij hebben twee functies: de ene vraagt of hij gestopt is, de andere vraagt of hij gestopt is en de afsluitstatus precies OK luidt. Al het andere is een mislukking, inclusief afsluitstatussen die we nooit gezien hebben, want een status die we niet herkennen is geen toestemming om succes aan te nemen.
Waar de taak leeft hoort bij de API en is geen detail
Backups hebben ons dit geleerd. Het is natuurlijk om te verwachten dat een gastbewerking aan de gast hangt: iets onder het eigen pad van de virtuele machine. Zo'n endpoint bestaat niet. Een backup wordt gestart op de node, met het gastkenmerk als parameter.
Dat is geen eigenaardigheid waar je omheen routeert. Het verandert wat je moet weten voordat je kunt beginnen: welke node, welke opslag op die node, en of die opslag überhaupt backups mag bevatten. Een opslag die alleen schijfimages aanneemt aanvaardt je verzoek opgewekt en faalt binnen in de taak, wat bij een klant naar boven komt als een foutmelding uit een programma waar hij nooit van gehoord heeft. Dus filteren wij de doellijst op wat de opslag verklaart te kunnen bevatten, en laten we elke opslag vallen die wel vermeld staat maar niet antwoordt, voordat we iemand een keuze voorleggen.
Dezelfde regel geldt omgekeerd voor het plaatsen van een schijf. Hetzelfde idee, tegenovergesteld inhoudstype, en het is de moeite waard om ze als twee expliciete filters te houden in plaats van als één slim gedeeld filter.
Pollen is een ritme, geen interval
Terwijl de taak draait moet je herhaaldelijk vragen. De naïeve keuzes zijn allebei slecht: pol snel en eindeloos en je beukt een uur lang op de beheer-API van iemand anders, pol traag en elke snelle bewerking voelt stroperig.
Ons compromis is drie snelle pogingen met een seconde ertussen, en daarna elke drie seconden. Een machine starten is klaar binnen het snelle venster en voelt onmiddellijk. Een backup van een uur kost een verzoek per drie seconden, en dat is niets.
De lus eindigt op precies drie manieren, en alle drie zijn expliciet: de taak meldt dat hij gestopt is, de deadline van het commando verstrijkt, of het hele commando wordt afgebroken. Er is geen vierde tak en geen pad waarop de lus eenvoudigweg doorloopt, en dat is een eigenschap die je met opzet hoort te regelen in plaats van erop te hopen. Wat een deadline wel en niet stopt is een onderwerp op zich; hier volstaat het te zeggen dat de pollus een manier moet hebben om te verliezen.
De meeste taken vertellen je niet hoe ver ze zijn
Dit is het deel dat iedereen die een voortgangsbalk bouwt teleurstelt. De taakstatus geeft je draaiend of gestopt. Hij geeft je geen percentage.
Sommige taken schrijven voortgang in hun log, als tekst. Een kloon zendt regels uit met een overgedragen aantal en een percentage tussen haakjes. Voor die taken lezen we dus de staart van het log en nemen we het laatste percentage dat we erin vinden. Een percentage uit een logregel vissen is niet elegant, en het is de enige plek waar het getal bestaat.
Voor al het andere tonen we een schatting, en hier doet de ontwerpkeuze er meer toe dan de formule. De onze nadert asymptotisch negentig procent vanuit een verwachte duur, zodat de balk altijd beweegt en het einde nooit haalt. Een balk die op 100 procent staat terwijl er niets gebeurt is erger dan geen balk, want hij vertelt de kijker dat je klaar bent, en spreekt zichzelf daarna tegen.
De verwachte duur die die curve voedt is een vormparameter, geen deadline. De onze zegt dat een backup meestal zo'n tien minuten kost en een terugzetactie ongeveer twintig, met als redenering dat terugzetten het archief leest en de schijven schrijft en dus ruwweg twee keer zoveel bytes verplaatst. Duurt een backup langer dan de schatting, dan breekt er niets; de curve blijft dan beleefd onder de lijn.
De fout staat in het log, niet in de status
Wanneer een taak mislukt is de afsluitstatus vaak vrijwel nutteloos. "unable to create VM" is een categorie, geen oorzaak. De oorzaak staat in de laatste paar regels van het tasklog.
Dus wordt een foutrapport samengesteld: de afsluitstatus, plus de staart van het log met de voortgangsregels eruit gefilterd, begrensd tot een handvol regels en een paar honderd tekens. Dat filteren van de voortgangsregels doet er meer toe dan het klinkt, want een mislukkende kloon kan eindigen met twintig regels overdrachtspercentages en de werkelijke reden daarboven.
Eén regel over die tekst, waar we ons strikt aan houden: het is een citaat. Het is wat de hypervisor zei, en het gaat ongewijzigd onze administratie in en het scherm op. We vertalen het niet, we poetsen het niet op, en we herschrijven het niet tot iets vriendelijkers. De zin eromheen is van ons en wordt voor de lezer vertaald; het geciteerde deel blijft precies zoals de andere kant het zei, want het is de enige tekenreeks in de hele uitwisseling waar een supportmedewerker op kan zoeken.
Vouw je eigen woorden niet in de zin van de machine
De laatste les is degene die we niet verwachtten te leren van een taakpoller.
Onze wachtlus nam vroeger een vrij in te vullen opschrift voor de bewerking en vouwde dat in drie zinnen: iets is bezig, iets is klaar, iets is mislukt. Het werkte, tot iemand merkte dat verschillende van die opschriften in onze eigen kantoortaal geschreven waren in plaats van in het Engels. Klanten lazen halfvertaalde zinnen in hun voortgangsbalken, en wanneer een bewerking mislukte werd die halfvertaalde zin blijvend in het foutveld opgeslagen.
De oplossing was niet de opschriften corrigeren. Het was de mogelijkheid wegnemen: de bewerking is nu een opsomming, en elk geval draagt kenmerken voor "bezig", "klaar" en "mislukt" in plaats van woorden. Er is geen tekst meer over om verkeerd te doen, en de zin wordt helemaal aan het eind in de taal van de lezer opgebouwd. Die catalogi eerlijk houden blijkt een vak op zich.
De checklist
Voor elke API die een mutatie beantwoordt met een kenmerk in plaats van met een uitkomst:
- Ontleed het kenmerk, sla het niet alleen op. Het vertelt je waar je moet vragen, en waar is niet altijd waar je gevraagd hebt.
- Scheid klaar van geslaagd, in het typesysteem als het kan.
- Behandel een niet-herkende eindtoestand als mislukking, nooit als succes.
- Geef de pollus een ritme en een gegarandeerde manier om te eindigen.
- Toon de voortgang die je werkelijk hebt, en een eerlijke schatting die je nooit voor echte voortgang kunt aanzien.
- Citeer de andere kant letterlijk in fouten, en bouw je eigen zin om het citaat heen in plaats van erin.
Geen van deze is moeilijk. Alle zes zijn dingen die we eerst fout gedaan hebben.