Backpressure: limiti della coda di invio per i client WebSocket lenti | eSeGeCe Blog

Backpressure: limiti della coda di invio per i client WebSocket lenti

· Componenti

Un server che trasmette in broadcast un feed veloce produce messaggi al proprio ritmo, non a quello del client più lento. Dati di mercato, telemetria, il tail di un log in tempo reale: il server spinge, e ci si aspetta che ogni client connesso stia al passo. La maggior parte lo fa. Uno di loro, prima o poi, no.

Quando un client smette di svuotare il proprio socket, i messaggi che gli invii non spariscono. Si accumulano nella coda di invio di quella connessione, e la coda è memoria. Un telefono entrato in una galleria ferroviaria, una scheda del browser rallentata dal sistema, un processo client che ha smesso di chiamare read, tutti quanti fanno crescere silenziosamente la memoria del tuo server finché qualcosa non cede. sgcWebSockets 2026.7 ti permette di limitare quella coda e di decidere che cosa succede quando è piena.

Il consumatore lento

Il problema non è che la rete sia lenta, è la mancata corrispondenza. Il tuo feed pubblica 500 messaggi al secondo. Un client su un collegamento scadente ne smaltisce 50. Gli altri 450 devono finire da qualche parte, e quel "qualche parte" è una lista per connessione nel processo del server. Dopo un minuto quella singola connessione trattiene decine di migliaia di messaggi. Dopo dieci minuti trattiene la tua RAM.

Il danno non si limita nemmeno al client lento. La memoria che consuma è memoria che i client sani non ottengono, e una volta che il processo inizia a fare swap o l'allocatore inizia ad arrancare, la latenza di tutti ne risente. Un telefono in una galleria diventa un disservizio per tutti.

Fino a oggi la coda di invio in sgcWebSockets era illimitata, il che va benissimo finché ogni client sta al passo e diventa spiacevole la prima volta che uno non ci riesce. La soluzione è metterle un tetto.

Limitare la coda

La coda vive sotto le QueueOptions del componente, accanto alle sotto-opzioni di livello già esistenti Text, Binary e Ping. Due nuove proprietà la limitano: MaxQueueSize stabilisce quanti messaggi pendenti può trattenere una singola connessione, e OverflowPolicy dice che cosa fare con il messaggio successivo una volta raggiunto quel tetto.

uses
  sgcWebSocket, sgcWebSocket_Classes, sgcWebSocket_Types;

begin
  // messages are queued per connection and flushed by the queue thread
  oServer.QueueOptions.Text.Level   := qmLevel1;
  oServer.QueueOptions.Binary.Level := qmLevel1;

  // at most 1000 pending messages per connection
  oServer.QueueOptions.MaxQueueSize := 1000;

  // when the queue is full, throw away the oldest pending message
  oServer.QueueOptions.OverflowPolicy := qopDropOldest;
end;

QueueOptions è published su TsgcWebSocketServer, TsgcWebSocketHTTPServer e TsgcWebSocketClient, e il componente la copia su ogni connessione man mano che viene accettata, quindi il limite è per connessione, non per server. Mille connessioni con MaxQueueSize = 1000 ti danno un caso peggiore di mille messaggi pendenti ciascuna, e quel caso peggiore è ora un numero che puoi calcolare sulla carta prima di andare in produzione.

Scegliere una politica di overflow

Qui non c'è una risposta universalmente giusta, ed è per questo che ce ne sono tre. La politica che vuoi dipende da che cosa significano i tuoi messaggi.

qopDropOldest è quella giusta per un feed in tempo reale in cui conta solo il valore più recente. Una quotazione, una lettura di un sensore, un indicatore di una dashboard: il messaggio di quattro secondi fa non vale nulla nell'istante in cui arriva il successivo. Scartare dalla testa della coda significa che il client rimasto indietro e poi ripresosi riceve la fotografia attuale, non un arretrato di messaggi obsoleti da digerire prima di rimettersi in pari. Questa è la politica predefinita.

qopDropNewest è quella giusta quando sono i messaggi precedenti a contare e preferisci scaricare il nuovo carico. Se un client viene guidato attraverso una sequenza e la testa di quella sequenza è la parte significativa, conserva ciò che è già in coda e rifiuta il nuovo arrivato.

qopDisconnect è quella giusta quando un client che non riesce a stare al passo è un client che non vuoi. È l'opzione onesta: invece di alimentare silenziosamente quella connessione con una vista dello stream piena di buchi e corrotta, la chiudi. Il WatchDog del client si riconnette, si risottoscrive e riparte da uno stato noto. Un client che sa di essere stato disconnesso può recuperare. Un client che non sa di aver perso messaggi non può.

// A market data feed: only the newest tick is worth anything.
oServer.QueueOptions.MaxQueueSize   := 500;
oServer.QueueOptions.OverflowPolicy := qopDropOldest;

// A sequence where the head matters: shed the new load instead.
oServer.QueueOptions.OverflowPolicy := qopDropNewest;

// A client that falls this far behind is a client we do not want.
oServer.QueueOptions.OverflowPolicy := qopDisconnect;

Qualunque cosa scegli, scegliela deliberatamente. Scartare messaggi è un compromesso, e il senso di questi tre valori è che sei tu a scegliere quale compromesso stai accettando, invece di scoprirlo in produzione.

Saperlo prima di scartare: OnSendBufferFull

Una coda che scarta messaggi in silenzio è una coda che ti mente. OnSendBufferFull è la via d'uscita. Viene scatenato prima che qualcosa venga scartato, ti consegna la connessione e la dimensione attuale della coda, e il parametro var DropMessage permette al tuo codice di porre il veto allo scarto.

procedure TForm1.WSServerSendBufferFull(Connection: TsgcWSConnection;
  const QueueSize: Integer; var DropMessage: Boolean);
begin
  // fires BEFORE anything is dropped: log it, count it, alert on it
  DoLog('slow consumer ' + Connection.IP + ' [' + Connection.Guid + '] queue=' +
    IntToStr(QueueSize));
  Inc(FSlowConsumerHits);

  // this connection is a paying feed we must not truncate: keep the message
  // and let the queue grow past the cap for this one send
  if IsPriorityConnection(Connection) then
    DropMessage := False;
end;

Imposta DropMessage := False e la politica di overflow viene saltata per quel messaggio: viene accodato comunque, oltre il tetto. Usa questa possibilità con parsimonia, perché un veto che applichi sempre equivale a non avere alcun limite. L'uso quotidiano dell'evento è quello noioso, ed è quello prezioso: sapere che è successo, a quale connessione è successo e quanto era rimasta indietro. Quel numero è il tuo preavviso, molto prima che il processo esaurisca la memoria.

OnSendBufferFull è published su TsgcWebSocketClient, TsgcWebSocketServer e TsgcWebSocketHTTPServer.

Che fare quando non puoi permetterti di perdere nulla

A volte la risposta onesta è che nessun messaggio può mai andare perso. Se è il tuo caso, nessuna delle tre politiche viste sopra fa per te, e nemmeno una coda più grande. Una coda più grande non risolve il problema, lo rimanda soltanto, e lo rimanda a un momento peggiore: più avanti in una raffica, più a fondo nella pressione sulla memoria, e più lontano dal punto in cui avresti ancora potuto fare qualcosa.

La risposta è un design diverso. Non trattenere i messaggi non consegnati in una coda di socket, rendili persistenti, e lascia che il client recuperi ciò che ha perso quando è in grado di farlo. È esattamente ciò che fa la cronologia dei messaggi e il recupero alla riconnessione: il server mantiene una cronologia limitata per canale con degli offset, e un client rimasto indietro o che ha perso la connessione riottiene i messaggi persi al momento della risottoscrizione. Abbinalo a qopDisconnect e ottieni la combinazione più solida disponibile: un client che non riesce a stare al passo viene tagliato fuori rapidamente, e quando torna recupera tutto ciò che ha perso invece di fingere di non averlo perso.

Illimitata resta l'impostazione predefinita

MaxQueueSize vale 0 per impostazione predefinita, il che significa illimitata, che è esattamente il comportamento che sgcWebSockets aveva prima. Aggiornare non cambia nulla. Il percorso di overflow non viene nemmeno imboccato a meno che tu non imposti un tetto positivo, quindi i server esistenti continuano ad accodare come hanno sempre fatto, finché non decidi diversamente.

Detto questo, se trasmetti in broadcast a client che non controlli, e su Internet pubblica non li controlli mai, una coda per connessione senza limiti è un impegno di memoria a tempo indeterminato preso per conto del peggior client che si connetterà mai a te. Impostare un tetto costa una riga.

Disponibilità

I limiti della coda di invio, le tre politiche di overflow e OnSendBufferFull sono inclusi in sgcWebSockets 2026.7 per Delphi da 7 a 13 e C++Builder, su Win32/Win64, Linux64, macOS, Android e iOS.

I clienti con un abbonamento attivo possono scaricare la nuova build dall'area clienti. Gli utenti trial possono ottenere l'installer aggiornato su esegece.com/products/websockets/download.

Domande, commenti o bisogno di aiuto per scegliere la politica giusta per il tuo feed? Contattaci, riceverai una risposta dalle persone che hanno scritto il codice.