Backpressure: limites da fila de envio para clientes WebSocket lentos | Blog eSeGeCe

Backpressure: limites da fila de envio para clientes WebSocket lentos

· Componentes

Um servidor que difunde um feed rápido produz mensagens no seu próprio ritmo, não no ritmo do cliente mais lento. Dados de mercado, telemetria, um log ao vivo: o servidor envia, e espera-se que cada cliente conectado acompanhe. A maioria acompanha. Um deles, mais cedo ou mais tarde, não acompanha.

Quando um cliente para de esvaziar o seu socket, as mensagens que você envia a ele não desaparecem. Elas se acumulam na fila de envio daquela conexão, e a fila é memória. Um celular que entrou em um túnel de trem, uma aba do navegador que foi limitada, um processo cliente que parou de chamar a leitura, todos eles fazem crescer silenciosamente a memória do seu servidor até que algo desmorone. O sgcWebSockets 2026.7 permite limitar essa fila e decidir o que acontece quando ela está cheia.

O consumidor lento

O problema não é a rede estar lenta, é o descompasso. O seu feed publica 500 mensagens por segundo. Um cliente em um enlace ruim consome 50. As outras 450 têm de ir para algum lugar, e esse "algum lugar" é uma lista por conexão dentro do processo do servidor. Depois de um minuto, aquela única conexão está segurando dezenas de milhares de mensagens. Depois de dez minutos, está segurando a sua RAM.

E o estrago não se limita ao cliente lento. A memória que ele consome é memória que os clientes saudáveis não recebem e, assim que o processo começa a paginar ou o alocador começa a sofrer, a latência de todo mundo vai junto. Um celular em um túnel vira uma indisponibilidade para todos.

Até agora, a fila de envio no sgcWebSockets era ilimitada, o que é ótimo enquanto todos os clientes acompanham e desagradável na primeira vez em que um deles não acompanha. A solução é colocar um teto nela.

Limitando a fila

A fila fica em QueueOptions, no componente, ao lado das subopções de nível Text, Binary e Ping já existentes. Duas novas propriedades a limitam: MaxQueueSize define quantas mensagens pendentes uma única conexão pode reter, e OverflowPolicy diz o que fazer com a próxima mensagem assim que esse teto é alcançado.

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 é publicada em TsgcWebSocketServer, TsgcWebSocketHTTPServer e TsgcWebSocketClient, e o componente a copia para cada conexão à medida que ela é aceita, portanto o limite é por conexão, não por servidor. Mil conexões com MaxQueueSize = 1000 dão a você um pior caso de mil mensagens pendentes em cada uma, e esse pior caso agora é um número que você pode calcular no papel antes de implantar.

Escolhendo uma política de estouro

Não existe uma resposta universalmente correta aqui, e é por isso que existem três. A política que você quer depende do que as suas mensagens significam.

qopDropOldest é a escolha certa para um feed ao vivo em que só o valor mais recente importa. Uma cotação, uma leitura de sensor, um medidor de painel: a mensagem de quatro segundos atrás perde o valor no momento em que a seguinte chega. Descartar a partir do início da fila faz com que o cliente que ficou para trás e depois se recuperou receba o quadro atual, e não um acúmulo de mensagens obsoletas que ele teria de mastigar antes de se atualizar. Esta é a política padrão.

qopDropNewest é a escolha certa quando as mensagens anteriores são as que importam e você prefere descartar a carga nova. Se um cliente está sendo conduzido por uma sequência e o começo dessa sequência é a parte significativa, mantenha o que já está enfileirado e recuse a recém-chegada.

qopDisconnect é a escolha certa quando um cliente que não consegue acompanhar é um cliente que você não quer. É a opção honesta: em vez de alimentar silenciosamente aquela conexão com uma visão furada e corrompida do fluxo, você a fecha. O WatchDog do cliente reconecta, ele reassina, e recomeça a partir de um estado conhecido. Um cliente que sabe que foi desconectado consegue se recuperar. Um cliente que não sabe que perdeu mensagens, não.

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

Seja qual for a sua escolha, escolha deliberadamente. Descartar mensagens é um compromisso, e o objetivo desses três valores é permitir que você escolha qual compromisso está assumindo, em vez de descobri-lo em produção.

Saber antes de descartar: OnSendBufferFull

Uma fila que descarta mensagens em silêncio é uma fila que mente para você. OnSendBufferFull é a válvula de escape. Ele dispara antes de qualquer descarte, entrega a você a conexão e o tamanho atual da fila, e o parâmetro var DropMessage permite que o seu código vete o descarte.

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;

Defina DropMessage := False e a política de estouro é ignorada para aquela mensagem: ela é enfileirada mesmo assim, acima do teto. Use isso com parcimônia, porque um veto que você sempre aplica é a mesma coisa que não ter limite nenhum. O uso cotidiano do evento é o mais monótono, e o mais valioso: saber que aquilo aconteceu, com qual conexão aconteceu, e quão para trás ela havia ficado. Esse número é o seu aviso prévio, muito antes de o processo ficar sem memória.

OnSendBufferFull é publicado em TsgcWebSocketClient, TsgcWebSocketServer e TsgcWebSocketHTTPServer.

O que fazer quando você não pode se dar ao luxo de descartar nada

Às vezes a resposta honesta é que nenhuma mensagem pode ser perdida. Se esse é o seu caso, nenhuma das três políticas acima é o que você quer, e uma fila maior também não é. Uma fila maior não resolve o problema, apenas o adia, e o adia para um momento pior: mais adiante em uma rajada, mais fundo na pressão de memória, e mais longe do ponto em que você ainda poderia ter feito algo a respeito.

A resposta é um projeto diferente. Não guarde mensagens não entregues em uma fila de socket, persista-as, e deixe o cliente buscar o que perdeu quando for capaz. É exatamente isso que o histórico de mensagens e a recuperação na reconexão fazem: o servidor mantém um histórico limitado por canal com offsets, e um cliente que ficou para trás ou perdeu a conexão reproduz as mensagens que perdeu ao reassinar. Combine isso com qopDisconnect e você obtém a combinação mais forte disponível: um cliente que não consegue acompanhar é cortado rapidamente e, quando volta, recupera tudo o que perdeu em vez de fingir que não perdeu nada.

Ilimitado continua sendo o padrão

MaxQueueSize tem 0 como padrão, o que significa ilimitado, que é exatamente o comportamento que o sgcWebSockets tinha antes. Atualizar não muda nada. O caminho de estouro nem sequer é percorrido a menos que você defina um teto positivo, portanto os servidores existentes continuam enfileirando como sempre fizeram até que você decida o contrário.

Dito isso, se você difunde para clientes que não controla, e na internet pública você nunca os controla, uma fila ilimitada por conexão é um compromisso de memória em aberto assumido em nome do pior cliente que algum dia vai se conectar a você. Definir um teto custa uma linha.

Disponibilidade

Os limites da fila de envio, as três políticas de estouro e o OnSendBufferFull chegam no sgcWebSockets 2026.7 para Delphi 7 até 13 e C++Builder, em Win32/Win64, Linux64, macOS, Android e iOS.

Os clientes com assinatura ativa podem baixar a nova versão na área de clientes. Os usuários de avaliação podem obter o instalador atualizado em esegece.com/products/websockets/download.

Dúvidas, comentários ou ajuda para escolher a política certa para o seu feed? Fale conosco, e você receberá uma resposta das pessoas que escreveram o código.