Upozorenja koja se šalju jednom, a ne jednom po serveru
Jedno od naših operativnih upozorenja, „malo kredita” za objavljenu aplikaciju graditelja, treba da stigne najviše jednom dnevno. Jednog popodneva stiglo je pet puta. Uzrok: svaka kopija našeg API-ja pamtila je u sopstvenoj memoriji koja su upozorenja već poslala, pa su dva servera slala dvaput, a svaki deploy je tu memoriju brisao i dozvoljavao ponovno slanje. Isti obrazac je imao i pozadinski posao koji je svoj rad radio dvaput. Oba sada koordinišu kroz deljenu memoriju, a oba u slučaju da ta memorija nije dostupna greše na sigurnoj strani.
Simptom
Mythex šalje timska upozorenja kada nešto treba da se pogleda: servis je pao, posao je zastao, a stanje kredita graditelja je nisko dok je njegova aplikacija objavljena. Svako upozorenje ima prozor deduplikacije, odnosno period tokom kojeg se isto upozorenje šalje samo jednom. Za upozorenje o malom broju kredita taj prozor je 24 sata.
Pet kopija istog upozorenja jednog popodneva značilo je da prozor ne drži. Gore od toga, dolazila su u parovima, otprilike 11 minuta razmaka.
Zašto se to dogodilo
Paket za upozorenja držao je deduplikaciju u memoriji: tabelu „ključ upozorenja → kada je poslednji put poslato”, unutar pokrenutog procesa. To radi kada postoji jedan proces koji se nikada ne restartuje. Produkcija nema ni jedno ni drugo:
- Dve API replike. Produkcija pokreće dve kopije API-ja. Svaka je imala sopstvenu tabelu, pa je svaka poslala upozorenje jednom. To su parovi.
- Deployi. Svaki deploy zamenjuje obe replike novim procesima i praznim tabelama. Posle deploya upozorenje je poslato ponovo, kao da nikada nije bilo poslato.
Razmak od oko 11 minuta između dva upozorenja u svakom paru potekao je iz posla koji podiže to upozorenje. Posao radi na obe replike i uzima zaključavanje, pa replike to naizmenično dobijaju, i svaka je podigla upozorenje kada je došao njen red.
U istom kodu krila se i druga, tiša greška. Da tabela ne bi rasla zauvek, čistila se kada dostigne 500 ključeva, brišući svaki unos stariji od 15 minuta, bez obzira na njegov prozor. Pod opterećenjem, 24-satna deduplikacija mogla je da traje četvrt sata.
Ispravka
Jedan deljeni zapis po upozorenju
Pre nego što API pošalje upozorenje, sada u Redisu, deljenoj memoriji u koju obe replike pišu i koja se ne resetuje kada se API restartuje, zauzima ključ deduplikacije tog upozorenja. Zauzimanje je jedna operacija: „postavi ovaj ključ samo ako ne postoji, i istekni ga posle prozora”. Koja god replika ga prva zauzme, šalje upozorenje; druga nađe ključ zauzet i ćuti. Novi proces posle deploya nađe ključ i dalje tu.
Ključ i prozor dolaze iz istog pravila koje koristi paket za upozorenja, sada deljenog između dva sloja, pa ne mogu da se ne slože oko toga šta se računa kao „isto upozorenje”.
Pri greškama, dozvoli slanje
Najvažnija odluka bila je šta se dešava kada se Redisu ne može pristupiti. Odlučili smo da upozorenje ipak pošaljemo. Ako je Redis pao ili ne odgovori za 1,5 sekundi, upozorenje odlazi. Duplo upozorenje je šum. Izgubljeno upozorenje znači da neko nije obavešten.
Najozbiljnija upozorenja, P0, deduplikacija uopšte ne zadržava.
Svaki unos čuva svoj prozor
U sopstvenoj deduplikaciji paketa za upozorenja u memoriji, svaki unos sada čuva kada se njegov prozor završava, a čišćenje uklanja samo unose čiji je prozor istekao. 24-satni ključ ostaje 24 sata, bez obzira na to koliko drugih upozorenja stigne.
Isti bug, na drugom mestu
Isti obrazac udvostručio je i deo pozadinskog posla: posao usklađivanja koji drži naš zapis o statusu svakog workspace-a u skladu sa statusom kod našeg sandbox provajdera. Svaki prolaz izlista svaki sandbox na nalogu, hiljade u produkciji, za oko pet sekundi, plus svaki pokrenut ili pauziran projekat. Obe replike su ga pokretale na svaka dva minuta.
Njegovi upisi su idempotentni, odnosno dvostruko pokretanje daje isti rezultat kao jednostruko, 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ši brojači korišćenja već zauzimali svoje, i samo replika koja ga zauzme pokreće prolaz. Ako se Redisu ne može pristupiti, prolaz se ipak pokreće: dvostruki rad je bezopasan, a propušteni nije.
Kako smo testirali
Testovi modeluju stvarnu postavku. Učitavaju modul za upozorenja dvaput, kao dve zasebne „replike”, usmere obe na jednu deljenu memoriju, podignu isto upozorenje iz svake, pa učitaju treću svežu kopiju da predstave „posle deploya” i podignu ga ponovo. Tačno jedno upozorenje se šalje. Drugi testovi proveravaju da upozorenje drugog graditelja i dalje odlazi, da P0 odlazi sa obe replike i da upozorenje i dalje odlazi kada je memorija nedostupna ili prekorači rok. Ispravka čišćenja ima sopstveni test: 24-satni ključ preživi 600 drugih upozorenja koja stignu 20 minuta kasnije.
Za posao usklađivanja, dve replike koje istovremeno otkucaju, plus treći otkucaj u istom prozoru, daju jedan izlistan prolaz.
Šta smo izvukli iz ovoga
- Stanje u memoriji pripada procesu. Sve što mora da važi među serverima, „poslato jednom” ili „pokrenuto jednom”, mora da živi tamo gde ga svi dele.
- Deployi su restartovi. Stanje koje preživi samo do sledećeg restarta ne preživi običan radni dan.
- Odluči u kom smeru se greši. Za upozorenja, pad deljene memorije treba da znači više upozorenja, a ne manje.
- Traži obrazac, a ne samo primer. Upozorenje je bio glasni slučaj; udvostručeni posao usklađivanja bio je isti bug koji tiho košta 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 da pokrećete više kopija servisa, naš vodič o tome šta je server pokriva osnove.