Kako bi en težak klepet lahko podrl naš API
- septembra, približno 17 minut po izdaji, sta obe naši produkcijski API strežniški vrstici hkrati zmanjkali pomnilnika, se znova zagnali, in približno pet minut uravnoteževalnik obremenitve ni imel nobenega zdravega strežnika, kamor bi poslal promet. Naš prvi popravek je API dal dvakrat več pomnilnika. Pravi vzrok se je pokazal pozneje tistega dne: odprtje enega določenega klepeta je API prisililo, da je v eni sami zahtevi naložil 53 MB sporočil. Zgodovina klepetov se zdaj nalaga v straneh, omejenih po velikosti in ne le po številu, ta klepet pa se odpre s prvo stranjo velikosti 5 MB namesto s sesutjem.
Kaj je šlo narobe
Naš API teče kot vsebnik na vsakem od dveh strežnikov za uravnoteževalnikom obremenitve. Vsak vsebnik je imel omejitev pomnilnika 384 MiB. V mirovanju porabi 130–140 MiB, zato se je zdela omejitev dovolj velika.
Oba vsebnika sta hkrati dosegla to omejitev in se sesula z napako „JavaScript heap out of memory“. Kopica (heap) je pomnilnik, ki ga proces Node.js uporablja za predmete, s katerimi dela. Ker sta oba strežnika odpovedala hkrati, zahtev ni bilo kam poslati. To je najhujša vrsta izpada za nastavitev z dvema strežnikoma: redundanca pomaga samo, kadar okvare ne nastopijo istočasno.
Prvi popravek: več prostora
Čas je kazal na izdajo. Po izdaji se agentovi potezi, ki jih je izdaja prekinila, nadaljujejo in ponovno shranijo svoje prepise, ta val dela pa se je zdel očiten vzrok skoka pomnilnika.
Zato smo prvo spremembo povišali omejitev API z 384 MiB na 768 MiB. Isti dan smo jo ročno uveljavili na obeh strežnikih in jo zapisali v konfiguracijo izdaje, da naslednja izdaja ne bi tiho vrnila 384. Popravek, narejen ročno na strežniku, ki ga naslednja izdaja razveljavi, je pogost način, kako se ista okvara vrne.
Več pomnilnika je bila razumna varnostna rezerva. Vendar ni bila razlaga.
Pravi vzrok: ena zahteva, 71 MB
Kasneje tistega dne smo napako reproducirali na kopiji enega klepeta, z uporabo stare omejitve pomnilnika iz produkcije. Kopija je sama, brez izdaje, sesula API.
Ko odprete projekt, spletna aplikacija od API-ja zahteva zgodovino klepeta. Krajna točka je vrnila zadnjih 30 sporočil v celoti. Pri večini klepetov je 30 sporočil majhna količina. Pri tem klepetu so zadnjih 30 sporočil zasedalo 53 MB v bazi in 71 MB odgovora. Več agentovih odgovorov je bilo velikih 4–10 MB vsak, večinoma prepisi pod-agentov: pomožnih agentov, ki jih glavni agent zažene, da vzporedno obdela del naloge, njihovo celotno delo pa je shranjeno skupaj z odgovorom.
Sestavljanje tega enega odgovora je potrebovalo več kopice, kot je API imel. Vsako odprtje ali osvežitev tega projekta je zahtevo poslalo znova. En klepet je lahko sesul kateri koli strežnik, na katerega je prišel, in ker se je stran lahko ponovno naložila, je lahko dosegel oba.
„30 sporočil“ nikoli ni bila prava omejitev velikosti. Bila je omejitev števila, ki je bila pri običajnih klepetih naključno majhna.
Popravek: strani, omejene po velikosti in številu
Stran zgodovine zdaj vsebuje 30 sporočil ali približno 4 MB, kar pride prej.
Deluje v dveh korakih:
- Najprej vprašaj po velikostih. API od baze zahteva ID-je in shranjene velikosti najnovejših sporočil, brez njihove vsebine. Ta poizvedba je poceni, ne glede na to, kako velika so sporočila.
- Naloži samo, kar se prilega. Od najnovejšega proti najstarejšemu dodaja sporočila, dokler naslednje ne bi preseglo 4 MB. Samo te vrstice se nato naložijo v celoti.
Dve pravili to varno držita:
- Najnovejše sporočilo vedno gre na stran, ne glede na velikost. Stran, ki ne bi vrnila ničesar, bi pustila klepet zataknjen, brez možnosti pomikanja nazaj.
- Nič se ne reže. Nobeno sporočilo ni skrajšano ali izpuščeno. Preostanek klepeta se naloži prek kontrole „Scroll up for earlier messages“ (Pomakni se gor za prejšnja sporočila), ki so jo starejša sporočila že uporabljala, po eno stran naenkrat.
Naleteli smo na še eno mesto z isto obliko. Ko ustavite potezo, API preveri zadnjih nekaj odgovorov, da vidi, ali je kateri prazen in ga je treba počistiti. To je počel tako, da je naložil osem celih sporočil. Pri klepetu z več-megabajtnimi odgovori je to isti problem v manjši obliki. Zdaj bazi zastavi vprašanje da-ali-ne – ali to sporočilo sploh ima vsebino? – brez nalaganja same vsebine.
Kako smo preverili
Na kopiji klepeta, pri stari omejitvi 384 MiB:
- Prva stran meri 5 MB. Prej je ista zahteva sesula API.
- Pet brskalniških zavihkov, ki hkrati odprejo klepet, doseže vrh 154 MB pomnilnika.
- Pomik nazaj skozi ves klepet vrne vseh 190 sporočil, vsako natanko enkrat, po vrsti.
Logika izbiranja strani ima tudi enotske teste, vključno s tistim, zgrajenim iz pravih velikosti najnovejših sporočil tega klepeta. Prva stran vrne eno sporočilo, javi, da jih je več, in naslednja stran se začne pri tistem, ki sledi.
Kaj smo si iz tega vzeli
- Število ni omejitev velikosti. Vsaka krajna točka, ki vrača „najnovejših N“ česa, kar lahko raste brez meja, potrebuje tudi proračun bajtov. Podatki bodo sčasoma našli vrzel.
- Očitni osumljenec ni vedno vzrok. Izdaja je bila pravi sprožilec dodatnega dela, in več pomnilnika je bilo vredno imeti. Vendar se je napaka reproducirala tudi brez izdaje, in brez reprodukcije bi se ustavili pri povečanju pomnilnika.
- Dva strežnika odpovesta skupaj, če dobita isto zahtevo. Redundanca ščiti pred okvaro enega strežnika. Ne ščiti pred zahtevo, ki ubije kateri koli strežnik, do katerega pride, zlasti če jo uporabnik samoumevno ponovi z osvežitvijo.
- Popravi v konfiguraciji, ne samo na stroju. Povečanje pomnilnika je isti dan šlo v konfiguracijo izdaje, zato ga je naslednja izdaja obdržala.
To je naša druga nedavna zgodba o pomnilniku, povezana z izhodom agenta. Prva je bila oznaka stanja, ki je v čakalno vrsto spravila 690 MB nizov. Če na Mythexu uporabljate pod-agente, lahko o tem, kako delujejo, preberete v dokumentaciji.