Blogg

Varsler som utløses én gang, ikke én gang per server

Ett av driftsvarslene våre, «lave kreditter» for en bygger sin publiserte app, skal nå oss høyst én gang per dag. En ettermiddag kom det fem ganger. Årsaken: hver kopi av API-et vårt husket i sitt eget minne hvilke varsler det allerede hadde sendt. Dermed sendte to servere varselet to ganger, og hver deploy tømte minnet og lot dem sende det på nytt. Det samme mønsteret fikk en bakgrunnsjobb til å gjøre jobben sin to ganger. Begge koordinerer nå gjennom en delt lagring, og begge feiler på den trygge siden hvis lagringen ikke er tilgjengelig.

Symptomet

Mythex sender interne varsler når noe trenger et blikk: en tjeneste er nede, en jobb har stoppet, eller en byggers kredittsaldo er lav mens appen er publisert. Hvert varsel har et deduplikasjonsvindu, en periode der det samme varselet bare sendes én gang. For varselet om lave kreditter er vinduet 24 timer.

Fem kopier av samme varsel på én ettermiddag betydde at vinduet ikke holdt. Verre var at de kom i par, omtrent 11 minutter fra hverandre.

Hvorfor det skjedde

Varselpakken vår holdt deduplikasjonen i minnet: en tabell med «varselnøkkel → når det sist ble sendt», inne i den kjørende prosessen. Det fungerer når det er én prosess som aldri starter på nytt. Produksjon har ingen av delene:

  • To API-replikaer. Produksjon kjører to kopier av API-et. Hver hadde sin egen tabell, og sendte derfor varselet én gang. Det er parene.
  • Deploy. Hver deploy erstatter begge replikaene med nye prosesser og tomme tabeller. Etter en deploy gikk varselet ut igjen, som om det aldri hadde vært sendt.

Det omtrent 11 minutter lange gapet mellom de to varslene i hvert par kom fra jobben som utløser dette varselet. Den kjører på begge replikaene og tar en lås, så replikaene vinner den om hverandre, og hver av dem utløste varselet når det var dens tur.

Det fantes en andre, mer lumsk feil i samme kode. For å hindre at tabellen vokste uendelig, ryddet den seg selv når den holdt 500 nøkler, og slettet hver oppføring eldre enn 15 minutter, uansett vindu. Under last kunne et 24-timers dedup vare i en kvarter.

Løsningen

Én delt post per varsel

Før API-et sender et varsel, reserverer det nå varselets deduplikasjonsnøkkel i Redis, en delt minnelagring som begge replikaene snakker med, og som ikke starter på nytt når API-et gjør det. Reservasjonen er én enkelt operasjon: «sett denne nøkkelen bare hvis den ikke finnes, og utløp den etter vinduet». Replikaen som reserverer først, sender varselet. Den andre finner nøkkelen tatt og holder seg stille. En ny prosess etter en deploy finner nøkkelen fortsatt der.

Nøkkelen og vinduet kommer fra samme regel som varselpakken bruker, nå delt mellom dem, slik at de to lagene ikke kan være uenige om hva som regnes som «samme varsel».

Feil åpent, ikke lukket

Den viktigste avgjørelsen var hva vi gjør når Redis ikke kan nås. Vi valgte å sende varselet likevel. Hvis Redis er nede, eller ikke svarer innen 1,5 sekunder, går varselet ut. Et dobbelt varsel er støy. Et varsel som forsvinner, betyr at noen ikke blir informert.

De mest alvorlige varslene, P0, holdes aldri tilbake av deduplisering i det hele tatt.

Hver oppføring beholder sitt eget vindu

I varselpakkens egen deduplisering i minnet lagrer hver oppføring nå når vinduet slutter, og rydding fjerner bare oppføringer der vinduet er over. En 24-timers nøkkel blir værende i 24 timer, uansett hvor mange andre varsler som kommer.

Samme feil, et annet sted

Det samme mønsteret hadde også doblet et stykke bakgrunnsarbeid: en avstemmingsjobb som holder registeret vårt over hvert arbeidsområdes status på linje med sandkassetilbyderens. Hver kjøring lister hver sandkasse i kontoen, tusenvis i produksjon og omtrent fem sekunder, pluss hvert kjørende eller pauset prosjekt. Begge replikaene kjørte den hvert to minutt.

Skrivingene er idempotente, altså gir å kjøre dem to ganger samme resultat som å kjøre dem én gang, så ingenting gikk i stykker. Det var bare dobbelt arbeid. Nå reserveres hvert to-minutters-vindu i Redis på samme måte som bruksmålerne våre allerede reserverte sine, og bare replikaen som reserverer det, kjører jobben. Hvis Redis ikke kan nås, kjøres jobben likevel: å gjøre arbeidet to ganger er ufarlig, å ikke gjøre det er det ikke.

Slik testet vi det

Testene modellerer det virkelige oppsettet. De laster varselmodulen to ganger, som to separate «replikaer», peker begge mot én delt lagring, utløser det samme varselet fra hver, og laster deretter en tredje frisk kopi for å etterligne «etter en deploy» og utløser det igjen. Nøyaktig ett varsel sendes. Andre tester sjekker at et annet byggers varsel fortsatt går ut, at et P0 sendes fra begge replikaene, og at varselet fortsatt sendes når lagringen er nede eller henger forbi tidsgrensen. Ryddefiksen har sin egen test: en 24-timers nøkkel overlever 600 andre varsler som kommer 20 minutter senere.

For avstemmingsjobben gir to replikaer som tikker samtidig, pluss en tredje tikk i samme vindu, bare én listing.

Det vi tok med oss

  • Tilstand i minnet hører til prosessen. Alt som må være sant på tvers av servere, som «sendt én gang» eller «kjørt én gang», må ligge et sted de deler.
  • Deploy er omstart. Tilstand som bare overlever til neste omstart, overlever ikke en vanlig arbeidsdag.
  • Bestem hvilken vei du feiler. For varsler bør en nede delt lagring bety flere varsler, ikke færre.
  • Se etter mønsteret, ikke bare forekomsten. Varselet var det høylytte tilfellet. Den doblede avstemmingsjobben var samme feil, som stille kostet arbeid.

Ingenting av dette er synlig fra innsiden av Mythex. For mer om hvordan vi holder plattformen stabil, se hvordan vi fikk deploy ned til null 502-feil og den manglende swap-filen. Hvis du er ny til ideen om å kjøre flere kopier av en tjeneste, dekker guiden vår om hva en server er det grunnleggende.

Mythex · Pricing · Bygg hva som helst med KI · Maler for nettsider og apper · Mythex sammenlignet · Ordliste for app-bygging med KI · Gratis verktøy for app-bygging med KI · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)