Hoe we deploys terugbrachten naar nul 502-fouten
Elke keer dat we de backend van Mythex deployden, faalden een handvol verzoeken met 502 Bad Gateway. Niet veel, maar een 502 tijdens een deploy betekent dat de klik van iemand niets deed, of een build die hij volgde even stilviel. We herleidden het tot twee afzonderlijke problemen: een zelfherstellende watchdog die tegen onze deploys vocht, en een kloof tussen het moment waarop een server stopte met luisteren en het moment waarop de load balancer stopte met hem verkeer te sturen. Over drie opeenvolgende productiedeploys na de fixes ging het aantal van 10 naar 6 naar 0.
De opzet
Onze API en orchestrator draaien als containers op een kleine groep servers achter een AWS Application Load Balancer (ALB). Een deploy werkt één server tegelijk bij: hij trekt de oude containers leeg, start nieuwe en gaat door. Een watchdog op elke server controleert of de containers draaien en herstart alles wat niet draait. Hij bestaat zodat een gecrashed proces om 3 uur 's nachts terugkomt zonder dat iemand wakker moet worden.
Beide zijn op zichzelf redelijk. Samen werkten ze niet.
Probleem 1: de watchdog racete de deploy
De watchdog liep elke minuut en „herstelde” bij één mislukte health check. Een deploy trekt diensten bewust tot 150 seconden leeg, en tijdens die tijd hoort een health check juist te falen. Dus vuurde de watchdog tijdens elke deploy.
Meestal verloor hij de race zonder schade. Eén keer won hij: hij herschreef het compose-bestand (de beschrijving van welke containers moeten draaien) midden in een deploy, en liet een server achter zonder gateway-container, terwijl de deployjob succes meldde.
De oplossingen:
- Eén lock. De deploy neemt nu hetzelfde bestandslot als het bootstrapscript van de server, zodat de watchdog en een deploy nooit tegelijk kunnen lopen.
- Vier strikes, niet één. De watchdog heeft vier opeenvolgende mislukte checks nodig voordat hij ingrijpt. Een dienst die wordt leeggetrokken lijkt niet langer dood.
- Eén definitie. Het compose-bestand heeft nu precies één bron, die naar de servers wordt gepubliceerd, in plaats van kopieën die uit de pas konden lopen.
- Verifiëren, niet vertrouwen. De deployjob slaagt niet meer omdat een commando iets teruggaf. Hij controleert de API, orchestrator, gateway en publicatieworker van elke server voordat hij kan slagen.
Probleem 2: de server vertrok voor het verkeer
Het tweede probleem was stiller. Wanneer een deploy een server leegtrekt, begint de server zijn health-endpoint 503 te geven zodat de load balancer hem uit de rotatie haalt. Bij ons gebeurde dat 5 seconden lang, waarna de server afsloot.
Maar de ALB controleert de health maar af en toe, en heeft meerdere opeenvolgende mislukte checks nodig voordat hij stopt met routeren naar een target: tot ongeveer 60 seconden van begin tot eind. Dus bleef de load balancer ongeveer 55 seconden echte verzoeken sturen naar een server die al was gestopt met luisteren. Dat waren onze 502's.
De oplossing was om de twee tijdlijnen op elkaar af te stemmen:
- De target group controleert nu elke 5 seconden en markeert een target als ongezond na 2 fouten, ongeveer 10 seconden om het te merken.
- De drain blijft echt verkeer bedienen gedurende 15 seconden nadat hij health-checks laat falen, zodat er altijd overlap is.
- Deze instellingen staan in het script dat de infrastructuur inricht, niet in een console die iemand ooit eenmaal heeft aangeklikt.
De getallen op elkaar laten aansluiten
Timeouts als deze zijn afhankelijk van elkaar. De drain moet langer duren dan de tijd die de balancer nodig heeft om het te merken; de drain plus afsluiting moet klaar zijn voordat de container hard wordt gestopt. Het is makkelijk dat iemand over zes maanden één getal wijzigt en de bug stilletjes terugbrengt.
Dus hebben we die onderlinge relaties als tests vastgelegd: de drain moet langer zijn dan het health-checkinterval maal de faaldrempel; de tijd om weer gezond te worden moet kort blijven; de hele drain moet passen binnen de stopvertragingstermijn van de container. Verander je één getal zonder de andere, dan faalt de CI. We hebben de tests zelf mutatie-getest: we veranderden elk getal naar een verkeerde waarde en controleerden dat een test het opmerkte.
Een deploy meten is makkelijk verkeerd
Onze eerste meting zei dat een schone deploy ongeveer 3% kapot was. Dat klopte niet. We telden elke niet-200-respons, en een 503 van een health-endpoint tijdens een drain is geen fout: het is de drain die zijn werk doet, en de balancer vertelt om die server niet te kiezen terwijl hij nog echte verzoeken bedient.
Wat gebruikers voelen is een 502: het verzoek bereikte niets. Alleen die tellen gaf het echte beeld: 10, dan 6, dan 0 over drie productiedeploys.
Wat we eruit haalden
- Zelfherstel moet weten van geplande downtime. Een watchdog die geen deploy van een crash kan onderscheiden, zal uiteindelijk tegen de deploy vechten.
- Leegtrekken is een handdruk. De server moet blijven bedienen totdat de load balancer aantoonbaar is gestopt met verkeer sturen, en die timing wordt op de balancer ingesteld, niet op de server.
- Leg invarianten vast als tests. Operationele getallen lopen uit de pas. Tests die ze onderling relateren, niet.
- Bepaal wat je meet voordat je meet. „Fouten” was de verkeerde maatstaf; „verzoeken die niets bereikten” was de juiste.
Als je een app op Mythex publiceert, is dit voor jou niet zichtbaar, en dat is het punt. Verwant: hoe je gepubliceerde app online blijft terwijl je hem opnieuw publiceert.