Backpressure: limieten op de verzendwachtrij voor trage WebSocket-clients | eSeGeCe Blog

Backpressure: limieten op de verzendwachtrij voor trage WebSocket-clients

· Componenten

Een server die een snelle feed uitzendt, produceert berichten in zijn eigen tempo, niet in het tempo van de traagste client. Marktdata, telemetrie, een live meelopend logbestand: de server duwt, en van elke verbonden client wordt verwacht dat hij bijblijft. De meeste doen dat. Eentje, vroeg of laat, niet.

Wanneer een client zijn socket niet meer leegtrekt, verdwijnen de berichten die u hem stuurt niet. Ze stapelen zich op in de verzendwachtrij van die verbinding, en die wachtrij is geheugen. Een telefoon die een treintunnel in reed, een browsertabblad dat werd afgeknepen, een clientproces dat stopte met lezen, ze laten allemaal stilletjes het geheugen van uw server groeien tot er iets omvalt. sgcWebSockets 2026.7 laat u die wachtrij begrenzen en beslissen wat er gebeurt wanneer ze vol is.

De trage consument

Het probleem is niet dat het netwerk traag is, het is de mismatch. Uw feed publiceert 500 berichten per seconde. Een client op een slechte verbinding trekt er 50 leeg. De andere 450 moeten ergens heen, en "ergens" is een lijst per verbinding in het serverproces. Na een minuut houdt die ene verbinding tienduizenden berichten vast. Na tien minuten houdt ze uw RAM vast.

De schade blijft ook niet beperkt tot de trage client. Het geheugen dat hij opslokt is geheugen dat de gezonde clients niet krijgen, en zodra het proces begint te swappen of de allocator begint te worstelen, gaat ieders latency mee. Eén telefoon in een tunnel wordt een storing voor iedereen.

Tot nu toe was de verzendwachtrij in sgcWebSockets onbegrensd, wat prima is zolang elke client bijblijft en onaangenaam wordt zodra er één dat niet doet. De oplossing is er een plafond op leggen.

De wachtrij begrenzen

De wachtrij leeft onder QueueOptions van de component, naast de bestaande sub-opties op niveau Text, Binary en Ping. Twee nieuwe properties begrenzen haar: MaxQueueSize beperkt hoeveel wachtende berichten een enkele verbinding mag vasthouden, en OverflowPolicy zegt wat er met het volgende bericht moet gebeuren zodra dat plafond is bereikt.

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 is published op TsgcWebSocketServer, TsgcWebSocketHTTPServer en TsgcWebSocketClient, en de component kopieert het naar elke verbinding zodra die wordt geaccepteerd, dus de limiet geldt per verbinding, niet per server. Duizend verbindingen met MaxQueueSize = 1000 geven u in het slechtste geval duizend wachtende berichten per stuk, en dat slechtste geval is nu een getal dat u op papier kunt uitrekenen voordat u uitrolt.

Een overflow-policy kiezen

Er is hier geen universeel juist antwoord, en daarom zijn er drie. De policy die u wilt hangt af van wat uw berichten betekenen.

qopDropOldest is juist voor een live feed waarin alleen de nieuwste waarde telt. Een prijsnotering, een sensormeting, een meter op een dashboard: het bericht van vier seconden geleden is waardeloos op het moment dat het volgende arriveert. Vooraan uit de wachtrij verwijderen betekent dat de client die achterop raakte en zich daarna herstelde het actuele beeld krijgt, en niet een achterstand van verouderde beelden waar hij zich eerst doorheen moet worstelen voordat hij bij is. Dit is de standaardwaarde.

qopDropNewest is juist wanneer de eerdere berichten degene zijn die ertoe doen en u liever de nieuwe last afwerpt. Als een client door een reeks wordt geleid en het begin van die reeks het betekenisvolle deel is, houd dan wat al in de wachtrij staat en weiger de nieuwkomer.

qopDisconnect is juist wanneer een client die niet kan bijblijven een client is die u niet wilt. Het is de eerlijke optie: in plaats van die verbinding stilletjes een beschadigd beeld vol gaten van de stroom voor te schotelen, sluit u haar. De WatchDog van de client maakt opnieuw verbinding, hij abonneert zich opnieuw, en hij begint opnieuw vanuit een bekende toestand. Een client die weet dat hij werd losgekoppeld kan herstellen. Een client die niet weet dat hij berichten kwijtraakte, kan dat niet.

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

Wat u ook kiest, kies het bewust. Berichten laten vallen is een afweging, en het punt van deze drie waarden is dat u zelf kiest welke afweging u maakt, in plaats van haar in productie te ontdekken.

Weten voordat u laat vallen: OnSendBufferFull

Een wachtrij die stilletjes berichten laat vallen is een wachtrij die tegen u liegt. OnSendBufferFull is de nooduitgang. Het wordt afgevuurd voordat er iets wordt weggegooid, het geeft u de verbinding en de huidige wachtrijgrootte, en met de parameter var DropMessage kan uw code het laten vallen tegenhouden.

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;

Zet DropMessage := False en de overflow-policy wordt voor dat bericht overgeslagen: het wordt toch in de wachtrij gezet, boven het plafond. Gebruik dat spaarzaam, want een veto dat u altijd toepast is hetzelfde als helemaal geen limiet. Het alledaagse gebruik van het event is het saaie, en tevens het waardevolle: weten dat het gebeurde, bij welke verbinding het gebeurde, en hoe ver die was achtergeraakt. Dat getal is uw vroegtijdige waarschuwing, lang voordat het proces zonder geheugen komt te zitten.

OnSendBufferFull is published op TsgcWebSocketClient, TsgcWebSocketServer en TsgcWebSocketHTTPServer.

Wat te doen wanneer u zich niet kunt veroorloven iets te laten vallen

Soms is het eerlijke antwoord dat geen enkel bericht ooit verloren mag gaan. Als dat uw geval is, is geen van de drie policies hierboven wat u wilt, en een grotere wachtrij evenmin. Een grotere wachtrij lost het probleem niet op, ze stelt het alleen uit, en ze stelt het uit naar een slechter moment: verder in een piek, dieper in de geheugendruk, en verder weg van het punt waarop u er nog iets aan had kunnen doen.

Het antwoord is een ander ontwerp. Houd onbezorgde berichten niet vast in een socketwachtrij, bewaar ze persistent, en laat de client ophalen wat hij miste zodra hij daartoe in staat is. Dat is precies wat berichtgeschiedenis en herstel bij herverbinding doet: de server houdt per kanaal een begrensde geschiedenis met offsets bij, en een client die achterop raakte of zijn verbinding verloor speelt bij het opnieuw abonneren de gemiste berichten af. Combineer dat met qopDisconnect en u krijgt de sterkste combinatie die er is: een client die niet kan bijblijven wordt snel losgekoppeld, en wanneer hij terugkomt herstelt hij alles wat hij miste in plaats van te doen alsof hij niets miste.

Onbeperkt blijft de standaard

MaxQueueSize staat standaard op 0, wat onbeperkt betekent, en dat is precies het gedrag dat sgcWebSockets voorheen had. Een upgrade verandert niets. Het overflow-pad wordt niet eens betreden tenzij u een positief plafond instelt, dus bestaande servers blijven berichten in de wachtrij zetten zoals ze dat altijd deden, totdat u anders beslist.

Dat gezegd hebbende, als u uitzendt naar clients die u niet beheert, en op het publieke internet beheert u ze nooit, dan is een onbegrensde wachtrij per verbinding een open geheugenverplichting die u aangaat namens de slechtste client die ooit verbinding met u zal maken. Een plafond instellen kost één regel.

Beschikbaarheid

Limieten op de verzendwachtrij, de drie overflow-policies en OnSendBufferFull worden geleverd in sgcWebSockets 2026.7 voor Delphi 7 tot en met 13 en C++Builder, op Win32/Win64, Linux64, macOS, Android en iOS.

Klanten met een actief abonnement kunnen de nieuwe build downloaden in de klantenzone. Trialgebruikers kunnen de bijgewerkte installer ophalen op esegece.com/products/websockets/download.

Vragen, feedback of hulp bij het kiezen van de juiste policy voor uw feed? Neem contact op, en u krijgt antwoord van de mensen die de code hebben geschreven.