Upozorenja koja se aktiviraju jednom, a ne jednom po serveru
Jedno od naših operativnih upozorenja — „nizak kredit” za objavljenu aplikaciju korisnika builder-a — treba da nam stigne najviše jednom dnevno. Jednog popodneva stiglo je pet puta. Uzrok: svaka kopija našeg API-ja pamtila je koja su upozorenja već poslala u sopstvenoj memoriji, pa su dva servera slala isto dvaput, a svaki deploy brisao je tu memoriju i dopuštao ponovno slanje. Isti obrazac je uzrokovao da pozadinski posao radi svoj posao dvaput. Oba sada koordinišu kroz dijeljeni store, i oba u slučaju da store nije dostupan greškom padaju na sigurnu stranu.
Simptom
Mythex šalje timskim upozorenja kada nešto treba pogledati: servis je pao, posao je zapeo, ili je stanje kredita korisnika builder-a nisko dok je njegova aplikacija objavljena. Svako upozorenje ima prozor deduplikacije — period tokom kojeg se isto upozorenje šalje samo jednom. Za upozorenje o niskom kreditu, taj prozor je 24 sata.
Pet kopija istog upozorenja u jednom popodnevu značilo je da prozor ne drži. Gore od toga, dolazila su u parovima, otprilike 11 minuta jedno od drugog.
Zašto se to desilo
Paket za upozorenja držao je deduplikaciju u memoriji: tabelu „ključ upozorenja → kada je posljednji put poslato”, unutar pokrenutog procesa. To radi kada postoji jedan proces koji se nikada ne restartuje. Produkcija nema ni jedno ni drugo:
- Dvije API replike. Produkcija pokreće dvije kopije API-ja. Svaka je imala svoju tabelu, pa je svaka poslala upozorenje jednom. Eto parova.
- Deployi. Svaki deploy zamjenjuje obje replike novim procesima i praznim tabelama. Nakon deploya, upozorenje je ponovo izašlo, kao da nikada nije bilo poslato.
Razmak od oko 11 minuta između dva upozorenja u svakom paru dolazi od posla koji podiže ovo upozorenje. On radi na obje replike i uzima lock, pa replike naizmjenično dobijaju pravo, i svaka je podigla upozorenje kada joj je došao red.
U istom kodu krila se i druga, tiša greška. Da tabela ne bi rasla u beskraj, čistila se kada dostigne 500 ključeva — brisala je svaki unos stariji od 15 minuta, bez obzira na njegov prozor. Pod opterećenjem, deduplikacija od 24 sata mogla je trajati četvrt sata.
Ispravka
Jedan dijeljeni zapis po upozorenju
Prije nego što API pošalje upozorenje, sada prvo zauzima ključ deduplikacije upozorenja u Redisu — dijeljenom store-u u memoriji sa kojim razgovaraju obje replike i koji se ne restartuje kada se API restartuje. Zauzimanje je jedna operacija „postavi ovaj ključ samo ako ne postoji, i istekni ga nakon prozora”. Ko prvi zauzme ključ, šalje upozorenje; druga replika nađe ključ zauzet i ćuti. Novi proces nakon deploya nađe ključ i dalje tu.
Ključ i prozor dolaze iz istog pravila koje koristi paket za upozorenja, sada dijeljenog između njih, pa dva sloja ne mogu da se razilaze oko toga šta znači „isto upozorenje”.
Ne zatvaraj, ostani otvoren
Najvažnija odluka bila je šta raditi kada Redis nije dostupan. Odlučili smo da upozorenje ipak pošaljemo. Ako je Redis pao ili ne odgovori u roku od 1,5 sekundi, upozorenje odlazi. Duplo upozorenje je šum. Izgubljeno upozorenje znači da neko nije obaviješten.
Najozbiljnija upozorenja, P0, deduplikacija uopšte ne zadržava.
Svaki unos zadržava svoj prozor
U deduplikaciji u memoriji u samom paketu za upozorenja, svaki unos sada bilježi kada njegov prozor završava, a čišćenje uklanja samo unose čiji je prozor istekao. Ključ od 24 sata ostaje 24 sata, bez obzira na to koliko drugih upozorenja stigne.
Isti bug, na drugom mjestu
Isti obrazac udvostručio je i dio pozadinskog posla: posao usklađivanja koji održava našu evidenciju statusa svakog radnog prostora u skladu sa statusom kod našeg sandbox providera. Svaki prolaz izlista svaki sandbox na nalogu — hiljade u produkciji, oko pet sekundi — plus svaki projekat koji radi ili je pauziran. Obje replike su ga pokretale svake dvije minute.
Njegova upisivanja su idempotentna — pokretanje dvaput daje isti rezultat kao jednom — pa ništa nije puklo. Samo je bilo dvostruko više posla. Sada se svaki dvominutni prozor zauzima u Redisu na isti način na koji su naša mjerila korišćenja već zauzimala svoje, i samo replika koja zauzme prozor pokreće prolaz. Ako Redis nije dostupan, prolaz se ipak pokreće: raditi posao dvaput je bezopasno, ne raditi ga nije.
Kako smo testirali
Testovi modeliraju pravo okruženje. Učitaju modul za upozorenja dvaput, kao dvije odvojene „replike”, obje ih usmjere na jedan dijeljeni store, podignu isto upozorenje iz svake, pa učitaju treću, svježu kopiju da simulira „nakon deploya” i podignu ga ponovo. Poslato je tačno jedno upozorenje. Drugi testovi provjeravaju da upozorenje drugog korisnika builder-a i dalje odlazi, da P0 odlazi sa obje replike i da se upozorenje šalje i kada je store nedostupan ili prekorači timeout. Ispravka čišćenja ima svoj test: ključ od 24 sata preživi 600 drugih upozorenja koja stignu 20 minuta kasnije.
Za posao usklađivanja, dvije replike koje otkucavaju u isto vrijeme, plus treći otkucaj u istom prozoru, daju jedan izlistavanje.
Šta smo iz toga naučili
- Stanje u memoriji pripada jednom procesu. Sve što mora biti tačno među serverima — „poslato jednom”, „pokrenuto jednom” — treba da živi negdje što oni dijele.
- Deployi su restarti. Stanje koje preživi samo do sljedećeg restarta ne preživi normalan radni dan.
- Odluči u kom smjeru pada. Za upozorenja, pad dijeljenog store-a treba značiti više upozorenja, a ne manje.
- Traži obrazac, a ne samo pojavu. Upozorenje je bio glasni slučaj; udvostručeni posao usklađivanja bio je isti bug, tiho koštajući rad.
Ništa od ovoga nije vidljivo unutar Mythexa. Za više o tome kako održavamo platformu stabilnom, pogledajte kako smo spustili deploye na nula 502 grešaka i nedostajući swap fajl. Ako ste novi u ideji pokretanja više kopija jednog servisa, naš vodič o šta je server pokriva osnove.