Blog

Come una chat pesante avrebbe potuto mandare giù la nostra API

Il 28 settembre, circa 17 minuti dopo un rilascio, entrambi i nostri server API di produzione sono rimasti senza memoria nello stesso istante, si sono riavviati, e per circa cinque minuti il bilanciatore di carico non ha avuto alcun server sano a cui inviare traffico. La nostra prima correzione ha raddoppiato la memoria dell'API. La causa reale è emersa più tardi quello stesso giorno: aprire una particolare chat chiedeva all'API di caricare 53 MB di messaggi in una sola richiesta. La cronologia delle chat ora si carica in pagine limitate sia per numero sia per dimensione, e quella chat si apre con una prima pagina da 5 MB invece di andare in crash.

Cosa è andato giù

La nostra API gira come container su ciascuno dei due server, dietro un bilanciatore di carico. Ogni container aveva un limite di memoria di 384 MiB. A riposo usa 130–140 MiB, quindi il limite sembrava avere molto margine.

Entrambi i container hanno raggiunto quel limite insieme e sono andati in crash con "JavaScript heap out of memory". L'heap è la memoria che un processo Node.js usa per gli oggetti con cui lavora. Poiché entrambi i server sono falliti in contemporanea, non c'era più dove instradare le richieste. È il peggior tipo di interruzione per una configurazione con due server: la ridondanza aiuta solo quando i guasti non avvengono nello stesso momento.

La prima correzione: più spazio

La tempistica puntava al deploy. Dopo un rilascio, i turni dell'agente che il deploy aveva interrotto vengono ripresi e salvano di nuovo le loro trascrizioni, e quell'ondata di lavoro sembrava la fonte ovvia di un picco di memoria.

Quindi la prima modifica ha alzato il limite dell'API da 384 MiB a 768 MiB. L'abbiamo applicata in tempo reale su entrambi i server quello stesso giorno, e l'abbiamo committata nella configurazione di deploy così il deploy successivo non avrebbe silenziosamente riportato il valore a 384. Una correzione fatta a mano su un server che il deploy successivo annulla è un modo comune perché la stessa interruzione ritorni.

Più memoria era un margine di sicurezza ragionevole. Non era una spiegazione.

La causa reale: una richiesta, 71 MB

Più tardi quel giorno abbiamo riprodotto il crash su una copia di una chat, usando il vecchio limite di memoria di produzione. La copia da sola mandava in crash l'API, senza nessun deploy di mezzo.

Quando apri un progetto, l'app web chiede all'API la cronologia della chat. L'endpoint restituiva i 30 messaggi più recenti, interi. Per la maggior parte delle chat, 30 messaggi sono poca cosa. Per questa, i 30 messaggi più recenti pesavano 53 MB nel database e una risposta da 71 MB. Diverse risposte dell'agente erano da 4 a 10 MB ciascuna, soprattutto le trascrizioni dei sotto-agenti: gli agenti ausiliari che l'agente principale avvia per lavorare in parallelo su una parte di un compito, il cui lavoro completo viene salvato insieme alla risposta.

Costruire quella singola risposta occupava più heap di quanto l'API avesse. E ogni apertura o aggiornamento di quel progetto rimandava la richiesta. Una sola chat poteva mandare giù qualunque server la ricevesse, e poiché la pagina poteva essere ricaricata, poteva arrivare a entrambi.

"30 messaggi" non era mai stato un vero limite di dimensione. Era un limite sul numero, che per le chat normali capitava di essere piccolo.

La soluzione: pagine limitate per dimensione oltre che per numero

Una pagina di cronologia ora contiene 30 messaggi oppure circa 4 MB di essi, a seconda di quale arriva prima.

Funziona in due passaggi:

  1. Chiedi prima le dimensioni. L'API chiede al database gli ID e le dimensioni memorizzate dei messaggi più recenti, senza i loro contenuti. Quella query è economica qualunque sia la dimensione dei messaggi.
  2. Carica solo ciò che ci sta. Andando dal più recente al più vecchio, aggiunge messaggi finché il successivo non spingerebbe la pagina oltre 4 MB. Solo quelle righe vengono poi caricate per intero.

Due regole rendono questo sicuro:

  • Il messaggio più recente finisce sempre nella pagina, qualunque sia la sua dimensione. Una pagina che non restituisce nulla lascerebbe la chat bloccata, senza modo di scorrere indietro.
  • Nulla viene tagliato. Nessun messaggio viene troncato o eliminato. Il resto della chat si carica tramite il controllo "Scroll up for earlier messages" che già usavano i messaggi più vecchi, una pagina alla volta.

Abbiamo trovato un altro punto con la stessa forma. Quando interrompi un turno, l'API controlla le ultime risposte per vedere se qualcuna è vuota e va ripulita. Lo faceva caricando otto messaggi interi. Su una chat con risposte di diversi megabyte, è lo stesso problema in forma minore. Ora chiede al database una domanda sì-o-no — questo messaggio ha qualche contenuto? — senza caricare il contenuto stesso.

Come l'abbiamo verificato

Sulla copia della chat, con il vecchio limite di 384 MiB:

  • La prima pagina è di 5 MB. Prima, la stessa richiesta mandava in crash l'API.
  • Cinque schede del browser che aprono la chat contemporaneamente arrivano a un picco di 154 MB di memoria.
  • Scorrendo indietro per tutta la chat vengono restituiti tutti i 190 messaggi, ciascuno una volta sola, in ordine.

La logica di scelta delle pagine ha anche test unitari, incluso uno costruito con le dimensioni reali degli ultimi messaggi di quella chat. La prima pagina restituisce un messaggio, indica che ce ne sono altri, e la pagina successiva parte da quello dopo.

Cosa ne abbiamo ricavato

  • Un conteggio non è un limite di dimensione. Ogni endpoint che restituisce "gli N più recenti" di qualcosa che può crescere senza limiti ha bisogno anche di un budget in byte. I dati finiscono prima o poi per trovare il vuoto.
  • Il sospettato più ovvio non sempre è la causa. Il deploy era un vero innesco di lavoro extra, e più memoria era utile. Ma il crash si riproduceva senza deploy, e senza riprodurlo ci saremmo fermati all'aumento di memoria.
  • Due server cadono insieme se ricevono la stessa richiesta. La ridondanza protegge da un server che muore. Non protegge da una richiesta che uccide qualunque server raggiunga, soprattutto una che un utente riproverà naturalmente aggiornando la pagina.
  • Correggi nella configurazione, non solo sulla macchina. L'aumento di memoria è finito nella configurazione di deploy lo stesso giorno, così il deploy successivo l'ha mantenuto.

Questa è la nostra seconda storia recente di memoria legata all'output dell'agente. La prima era un'etichetta di stato che accodava 690 MB di stringhe. Se usi i sotto-agenti su Mythex, puoi leggere come funzionano nella documentazione.

Mythex · Pricing · Crea qualsiasi cosa con l'AI · Template per siti web e app · Mythex a confronto · Glossario per creare app con l'AI · Strumenti gratuiti per creare app con l'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 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)