Ein Server, der einen schnellen Feed verteilt, erzeugt Nachrichten in seinem eigenen Tempo, nicht im Tempo des langsamsten Clients. Marktdaten, Telemetrie, ein laufendes Log: Der Server schiebt, und von jedem verbundenen Client wird erwartet, dass er mithält. Die meisten tun das. Einer von ihnen tut es früher oder später nicht.
Wenn ein Client seinen Socket nicht mehr leert, verschwinden die Nachrichten, die Sie ihm senden, nicht. Sie stapeln sich in der Sendewarteschlange dieser Verbindung, und die Warteschlange ist Speicher. Ein Telefon, das in einen Bahntunnel gefahren ist, ein Browser-Tab, der gedrosselt wurde, ein Client-Prozess, der aufgehört hat zu lesen, sie alle lassen den Speicher Ihres Servers still und leise wachsen, bis etwas umfällt. sgcWebSockets 2026.7 erlaubt es Ihnen, diese Warteschlange zu begrenzen und zu entscheiden, was geschieht, wenn sie voll ist.
Der langsame Konsument
Das Problem ist nicht, dass das Netz langsam ist, es ist das Missverhältnis. Ihr Feed veröffentlicht 500 Nachrichten pro Sekunde. Ein Client auf einer schlechten Strecke nimmt 50 davon ab. Die anderen 450 müssen irgendwohin, und dieses "Irgendwohin" ist eine Liste pro Verbindung im Serverprozess. Nach einer Minute hält diese eine Verbindung Zehntausende von Nachrichten. Nach zehn Minuten hält sie Ihren Arbeitsspeicher.
Der Schaden bleibt auch nicht auf den langsamen Client beschränkt. Der Speicher, den er frisst, ist Speicher, den die gesunden Clients nicht bekommen, und sobald der Prozess anfängt zu swappen oder der Allokator ins Straucheln gerät, geht die Latenz aller mit. Ein Telefon in einem Tunnel wird zum Ausfall für alle.
Bis jetzt war die Sendewarteschlange in sgcWebSockets unbegrenzt, was in Ordnung ist, solange jeder Client mithält, und unangenehm wird, sobald einer es nicht mehr tut. Die Lösung ist eine Obergrenze.
Die Warteschlange begrenzen
Die Warteschlange liegt unter QueueOptions der Komponente, neben den bestehenden Unteroptionen für die Level von Text, Binary und Ping. Zwei neue Properties begrenzen sie: MaxQueueSize deckelt, wie viele ausstehende Nachrichten eine einzelne Verbindung halten darf, und OverflowPolicy legt fest, was mit der nächsten Nachricht geschieht, sobald diese Grenze erreicht ist.
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 ist bei TsgcWebSocketServer, TsgcWebSocketHTTPServer und TsgcWebSocketClient published, und die Komponente überträgt die Optionen auf jede Verbindung, sobald diese angenommen wird, die Grenze gilt also pro Verbindung, nicht pro Server. Tausend Verbindungen mit MaxQueueSize = 1000 ergeben im schlimmsten Fall jeweils tausend ausstehende Nachrichten, und dieser schlimmste Fall ist jetzt eine Zahl, die Sie vor dem Deployment auf Papier ausrechnen können.
Die richtige Überlaufstrategie wählen
Es gibt hier keine allgemeingültig richtige Antwort, deshalb gibt es drei. Welche Strategie Sie wollen, hängt davon ab, was Ihre Nachrichten bedeuten.
qopDropOldest ist richtig für einen Live-Feed, bei dem nur der neueste Wert zählt. Ein Kurs-Tick, ein Sensorwert, eine Anzeige im Dashboard: Die Nachricht von vor vier Sekunden ist wertlos, sobald die nächste eintrifft. Vom Kopf der Warteschlange zu verwerfen bedeutet, dass der Client, der zurückgefallen und dann wieder aufgeholt hat, das aktuelle Bild bekommt und nicht einen Rückstau veralteter Nachrichten, den er erst durchkauen muss, bevor er wieder aufschließt. Das ist die Vorgabe.
qopDropNewest ist richtig, wenn die früheren Nachrichten diejenigen sind, auf die es ankommt, und Sie lieber die neue Last abwerfen. Wenn ein Client durch eine Abfolge geführt wird und der Anfang dieser Abfolge der bedeutsame Teil ist, behalten Sie das, was bereits in der Warteschlange steht, und weisen den Neuankömmling ab.
qopDisconnect ist richtig, wenn ein Client, der nicht mithalten kann, ein Client ist, den Sie nicht wollen. Es ist die ehrliche Variante: Statt dieser Verbindung stillschweigend eine löchrige, beschädigte Sicht auf den Stream zu servieren, schließen Sie sie. Der WatchDog des Clients verbindet sich neu, er abonniert erneut, und er startet wieder aus einem bekannten Zustand. Ein Client, der weiß, dass er getrennt wurde, kann sich erholen. Ein Client, der nicht weiß, dass er Nachrichten verloren hat, kann es nicht.
// 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;
Was auch immer Sie wählen, wählen Sie es bewusst. Nachrichten zu verwerfen ist ein Kompromiss, und der Sinn dieser drei Werte ist, dass Sie sich aussuchen können, welchen Kompromiss Sie eingehen, statt ihn in der Produktion zu entdecken.
Bescheid wissen, bevor verworfen wird: OnSendBufferFull
Eine Warteschlange, die stillschweigend Nachrichten verwirft, ist eine Warteschlange, die Sie anlügt. OnSendBufferFull ist die Notluke. Das Ereignis wird ausgelöst, bevor irgendetwas verworfen wird, es übergibt Ihnen die Verbindung und die aktuelle Größe der Warteschlange, und der Parameter var DropMessage erlaubt Ihrem Code, das Verwerfen zu verhindern.
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;
Setzen Sie DropMessage := False, und die Überlaufstrategie wird für diese Nachricht übersprungen: Sie wird trotzdem eingereiht, oberhalb der Grenze. Nutzen Sie das sparsam, denn ein Veto, das Sie immer einlegen, ist dasselbe wie gar keine Grenze. Der alltägliche Nutzen des Ereignisses ist der langweilige, und zugleich der wertvolle: zu wissen, dass es passiert ist, welcher Verbindung es passiert ist, und wie weit sie zurückgefallen war. Diese Zahl ist Ihre Frühwarnung, lange bevor dem Prozess der Speicher ausgeht.
OnSendBufferFull ist bei TsgcWebSocketClient, TsgcWebSocketServer und TsgcWebSocketHTTPServer published.
Was tun, wenn Sie sich nicht leisten können, irgendetwas zu verwerfen
Manchmal lautet die ehrliche Antwort, dass keine Nachricht jemals verloren gehen darf. Wenn das Ihr Fall ist, ist keine der drei Strategien oben das, was Sie wollen, und eine größere Warteschlange auch nicht. Eine größere Warteschlange behebt das Problem nicht, sie schiebt es nur auf, und sie schiebt es auf einen schlechteren Moment: weiter hinein in einen Burst, tiefer in den Speicherdruck, und weiter weg von dem Punkt, an dem Sie noch etwas hätten unternehmen können.
Die Antwort ist ein anderer Entwurf. Halten Sie unzugestellte Nachrichten nicht in einer Socket-Warteschlange, sondern speichern Sie sie dauerhaft, und lassen Sie den Client abholen, was er verpasst hat, sobald er dazu in der Lage ist. Genau das leisten Nachrichtenverlauf und Wiederherstellung nach dem Reconnect: Der Server führt einen begrenzten Verlauf pro Kanal mit Offsets, und ein Client, der zurückgefallen ist oder dessen Verbindung abgebrochen ist, spielt beim erneuten Abonnieren die verpassten Nachrichten nach. Kombinieren Sie das mit qopDisconnect, und Sie erhalten die stärkste verfügbare Kombination: Ein Client, der nicht mithalten kann, wird schnell abgeschnitten, und wenn er zurückkommt, holt er alles nach, was er verpasst hat, statt so zu tun, als hätte er nichts verpasst.
Unbegrenzt bleibt die Vorgabe
MaxQueueSize ist auf 0 voreingestellt, was unbegrenzt bedeutet, also genau das Verhalten, das sgcWebSockets vorher hatte. Ein Update ändert nichts. Der Überlaufpfad wird nicht einmal betreten, solange Sie keine positive Grenze setzen, bestehende Server reihen also weiter so ein, wie sie es immer getan haben, bis Sie sich anders entscheiden.
Allerdings: Wenn Sie an Clients senden, die Sie nicht kontrollieren, und im öffentlichen Internet kontrollieren Sie sie nie, dann ist eine unbegrenzte Warteschlange pro Verbindung eine unbefristete Speicherzusage, die Sie im Namen des schlechtesten Clients geben, der sich je bei Ihnen verbinden wird. Eine Grenze zu setzen kostet eine Zeile.
Verfügbarkeit
Die Grenzen für die Sendewarteschlange, die drei Überlaufstrategien und OnSendBufferFull erscheinen in sgcWebSockets 2026.7 für Delphi 7 bis 13 und C++Builder, auf Win32/Win64, Linux64, macOS, Android und iOS.
Kunden mit einem aktiven Abonnement können den neuen Build im Kundenbereich herunterladen. Testbenutzer finden den aktualisierten Installer unter esegece.com/products/websockets/download.
Fragen, Feedback oder Hilfe bei der Wahl der richtigen Strategie für Ihren Feed? Kontaktieren Sie uns, und Sie erhalten eine Antwort von den Leuten, die den Code geschrieben haben.
