Jeden ťažký chat mohol zhodiť naše API
- septembra, asi 17 minút po vydaní, naraz obe naše produkčné API servery minuli pamäť, reštartovali sa a na približne päť minút load balancer nemal žiadny zdravý server, ktorému by mohol posielať premávku. Naša prvá oprava dala API dvojnásobnú pamäť. Skutočná príčina sa ukázala neskôr v ten deň: otvorenie jedného konkrétneho chatu nútilo API načítať 53 MB správ v jedinej požiadavke. História chatu sa teraz načítava po stránkach obmedzených veľkosťou aj počtom správ a tento chat sa otvára s prvou stránkou 5 MB namiesto pádu.
Čo padlo
Naše API beží ako kontajner na každom z dvoch serverov za load balancerom. Každý kontajner mal limit pamäte 384 MiB. V pokoji používa 130 – 140 MiB, takže limit vyzeral ako dosť veľká rezerva.
Oba kontajnery naraz narazili na tento limit a spadli s hlásením „JavaScript heap out of memory“. Heap je pamäť, ktorú Node.js proces používa pre objekty, s ktorými pracuje. Keďže zlyhali oba servery naraz, nebolo kam smerovať požiadavky. Pri dvoch serveroch je to najhorší druh výpadku: redundancia pomáha len vtedy, keď zlyhania nenastanú súčasne.
Prvá oprava: viac miesta
Časovanie ukazovalo na nasadenie. Po vydaní sa agentské kroky, ktoré nasadenie prerušilo, znova obnovia a znova uložia svoje prepisy, a tento nápor práce vyzeral ako zrejmý zdroj nárastu pamäte.
Prvá zmena preto zvýšila limit API z 384 MiB na 768 MiB. Ručne sme ju v ten deň aplikovali na oba servery a zapísali do konfigurácie nasadenia, aby ďalšie nasadenie nevrátilo 384 späť. Oprava urobená ručne na serveri, ktorú ďalšie nasadenie vráti späť, je bežný spôsob, ako sa tá istá porucha vráti.
Viac pamäte bola rozumná bezpečnostná rezerva. Nebolo to však vysvetlenie.
Skutočná príčina: jedna požiadavka, 71 MB
Neskôr v ten deň sme pád zreprodukovali na kópii jedného chatu, s pôvodným limitom pamäte produkcie. Kópia zhodila API samu, bez akéhokoľvek nasadenia.
Keď otvoríte projekt, webová aplikácia si od API vyžiada históriu chatu. Endpoint vracal najnovších 30 správ celých. Pri väčšine chatov je 30 správ malé množstvo. Pri tomto chate mali najnovšie 30 správ v databáze 53 MB a odpoveď mala 71 MB. Niekoľko odpovedí agenta malo 4 až 10 MB každá, väčšinou prepisy sub-agentov: pomocných agentov, ktorých hlavný agent spúšťa, aby paralelne pracovali na časti úlohy, a ktorých celá práca sa ukladá spolu s odpoveďou.
Zostavenie tejto jednej odpovede zabralo viac heapu, než mal API k dispozícii. A každé otvorenie alebo obnovenie tohto projektu znova poslalo požiadavku. Jediný chat mohol zhodiť ten server, na ktorý dorazil, a keďže sa stránka dala obnoviť, mohol zasiahnuť aj oba.
„30 správ“ nikdy nebolo skutočným limitom veľkosti. Bol to limit počtu, ktorý bol pri bežných chatoch náhodou malý.
Oprava: stránky obmedzené veľkosťou aj počtom
Stránka histórie má teraz 30 správ alebo asi 4 MB správ, podľa toho, čo nastane skôr.
Funguje to v dvoch krokoch:
- Najprv sa pýtame na veľkosti. API od databázy vyžiada ID a uložené veľkosti najnovších správ bez ich obsahu. Takýto dotaz je lacný bez ohľadu na to, aké veľké správy sú.
- Načítame len to, čo sa zmestí. Od najnovšej po najstaršiu pridáva správy, kým ďalšia nepresiahne 4 MB. Až potom sa načítajú úplne iba tieto riadky.
Dve pravidlá to držia bezpečné:
- Najnovšia správa vždy ide na stránku, bez ohľadu na veľkosť. Stránka, ktorá by nevrátila nič, by nechala chat zaseknutý bez možnosti posunúť sa späť.
- Nič sa neorezáva. Žiadna správa sa neskracuje ani nevypúšťa. Zvyšok chatu sa načíta cez ovládací prvok „Scroll up for earlier messages“, ktorý staršie správy už používali, po jednej stránke naraz.
Našli sme ešte jedno miesto s rovnakým vzorom. Keď zastavíte beh, API skontroluje posledných pár odpovedí, či niektorá nie je prázdna a nemala by sa vyčistiť. Robilo to tak, že načítalo osem celých správ. Pri chate s multi-megabajtovými odpoveďami je to rovnaký problém v menšej podobe. Teraz sa databázy pýta áno-nie otázku — má táto správa nejaký obsah? — bez načítania samotného obsahu.
Ako sme to overili
Na kópii chatu, s pôvodným limitom 384 MiB:
- Prvá stránka má 5 MB. Predtým rovnaká požiadavka zhodila API.
- Keď päť prehliadačových kariet naraz otvorí chat, pamäť vyskočí najviac na 154 MB.
- Posúvanie späť cez celý chat vráti všetkých 190 správ, každú presne raz, v poradí.
Logika výberu stránok má aj unit testy, vrátane jedného postaveného na skutočných veľkostiach najnovších správ toho chatu. Prvá stránka vráti jednu správu, oznámi, že sú ďalšie, a ďalšia stránka začína tou nasledujúcou.
Čo sme si z toho odniesli
- Počet nie je limit veľkosti. Každý endpoint, ktorý vracia „najnovších N“ z niečoho, čo môže neobmedzene rásť, potrebuje aj rozpočet bajtov. Dáta medzeru nakoniec nájdu.
- Najzrejmejší podozrivý nie je vždy príčina. Nasadenie bolo skutočným spúšťačom navyše a viac pamäte stálo za to. Pád sa však zreprodukoval aj bez nasadenia, a bez zreprodukovania by sme sa zastavili pri zvýšení pamäte.
- Dva servery zlyhajú spoločne, keď dostanú rovnakú požiadavku. Redundancia chráni pred pádom jedného servera. Nechráni pred požiadavkou, ktorá zhodí ktorýkoľvek server, na ktorý dorazí, najmä ak ju používateľ prirodzene zopakuje obnovením stránky.
- Oprav v konfigurácii, nie len na serveri. Zvýšenie pamäte sa v ten deň dostalo do konfigurácie nasadenia, takže ho ďalšie nasadenie zachovalo.
Toto je náš druhý nedávny príbeh o pamäti súvisiaci s výstupom agenta. Prvým bol stavový štítok, ktorý zaradil do fronty 690 MB reťazcov. Ak používate sub-agentov v Mythex, o tom, ako fungujú, si môžete prečítať v dokumentácii.