Backpressure: limity kolejki wysyłania dla wolnych klientów WebSocket | Blog eSeGeCe

Backpressure: limity kolejki wysyłania dla wolnych klientów WebSocket

· Komponenty

Serwer, który rozgłasza szybki strumień danych, produkuje wiadomości we własnym tempie, a nie w tempie najwolniejszego klienta. Dane giełdowe, telemetria, podgląd logów na żywo: serwer wypycha dane i oczekuje, że każdy podłączony klient nadąży. Większość nadąża. Jeden z nich, prędzej czy później, nie nadąży.

Gdy klient przestaje opróżniać swoje gniazdo, wysyłane do niego wiadomości nie znikają. Piętrzą się w kolejce wysyłania tego połączenia, a kolejka to pamięć. Telefon, który wjechał do tunelu kolejowego, karta przeglądarki, której obniżono priorytet, proces klienta, który przestał wywoływać odczyt, wszystkie one po cichu zwiększają zużycie pamięci twojego serwera, aż coś padnie. sgcWebSockets 2026.7 pozwala ograniczyć tę kolejkę i zdecydować, co się dzieje, gdy jest pełna.

Wolny konsument

Problemem nie jest to, że sieć jest wolna, tylko niedopasowanie tempa. Twój strumień publikuje 500 wiadomości na sekundę. Klient na kiepskim łączu odbiera 50. Pozostałe 450 musi gdzieś trafić, a tym "gdzieś" jest lista utrzymywana dla każdego połączenia w procesie serwera. Po minucie to jedno połączenie przechowuje dziesiątki tysięcy wiadomości. Po dziesięciu minutach przechowuje twój RAM.

Szkody nie ograniczają się zresztą do wolnego klienta. Pamięć, którą pochłania, to pamięć, której nie dostają zdrowi klienci, a gdy proces zacznie korzystać z pliku wymiany albo alokator zacznie się dławić, opóźnienia rosną wszystkim. Jeden telefon w tunelu zamienia się w awarię dla wszystkich.

Do tej pory kolejka wysyłania w sgcWebSockets była nieograniczona, co jest w porządku, dopóki każdy klient nadąża, i staje się nieprzyjemne, gdy pierwszy z nich przestanie nadążać. Rozwiązaniem jest nałożenie na nią górnego limitu.

Ograniczanie kolejki

Kolejka znajduje się we właściwości komponentu QueueOptions, obok istniejących podopcji poziomu Text, Binary i Ping. Ograniczają ją dwie nowe właściwości: MaxQueueSize określa, ile oczekujących wiadomości może przechowywać pojedyncze połączenie, a OverflowPolicy mówi, co zrobić z kolejną wiadomością po osiągnięciu tego limitu.

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 jest właściwością published w komponentach TsgcWebSocketServer, TsgcWebSocketHTTPServer i TsgcWebSocketClient, a komponent kopiuje ją do każdego przyjmowanego połączenia, więc limit obowiązuje na połączenie, a nie na serwer. Tysiąc połączeń z MaxQueueSize = 1000 daje w najgorszym przypadku tysiąc oczekujących wiadomości na każde z nich, a ten najgorszy przypadek jest teraz liczbą, którą możesz sobie wyliczyć na kartce, zanim wdrożysz rozwiązanie.

Wybór zasady przepełnienia

Nie ma tu jednej uniwersalnie poprawnej odpowiedzi i właśnie dlatego zasady są trzy. To, której potrzebujesz, zależy od tego, co znaczą twoje wiadomości.

qopDropOldest nadaje się do strumienia na żywo, w którym liczy się tylko najnowsza wartość. Notowanie ceny, odczyt czujnika, wskaźnik na pulpicie: wiadomość sprzed czterech sekund traci wartość w chwili, gdy nadchodzi kolejna. Odrzucanie z początku kolejki oznacza, że klient, który został w tyle, a potem się pozbierał, dostaje aktualny obraz, a nie zaległości pełne przestarzałych danych, przez które musi się przegryźć, zanim nadrobi. To jest ustawienie domyślne.

qopDropNewest sprawdza się wtedy, gdy liczą się wcześniejsze wiadomości i wolisz odrzucić nowe obciążenie. Jeśli klient jest prowadzony przez pewną sekwencję, a początek tej sekwencji jest tym, co ma znaczenie, zachowaj to, co już stoi w kolejce, i odrzuć nowo przybyłą wiadomość.

qopDisconnect sprawdza się wtedy, gdy klient, który nie nadąża, jest klientem, którego nie chcesz. To uczciwa opcja: zamiast po cichu karmić to połączenie dziurawym, zniekształconym obrazem strumienia, po prostu je zamykasz. WatchDog klienta łączy się ponownie, klient odnawia subskrypcje i zaczyna od znanego stanu. Klient, który wie, że został rozłączony, potrafi się pozbierać. Klient, który nie wie, że stracił wiadomości, nie potrafi.

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

Cokolwiek wybierzesz, wybierz to świadomie. Odrzucanie wiadomości jest kompromisem, a sens tych trzech wartości polega na tym, że sam wybierasz, jaki kompromis zawierasz, zamiast odkrywać go na produkcji.

Wiedzieć, zanim coś odrzucisz: OnSendBufferFull

Kolejka, która po cichu odrzuca wiadomości, to kolejka, która cię okłamuje. OnSendBufferFull jest wyjściem awaryjnym. Uruchamia się zanim cokolwiek zostanie odrzucone, przekazuje ci połączenie oraz bieżący rozmiar kolejki, a parametr var DropMessage pozwala twojemu kodowi zawetować odrzucenie.

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;

Ustaw DropMessage := False, a zasada przepełnienia zostanie pominięta dla tej wiadomości: mimo wszystko trafi ona do kolejki, ponad limit. Korzystaj z tego oszczędnie, bo weto stosowane zawsze jest tym samym, co brak jakiegokolwiek limitu. Codzienne zastosowanie tego zdarzenia jest nudne i zarazem najbardziej wartościowe: wiedzieć, że przepełnienie wystąpiło, któremu połączeniu się to przydarzyło i jak bardzo zostało ono w tyle. Ta liczba jest twoim wczesnym ostrzeżeniem, na długo zanim procesowi zabraknie pamięci.

OnSendBufferFull jest zdarzeniem published w komponentach TsgcWebSocketClient, TsgcWebSocketServer i TsgcWebSocketHTTPServer.

Co zrobić, gdy nie możesz sobie pozwolić na odrzucenie czegokolwiek

Czasem uczciwa odpowiedź brzmi, że żadna wiadomość nie może zostać utracona. Jeśli tak jest w twoim przypadku, żadna z trzech powyższych zasad nie jest tym, czego potrzebujesz, i nie jest nim też większa kolejka. Większa kolejka nie rozwiązuje problemu, tylko go odsuwa, i to na gorszy moment: głębiej w środek nawału danych, głębiej w niedobór pamięci i dalej od punktu, w którym mogłeś jeszcze coś z tym zrobić.

Odpowiedzią jest inny projekt rozwiązania. Nie przechowuj niedostarczonych wiadomości w kolejce gniazda, tylko zapisz je trwale i pozwól klientowi pobrać to, co go ominęło, gdy będzie w stanie to zrobić. Dokładnie to robi historia wiadomości i odzyskiwanie po ponownym połączeniu: serwer utrzymuje ograniczoną historię dla każdego kanału wraz z przesunięciami, a klient, który został w tyle albo stracił połączenie, odtwarza pominięte wiadomości przy ponownej subskrypcji. Połącz to z qopDisconnect, a otrzymasz najmocniejszą dostępną kombinację: klient, który nie nadąża, zostaje szybko odcięty, a gdy wraca, odzyskuje wszystko, co go ominęło, zamiast udawać, że nic go nie ominęło.

Brak limitu nadal jest ustawieniem domyślnym

MaxQueueSize ma domyślną wartość 0, co oznacza brak limitu, czyli dokładnie takie zachowanie, jakie sgcWebSockets miał wcześniej. Aktualizacja niczego nie zmienia. Ścieżka obsługi przepełnienia nie jest nawet uruchamiana, dopóki nie ustawisz dodatniego limitu, więc istniejące serwery kolejkują tak jak zawsze, dopóki nie zdecydujesz inaczej.

Mimo to, jeśli rozgłaszasz do klientów, których nie kontrolujesz, a w publicznym internecie nigdy ich nie kontrolujesz, nieograniczona kolejka na każde połączenie jest bezterminowym zobowiązaniem pamięciowym zaciągniętym w imieniu najgorszego klienta, jaki kiedykolwiek się z tobą połączy. Ustawienie limitu kosztuje jedną linijkę.

Dostępność

Limity kolejki wysyłania, trzy zasady przepełnienia oraz OnSendBufferFull są dostępne w sgcWebSockets 2026.7 dla Delphi 7 do 13 oraz C++Builder, na Win32/Win64, Linux64, macOS, Android i iOS.

Klienci z aktywną subskrypcją mogą pobrać nową wersję ze strefy klienta. Użytkownicy wersji próbnej mogą pobrać zaktualizowany instalator ze strony esegece.com/products/websockets/download.

Pytania, uwagi albo pomoc w doborze właściwej zasady dla twojego strumienia? Skontaktuj się z nami, a odpowiedź otrzymasz od osób, które napisały ten kod.