Blog

Un chat încărcat ar putea doborî API-ul nostru

Pe 28 septembrie, la aproximativ 17 minute după o lansare, ambele servere API de producție au rămas fără memorie exact în același moment, au repornit, iar timp de circa cinci minute load balancer-ul nu a avut niciun server sănătos către care să trimită trafic. Prima noastră soluție a dublat memoria API-ului. Cauza reală a apărut mai târziu în aceeași zi: deschiderea unui anumit chat cerea API-ului să încarce 53 MB de mesaje într-o singură cerere. Istoricul chatului se încarcă acum pe pagini limitate atât după număr, cât și după dimensiune, iar chatul respectiv se deschide cu o primă pagină de 5 MB în loc să se prăbușească.

Ce a căzut

API-ul nostru rulează ca un container pe fiecare dintre două servere, în spatele unui load balancer. Fiecare container avea o limită de memorie de 384 MiB. În repaus folosește 130–140 MiB, așa că limita părea să lase mult spațiu liber.

Ambele containere au atins limita în același timp și s-au prăbușit cu mesajul „JavaScript heap out of memory”. Heap-ul este memoria pe care o folosește un proces Node.js pentru obiectele cu care lucrează. Pentru că ambele servere au eșuat deodată, nu mai exista unde să fie rutate cererile. Acesta este cel mai rău tip de pană pentru o configurație cu două servere: redundanța ajută doar atunci când eșecurile nu apar simultan.

Prima soluție: mai mult spațiu

Momentul părea să indice lansarea. După o lansare, turele agentului întrerupte de deploy sunt reluate și își salvează din nou transcrierile, iar acea rafală de muncă părea sursa evidentă a unui vârf de memorie.

Așa că prima modificare a ridicat limita API-ului de la 384 MiB la 768 MiB. Am aplicat-o live pe ambele servere în aceeași zi și am comis-o în configurația de deploy, ca următorul deploy să nu readucă discret valoarea 384. O reparație făcută manual pe un server, pe care următorul deploy o anulează, este un mod frecvent prin care aceeași pană revine.

Mai multă memorie era o marjă de siguranță rezonabilă. Nu era o explicație.

Cauza reală: o cerere, 71 MB

Mai târziu în aceeași zi am reprodus prăbușirea pe o copie a unui chat, folosind vechea limită de memorie a producției. Copia a doborât API-ul singură, fără niciun deploy implicat.

Când deschizi un proiect, aplicația web cere API-ului istoricul chatului. Endpoint-ul returna ultimele 30 de mesaje, întregi. Pentru majoritatea chaturilor, 30 de mesaje înseamnă puțin. Pentru acesta, ultimele 30 de mesaje aveau 53 MB în baza de date și un răspuns de 71 MB. Câteva dintre răspunsurile agentului aveau între 4 și 10 MB fiecare, mai ales transcrierile subagenților: agenții ajutători pe care agentul principal îi pornește ca să lucreze în paralel la o parte din sarcină, a căror muncă integrală este salvată împreună cu răspunsul.

Construirea acelui singur răspuns necesita mai mult heap decât avea API-ul. Iar fiecare deschidere sau reîmprospătare a proiectului trimitea din nou cererea. Un singur chat putea doborî oricare server pe care ajungea, iar pentru că pagina putea fi reîncărcată, putea ajunge la ambele.

„30 de mesaje” nu fusese niciodată o limită reală de dimensiune. Fusese o limită de număr, care nimerea să fie mică pentru chaturile obișnuite.

Soluția: pagini limitate după dimensiune și după număr

O pagină de istoric are acum 30 de mesaje sau aproximativ 4 MB din ele, oricare vine primul.

Funcționează în doi pași:

  1. Cere mai întâi dimensiunile. API-ul cere bazei de date ID-urile și dimensiunile salvate ale celor mai noi mesaje, fără conținutul lor. Interogarea este ieftină indiferent cât de mari sunt mesajele.
  2. Încarcă doar ce încape. Mergând de la cel mai nou la cel mai vechi, adaugă mesaje până când următorul ar împinge pagina peste 4 MB. Doar acele rânduri sunt apoi încărcate integral.

Două reguli țin asta în siguranță:

  • Cel mai nou mesaj intră mereu în pagină, indiferent de dimensiune. O pagină care nu returnează nimic ar lăsa chatul blocat, fără cale de a derula înapoi.
  • Nimic nu este tăiat. Niciun mesaj nu este trunchiat sau eliminat. Restul chatului se încarcă prin controlul „Scroll up for earlier messages”, pe care mesajele mai vechi îl foloseau deja, câte o pagină pe rând.

Am găsit și un alt loc cu aceeași formă. Când oprești o tură, API-ul verifică ultimele câteva răspunsuri ca să vadă dacă vreunul este gol și trebuie curățat. Făcea asta încărcând opt mesaje întregi. Pe un chat cu răspunsuri de câțiva megaocteți, este aceeași problemă, într-o formă mai mică. Acum pune bazei de date o întrebare cu da sau nu — are acest mesaj vreun conținut? — fără să încarce conținutul propriu-zis.

Cum am verificat

Pe copia chatului, rulând cu vechea limită de 384 MiB:

  • Prima pagină are 5 MB. Înainte, aceeași cerere doborâ API-ul.
  • Cinci tab-uri de browser care deschid chatul simultan ating un vârf de 154 MB de memorie.
  • Derularea înapoi prin întregul chat returnează toate cele 190 de mesaje, fiecare exact o dată, în ordine.

Logica de alegere a paginilor are și teste unitare, inclusiv unul construit din dimensiunile reale ale celor mai noi mesaje ale chatului respectiv. Prima pagină revine cu un singur mesaj, spune că mai sunt altele, iar următoarea pagină începe cu cel de după el.

Ce am învățat

  • Un număr nu este o limită de dimensiune. Orice endpoint care returnează „cele mai noi N” dintr-un lucru care poate crește fără limită are nevoie și de un buget de octeți. Datele vor găsi, mai devreme sau mai târziu, golul.
  • Suspectul evident nu este mereu cauza. Lansarea a fost un declanșator real pentru muncă suplimentară, iar memoria în plus a meritat. Dar prăbușirea s-a reprodus și fără un deploy, iar fără reproducere ne-am fi oprit la creșterea memoriei.
  • Două servere cad împreună când primesc aceeași cerere. Redundanța protejează împotriva morții unui singur server. Nu protejează împotriva unei cereri care doboară orice server pe care ajunge, mai ales una pe care un utilizator o va reîncerca natural reîmprospătând pagina.
  • Repară în configurație, nu doar pe mașină. Creșterea memoriei a intrat în configurația de deploy în aceeași zi, astfel încât următorul deploy a păstrat-o.

Aceasta este a doua poveste recentă de memorie legată de ieșirea agentului. Prima a fost o etichetă de stare care a pus în coadă 690 MB de șiruri. Dacă folosești subagenți pe Mythex, poți citi despre cum funcționează în documentație.

Mythex · Pricing · Construiește orice cu AI · Șabloane pentru site-uri și aplicații · Mythex comparat · Glosar pentru crearea de aplicații cu AI · Instrumente gratuite pentru crearea de aplicații cu AI · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)