Hogyan tudott egy nehéz chat leállítani az API-nkat
Szeptember 28-án, egy kiadás után kb. 17 perccel, mindkét éles API-szerverünk egyszerre fogyott ki a memóriából, újraindult, és körülbelül öt percig a terheléselosztónak nem volt egészséges szervere, amelynek forgalmat küldhetett volna. Az első javításunk az API-nak a kétszeres memóriát adta. Az igazi ok később, aznap derült ki: egy konkrét chat megnyitása egyetlen kérésben 53 MB üzenet betöltését kérte az API-tól. Az előzménylapok most a darabszám mellett mérettel is korlátozottak, és ez a chat 5 MB-os első oldallal nyílik, nem összeomlással.
Mi állt le
Az API konténerként fut mindkét szerveren, a terheléselosztó mögött. Minden konténer memóriakorlátja 384 MiB volt. Nyugalmi állapotban 130–140 MiB-ot használ, így a korlát bőven elegendőnek tűnt.
Mindkét konténer egyszerre érte el a korlátot, és „JavaScript heap out of memory” hibával összeomlott. A heap az a memória, amelyet egy Node.js folyamat az általa használt objektumokhoz foglal. Mivel mindkét szerver egyszerre hibázott, nem maradt, ahová a kéréseket irányítani lehetett volna. Ez a legrosszabb fajta kiesés egy kétszerveres felállásban: a redundancia csak akkor segít, ha a hibák nem egyszerre jönnek.
Az első javítás: több hely
Az időzítés a kiadásra mutatott. Egy kiadás után a deploy által megszakított ügynöki körök folytatódnak, és az átiratukat újra mentik, és ez a munkahullám nyilvánvaló forrásnak tűnt egy memóriacsúcsra.
Az első változtatás ezért 384 MiB-ról 768 MiB-ra emelte az API korlátját. Ugyanazon a napon élesben alkalmaztuk mindkét szerveren, és a deploy-konfigurációba is beírtuk, hogy a következő deploy ne állítsa csendben vissza 384-re. Egy kézzel, szerveren végzett javítás, amelyet a következő deploy visszacsinál, gyakori módja annak, hogy ugyanaz a kiesés visszatérjen.
A nagyobb memória ésszerű biztonsági tartalék volt. Nem magyarázat.
Az igazi ok: egy kérés, 71 MB
Később aznap egy chat másolatán reprodukáltuk az összeomlást, az éles környezet régi memóriakorlátjával. A másolat önmagában összeomlasztotta az API-t, deploy nélkül.
Amikor megnyitsz egy projektet, a webalkalmazás lekéri az API-tól a chat előzményeit. Az endpoint a legutóbbi 30 üzenetet adta vissza, teljesen. A legtöbb chatnél 30 üzenet kicsi. Ennél a chatnél a legutóbbi 30 üzenet 53 MB volt az adatbázisban, és 71 MB volt a válasz. Az ügynök több válasza 4–10 MB volt egyenként, főleg al-ügynökök átiratai: a segéd-ügynökök, amelyeket a fő ügynök a feladat egy részének párhuzamos elvégzésére indít, és teljes munkájuk a válasszal együtt van mentve.
Ezt az egyetlen választ összeállítani több heapet igényelt, mint amennyi az API-nak volt. És a projekt minden megnyitása vagy frissítése újraküldte a kérést. Egyetlen chat bármelyik szervert, amelyre került, leállíthatta, és mivel az oldal újratölthető, mindkettőt elérhette.
A „30 üzenet” sosem volt valódi mérethatár. Darabszámhatár volt, amely a normál chatekhez épp kicsi volt.
A javítás: méret szerint is korlátozott oldalak
Az előzmények egy oldala most 30 üzenet vagy kb. 4 MB, attól függően, melyik teljesül előbb.
Két lépésben működik:
- Előbb méretet kérünk. Az API az adatbázistól a legutóbbi üzenetek azonosítóit és tárolt méretét kéri, a tartalmuk nélkül. Ez a lekérdezés olcsó, bármilyen nagyok is az üzenetek.
- Csak azt töltjük be, ami belefér. A legújabbtól a legrégebbi felé haladva üzeneteket veszünk hozzá, amíg a következő túllépné a 4 MB-ot az oldalon. Csak azokat a sorokat töltjük be teljesen.
Két szabály biztosítja, hogy ez biztonságos legyen:
- A legújabb üzenet mindig az oldalra kerül, bármekkora is. Egy üres oldal beragadt chatet eredményezne, ahonnan nem lehetne visszagörgetni.
- Semmi nincs levágva. Egyetlen üzenet sincs csonkítva vagy eldobva. A chat többi része a „Görgess fel a korábbi üzenetekért” gombon keresztül töltődik be, oldalanként, ahogy a régebbi üzenetek eddig is.
Egy másik helyen is ugyanez a minta volt. Amikor leállítasz egy kört, az API ellenőrzi az utolsó néhány választ, hogy van-e köztük üres, amelyet tisztítani kell. Ehhez nyolc teljes üzenetet töltött be. Több megabájtos válaszokból álló chaten ez a probléma kisebb formája. Most az adatbázisnak egy igen-nem kérdést tesz fel — van ennek az üzenetnek tartalma? —, anélkül, hogy magát a tartalmat betöltené.
Hogyan ellenőriztük
A chat másolatán, a régi 384 MiB-os korláttal:
- Az első oldal 5 MB. Korábban ugyanez a kérés összeomlasztotta az API-t.
- Öt böngészőlap, amely egyszerre nyitja meg a chatet, 154 MB csúcsmemóriát használ.
- A teljes chat visszagörgetése mind a 190 üzenetet visszaadja, mindegyik pontosan egyszer, sorrendben.
Az oldalválasztó logikának egységtesztjei is vannak, köztük egy, amely a chat legutóbbi üzeneteinek valódi méreteiből épül. Az első oldalon egy üzenet jön vissza, jelzi, hogy van több, a következő oldal pedig a rákövetkező üzenettel kezdődik.
Mit vittünk ebből magunkkal
- A darabszám nem méretkorlát. Minden végpont, amely „a legújabb N” elemet ad vissza egy olyan dologból, amely korlátlanul nőhet, bájtkeretet is igényel. Az adat előbb-utóbb megtalálja a rést.
- A kézenfekvő gyanúsított nem mindig az ok. A deploy valódi kiváltó tényező volt a többletmunkához, és a több memória megérte. De az összeomlás deploy nélkül is reprodukálódott, és reprodukció nélkül a memórianövelésnél megálltunk volna.
- Két szerver ugyanarra a kérésre együtt bukik. A redundancia egy szerver elhalása ellen véd. Egy olyan kérés ellen nem véd, amely bármelyik szervert, amelyet elér, megöli — főleg, ha a felhasználó természetes módon frissítéssel próbálkozik újra.
- A konfigban javíts, ne csak a gépen. A memórianövelés még aznap bekerült a deploy-konfigurációba, így a következő deploy megtartotta.
Ez a második közelmúltbeli memóriatörténetünk, amely az ügynök kimenetét érinti. Az első egy állapotjelző címke, amely 690 MB stringet sorolt be. Ha al-ügynököket használsz a Mythexen, a dokumentációban olvashatsz arról, hogyan működnek.