Alarmer, der udløses én gang, ikke én gang pr. server
En af vores driftsalarmer, »lav kredit« for en builders publicerede app, er beregnet til at nå os højst én gang i døgnet. En eftermiddag kom den fem gange. Årsagen: hver kopi af vores API huskede i sin egen hukommelse, hvilke alarmer den allerede havde sendt, så to servere sendte den to gange, og hver deploy nulstillede hukommelsen og lod dem sende den igen. Det samme mønster fik et baggrundsjob til at udføre sit arbejde to gange. Begge koordinerer nu via en delt store, og begge fejler på den sikre side, hvis lageret er utilgængeligt.
Symptomet
Mythex sender vores teamalarmer, når noget kræver et kig: en tjeneste er nede, et job står stille, eller en builders kreditsaldo er ved at løbe tør, mens dens app er publiceret. Hver alarm har et dedupliceringsvindue, altså en periode, hvor den samme alarm kun sendes én gang. For alarmen om lave credits er vinduet 24 timer.
Fem kopier af den samme alarm på én eftermiddag betød, at vinduet ikke holdt. Værre: de kom i par, cirka 11 minutter fra hinanden.
Hvorfor det skete
Vores alarmpakke holdt deduplikeringen i hukommelsen: en tabel med »alarmnøgle → hvornår den sidst blev sendt«, inde i den kørende proces. Det virker, når der er én proces, der aldrig genstarter. Produktion har hverken det ene eller det andet:
- To API-replikaer. Produktion kører to kopier af API'et. Hver havde sin egen tabel, så hver sendte alarmen én gang. Det er parrene.
- Deploys. Hver deploy erstatter begge replikaer med friske processer og tomme tabeller. Efter en deploy gik alarmen ud igen, som om den aldrig var sendt.
De ca. 11 minutter mellem de to alarmer i hvert par kom fra jobbet, der udløser alarmen. Det kører på begge replikaer og tager et lock, så replikaerne vinder det på skift, og hver af dem udløste alarmen, når turen var deres.
Der lå en anden, mere stille fejl i samme kode. For at stoppe tabellen i at vokse for evigt rensede den sig selv, når den havde 500 nøgler, ved at slette alle poster ældre end 15 minutter, uanset deres vindue. Under belastning kunne en 24-timers deduplikering holde i en kvart time.
Rettelsen
Én delt post pr. alarm
Før API'et sender en alarm, claimer det nu alarmens deduplikeringsnøgle i Redis, en delt hukommelsesbaseret store, som begge replikaer taler med, og som ikke genstarter, når API'et gør. Claimet er en enkelt operation: »sæt denne nøgle kun, hvis den ikke findes, og lad den udløbe efter vinduet«. Den replika, der claimer først, sender alarmen; den anden finder nøglen optaget og forbliver stille. En ny proces efter en deploy finder stadig nøglen.
Nøglen og vinduet kommer fra den samme regel, som alarmpakken bruger, nu delt mellem de to lag, så de to aldrig kan være uenige om, hvad der tæller som »den samme alarm«.
Fejl åbent, ikke lukket
Den vigtigste beslutning var, hvad der sker, når Redis ikke kan nås. Vi valgte at sende alarmen alligevel. Hvis Redis er nede, eller ikke svarer inden for 1,5 sekund, går alarmen ud. En dublet af en alarm er støj. En tabt alarm betyder, at nogen ikke bliver informeret.
De alvorligste alarmer, P0, holdes slet ikke tilbage af deduplikering.
Hver post beholder sit eget vindue
I alarmpakkens egen deduplikering i hukommelsen gemmer hver post nu, hvornår dens vindue slutter, og oprensning fjerner kun poster, hvis vindue er overstået. En 24-timers nøgle bliver i 24 timer, uanset hvor mange andre alarmer der kommer.
Den samme fejl et andet sted
Samme mønster havde også fordoblet et stykke baggrundsarbejde: et afstemningsjob, der holder vores registrering af hvert workspaces status på linje med vores sandkasse-udbyders. Hvert gennemløb lister hver sandkasse på kontoen (tusindvis i produktion, ca. fem sekunder) plus hvert kørende eller pauset projekt. Begge replikaer kørte det hvert andet minut.
Dets skrivninger er idempotente, det vil sige, at det at køre dem to gange giver det samme resultat som at køre dem én gang, så intet gik i stykker. Det var bare dobbelt arbejde. Nu claimes hvert to-minutters-vindue i Redis, på samme måde som vores forbrugsmålere allerede claimede deres, og kun den replika, der claimer, kører gennemløbet. Hvis Redis ikke kan nås, kører gennemløbet alligevel: at udføre arbejdet to gange er uskadeligt, at lade være gør ikke.
Sådan testede vi det
Testene modellerer det rigtige setup. De indlæser alarmmodulet to gange, som to separate »replikaer«, peger begge på én delt store, udløser den samme alarm fra hver, og indlæser derefter en tredje frisk kopi for at stå for »efter en deploy« og udløser den igen. Præcis én alarm sendes. Andre tests kontrollerer, at en anden builders alarm stadig går ud, at en P0 sendes fra begge replikaer, og at alarmen stadig sendes, når lageret er nede eller hænger længere end timeout. Oprydningsrettelsen har sin egen test: en 24-timers nøgle overlever 600 andre alarmer, der kommer 20 minutter senere.
For afstemningsjobbet: to replikaer, der tikker samtidig, plus et tredje tik i samme vindue, giver ét listekald.
Det vi lærte
- Tilstand i hukommelsen er pr. proces. Alt, der skal være sandt på tværs af servere, »sendt én gang« eller »kørt én gang«, skal leve et sted, de deler.
- Deploys er genstarter. Tilstand, der kun overlever, indtil den næste genstart, overlever ikke en almindelig arbejdsdag.
- Beslut, hvilken vej der skal fejles. For alarmer skal et nede delt lager betyde flere alarmer, ikke færre.
- Led efter mønsteret, ikke kun forekomsten. Alarmen var det højlydte tilfælde; det fordoblede afstemningsjob var den samme fejl, der stille kostede arbejde.
Intet af dette er synligt inde fra Mythex. For mere om, hvordan vi holder platformen stabil, se hvordan vi fik deploys ned til nul 502-fejl og den manglende swap-fil. Hvis du er ny til ideen om at køre mere end én kopi af en tjeneste, dækker vores guide til hvad en server er det grundlæggende.