Contre-pression : limites de file d'envoi pour les clients WebSocket lents | Blog eSeGeCe

Contre-pression : limites de file d'envoi pour les clients WebSocket lents

· Composants

Un serveur qui diffuse un flux rapide produit les messages à son propre rythme, pas à celui du client le plus lent. Données de marché, télémétrie, journal en direct : le serveur pousse, et chaque client connecté est censé suivre. La plupart y arrivent. L'un d'eux, tôt ou tard, n'y arrive pas.

Quand un client cesse de vider sa socket, les messages que vous lui envoyez ne disparaissent pas. Ils s'accumulent dans la file d'envoi de cette connexion, et cette file, c'est de la mémoire. Un téléphone entré dans un tunnel de train, un onglet de navigateur bridé, un processus client qui a cessé d'appeler read, tous font enfler discrètement la mémoire de votre serveur jusqu'à ce que quelque chose lâche. sgcWebSockets 2026.7 vous permet de borner cette file et de décider ce qui se passe lorsqu'elle est pleine.

Le consommateur lent

Le problème n'est pas que le réseau soit lent, c'est le décalage. Votre flux publie 500 messages par seconde. Un client sur une mauvaise liaison en absorbe 50. Les 450 autres doivent bien aller quelque part, et ce « quelque part » est une liste par connexion dans le processus serveur. Au bout d'une minute, cette seule connexion retient des dizaines de milliers de messages. Au bout de dix minutes, elle retient votre RAM.

Les dégâts ne se limitent pas au client lent. La mémoire qu'il consomme est de la mémoire que les clients sains n'obtiennent pas, et dès que le processus se met à faire du swap ou que l'allocateur commence à peiner, la latence de tout le monde en pâtit. Un téléphone dans un tunnel devient une panne pour tous.

Jusqu'à présent, la file d'envoi de sgcWebSockets était sans limite, ce qui convient tant que tous les clients suivent, et devient très désagréable la première fois que l'un d'eux ne suit plus. La solution est de lui poser un plafond.

Borner la file

La file se trouve sous les QueueOptions du composant, à côté des sous-options existantes de niveau Text, Binary et Ping. Deux nouvelles propriétés la bornent : MaxQueueSize plafonne le nombre de messages en attente qu'une même connexion peut retenir, et OverflowPolicy indique quoi faire du message suivant une fois ce plafond atteint.

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 est publiée sur TsgcWebSocketServer, TsgcWebSocketHTTPServer et TsgcWebSocketClient, et le composant la recopie sur chaque connexion au moment où elle est acceptée, donc la limite est par connexion, pas par serveur. Mille connexions avec MaxQueueSize = 1000 vous donnent un pire cas de mille messages en attente chacune, et ce pire cas est désormais un nombre que vous pouvez calculer sur le papier avant de déployer.

Choisir une politique de débordement

Il n'y a pas de bonne réponse universelle ici, c'est pourquoi il y en a trois. La politique qui vous convient dépend de ce que signifient vos messages.

qopDropOldest convient à un flux en direct où seule la valeur la plus récente compte. Une cotation, une mesure de capteur, une jauge de tableau de bord : le message d'il y a quatre secondes ne vaut plus rien dès que le suivant arrive. Supprimer en tête de file signifie que le client qui a pris du retard puis s'est rétabli reçoit l'image courante, et non un arriéré de valeurs périmées qu'il devrait digérer avant de rattraper. C'est la politique par défaut.

qopDropNewest convient quand ce sont les messages les plus anciens qui comptent et que vous préférez délester la nouvelle charge. Si un client est guidé à travers une séquence et que le début de cette séquence est la partie significative, gardez ce qui est déjà en file et refusez le nouveau venu.

qopDisconnect convient quand un client incapable de suivre est un client dont vous ne voulez pas. C'est l'option honnête : plutôt que d'alimenter silencieusement cette connexion avec une vue trouée et corrompue du flux, vous la fermez. Le WatchDog du client le reconnecte, il se réabonne, et il repart d'un état connu. Un client qui sait qu'il a été déconnecté peut se rétablir. Un client qui ignore qu'il a perdu des messages ne le peut pas.

// 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;

Quel que soit votre choix, faites-le délibérément. Supprimer des messages est un compromis, et l'intérêt de ces trois valeurs est que vous choisissez le compromis que vous acceptez au lieu de le découvrir en production.

Savoir avant de supprimer : OnSendBufferFull

Une file qui supprime des messages en silence est une file qui vous ment. OnSendBufferFull est la porte de sortie. Elle se déclenche avant que quoi que ce soit ne soit écarté, elle vous remet la connexion et la taille courante de la file, et le paramètre var DropMessage permet à votre code d'opposer son veto à la suppression.

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;

Mettez DropMessage := False et la politique de débordement est ignorée pour ce message : il est mis en file malgré tout, au-delà du plafond. Utilisez cela avec parcimonie, car un veto que vous appliquez toujours revient exactement à n'avoir aucune limite. L'usage quotidien de l'événement est le plus banal, et le plus précieux : savoir que c'est arrivé, à quelle connexion, et à quel point elle avait pris du retard. Ce chiffre est votre alerte précoce, bien avant que le processus ne manque de mémoire.

OnSendBufferFull est publiée sur TsgcWebSocketClient, TsgcWebSocketServer et TsgcWebSocketHTTPServer.

Que faire quand vous ne pouvez rien vous permettre de perdre

Parfois, la réponse honnête est qu'aucun message ne doit jamais être perdu. Si c'est votre cas, aucune des trois politiques ci-dessus ne vous convient, et une file plus grande non plus. Une file plus grande ne règle pas le problème, elle ne fait que le repousser, et elle le repousse à un moment pire : plus loin dans la rafale, plus profondément dans la pression mémoire, et plus loin du point où vous auriez encore pu y faire quelque chose.

La réponse est une conception différente. Ne conservez pas les messages non délivrés dans une file de socket, persistez-les, et laissez le client récupérer ce qu'il a manqué lorsqu'il en est capable. C'est exactement ce que fait l'historique des messages et la récupération à la reconnexion : le serveur conserve un historique borné par canal avec des offsets, et un client qui a pris du retard ou perdu sa connexion rejoue les messages manqués lors du réabonnement. Combinez cela avec qopDisconnect et vous obtenez la combinaison la plus solide disponible : un client incapable de suivre est rapidement largué, et à son retour il récupère tout ce qu'il a manqué au lieu de faire comme si de rien n'était.

Sans limite reste le comportement par défaut

MaxQueueSize vaut 0 par défaut, ce qui signifie sans limite, c'est-à-dire exactement le comportement qu'avait sgcWebSockets auparavant. La mise à jour ne change rien. Le chemin de débordement n'est même pas emprunté tant que vous ne définissez pas un plafond positif, donc les serveurs existants continuent de mettre en file comme ils l'ont toujours fait tant que vous n'en décidez pas autrement.

Cela dit, si vous diffusez vers des clients que vous ne contrôlez pas, et sur l'internet public vous ne les contrôlez jamais, une file par connexion sans limite est un engagement mémoire ouvert pris au nom du pire client qui se connectera un jour à vous. Poser un plafond coûte une ligne.

Disponibilité

Les limites de file d'envoi, les trois politiques de débordement et OnSendBufferFull sont livrés dans sgcWebSockets 2026.7 pour Delphi 7 à 13 et C++Builder, sur Win32/Win64, Linux64, macOS, Android et iOS.

Les clients disposant d'un abonnement actif peuvent télécharger la nouvelle version depuis l'espace client. Les utilisateurs en version d'essai peuvent récupérer le programme d'installation mis à jour sur esegece.com/products/websockets/download.

Des questions, des remarques ou besoin d'aide pour choisir la bonne politique pour votre flux ? Contactez-nous, vous recevrez une réponse des personnes qui ont écrit le code.