Jeden těžký chat by mohl položit naše API
- září, zhruba 17 minut po nasazení, oba naše produkční API servery najednou došly paměti, restartovaly se, a zhruba pět minut neměl load balancer žádný zdravý server, kam by poslal provoz. Naše první oprava dala API dvojnásobnou paměť. Skutečná příčina se ukázala později toho dne: otevření jednoho konkrétního chatu požádalo API o načtení 53 MB zpráv v jednom požadavku. Historie chatu se teď načítá po stránkách omezených velikostí i počtem a ten chat se otevře s první stránkou o 5 MB místo pádu.
Co se stalo
Naše API běží jako kontejner na každém ze dvou serverů za load balancerem. Každý kontejner měl limit paměti 384 MiB. V klidu používá 130–140 MiB, takže limit vypadal jako dost místa navíc.
Oba kontejnery narazily na limit zároveň a spadly s hlášením „JavaScript heap out of memory“. Halda je paměť, kterou proces Node.js používá pro objekty, se kterými pracuje. Protože oba servery selhaly najednou, nebylo kam směrovat požadavky. To je nejhorší druh výpadku u nastavení se dvěma servery: redundance pomáhá jen tehdy, když selhání nenastanou ve stejnou chvíli.
První oprava: více místa
Časování ukazovalo na nasazení. Po vydání se běhy agenta, které nasazení přerušilo, znovu obnoví a jejich přepisy se znovu uloží, a ten náhlý nápor práce vypadal jako zřejmý zdroj špičky v paměti.
Takže první změna zvýšila limit API z 384 MiB na 768 MiB. Použili jsme to ručně na obou serverech ještě tentýž den a zapsali jsme to do konfigurace nasazení, aby další nasazení tiše nevrátilo 384. Oprava provedená ručně na serveru, kterou pak další nasazení vrátí zpět, je častý způsob, jak se stejný výpadek vrátí.
Více paměti byla rozumná bezpečnostní rezerva. Nebylo to vysvětlení.
Skutečná příčina: jeden požadavek, 71 MB
Později toho dne jsme pád zreprodukovali na kopii jednoho chatu s původním limitem paměti z produkce. Kopie sama o sobě shodila API bez jakéhokoli nasazení.
Když otevřeš projekt, webová aplikace si od API vyžádá historii chatu. Koncový bod vracel nejnovějších 30 zpráv celých. U většiny chatů je 30 zpráv malé množství. U tohoto chatu měl těchto 30 nejnovějších zpráv přesně 53 MB v databázi a odpověď měla 71 MB. Několik odpovědí agenta mělo 4–10 MB každá, hlavně přepisy subagentů: pomocných agentů, které hlavní agent spouští, aby paralelně pracovali na části úkolu, a jejichž celá práce se uloží spolu s odpovědí.
Sestavení té jediné odpovědi zabralo víc haldy, než API mělo. A každé otevření nebo obnovení tohoto projektu posílalo požadavek znovu. Jediný chat mohl položit kterýkoli server, na který dopadl, a protože se stránka dala znovu načíst, mohl dosáhnout na oba.
„30 zpráv“ nikdy nebylo skutečným limitem velikosti. Bylo to limitem počtu, který byl pro běžné chaty náhodou malý.
Oprava: stránky omezené velikostí i počtem
Stránka historie je teď dlouhá nejvýš 30 zpráv nebo zhruba 4 MB, podle toho, co nastane dřív.
Běží ve dvou krocích:
- Nejdřív se zeptej na velikosti. API požádá databázi o ID a uložené velikosti nejnovějších zpráv, bez jejich obsahu. Tenhle dotaz je levný bez ohledu na to, jak velké zprávy jsou.
- Načti jen to, co se vejde. Od nejnovější po nejstarší přidává zprávy, dokud by další nepřekročila 4 MB stránky. Pak se načtou celé jen tyto řádky.
Dvě pravidla to dělají bezpečným:
- Nejnovější zpráva se na stránku vždy dostane, ať je jakkoli velká. Stránka, která by nevrátila nic, by nechala chat zaseknutý bez možnosti rolovat zpět.
- Nic se neřeže. Žádná zpráva se nezkracuje ani nezahazuje. Zbytek chatu se načítá přes ovládací prvek „Posuň nahoru pro starší zprávy“, který už starší zprávy používaly, po jedné stránce.
Našli jsme ještě jedno místo se stejným tvarem problému. Když zastavíš běh, API zkontroluje posledních několik odpovědí, zda některá není prázdná a neměla by se uklidit. Dělalo to tak, že načetlo osm celých zpráv. U chatu s odpověďmi o několika megabajtech je to stejný problém v menší podobě. Teď databázi položí ano-ne otázku – má tahle zpráva nějaký obsah? – aniž by načetlo samotný obsah.
Jak jsme to ověřili
Na kopii chatu při původním limitu 384 MiB:
- První stránka má 5 MB. Předtím týž požadavek API shodil.
- Pět karet prohlížeče otevírajících chat najednou dosáhne špičky 154 MB paměti.
- Rolování zpět přes celý chat vrátí všech 190 zpráv, každou přesně jednou a v pořadí.
Logika výběru stránek má také jednotkové testy, včetně jednoho postaveného na skutečných velikostech nejnovějších zpráv toho chatu. První stránka vrátí jednu zprávu, oznámí, že je dalších, a další stránka začne tou, která následuje.
Co jsme si z toho odnesli
- Počet není limit velikosti. Každý koncový bod, který vrací „nejnovějších N“ něčeho, co může neomezeně růst, potřebuje i rozpočet bajtů. Data tu mezeru nakonec najdou.
- Nejzjevnější podezřelý není vždy příčina. Nasazení bylo skutečným spouštěčem navíc práce a víc paměti stálo za to. Ale pád se zreprodukoval i bez nasazení, a kdybychom ho nezreprodukovali, zastavili bychom se u zvýšení paměti.
- Dva servery selžou spolu, když dostanou týž požadavek. Redundance chrání před pádem jednoho serveru. Nechrání před požadavkem, který shodí kterýkoli server, který dosáhne – zvlášť takový, který uživatel přirozeně zkusí znovu obnovením stránky.
- Oprav to v konfiguraci, ne jen na stroji. Zvýšení paměti se ten samý den zapsalo do konfigurace nasazení, takže ho další nasazení zachovalo.
Tohle je náš druhý nedávný příběh o paměti, který se týká výstupu agenta. První byl popisek stavu, který do fronty zařadil 690 MB řetězců. Pokud na Mythexu používáš subagenty, o tom, jak fungují, si můžeš přečíst v dokumentaci.