Contrapresión: límites de la cola de envío para clientes WebSocket lentos | Blog de eSeGeCe

Contrapresión: límites de la cola de envío para clientes WebSocket lentos

· Componentes

Un servidor que difunde un flujo rápido produce mensajes a su propio ritmo, no al ritmo del cliente más lento. Datos de mercado, telemetría, la cola de un registro en vivo: el servidor empuja, y se espera que todos los clientes conectados sigan el ritmo. La mayoría lo hace. Uno de ellos, antes o después, no.

Cuando un cliente deja de vaciar su socket, los mensajes que le envías no desaparecen. Se amontonan en la cola de envío de esa conexión, y la cola es memoria. Un teléfono que se ha metido en un túnel de tren, una pestaña del navegador que ha quedado ralentizada, un proceso cliente que ha dejado de llamar a read, todos ellos hacen crecer en silencio la memoria de tu servidor hasta que algo se cae. sgcWebSockets 2026.7 te permite acotar esa cola y decidir qué ocurre cuando está llena.

El consumidor lento

El problema no es que la red sea lenta, es el desajuste. Tu flujo publica 500 mensajes por segundo. Un cliente con un enlace malo vacía 50. Los otros 450 tienen que ir a alguna parte, y esa "alguna parte" es una lista por conexión dentro del proceso del servidor. Al cabo de un minuto, esa única conexión está guardando decenas de miles de mensajes. Al cabo de diez minutos, está guardando tu RAM.

El daño tampoco se limita al cliente lento. La memoria que se come es memoria que no reciben los clientes sanos y, en cuanto el proceso empieza a paginar o el asignador empieza a sufrir, la latencia de todo el mundo se va con ella. Un teléfono en un túnel se convierte en una caída de servicio para todos.

Hasta ahora la cola de envío de sgcWebSockets no tenía límite, lo cual está bien mientras todos los clientes sigan el ritmo, y resulta desagradable la primera vez que uno no lo hace. La solución es ponerle un techo.

Acotar la cola

La cola vive bajo QueueOptions del componente, junto a las subopciones de nivel Text, Binary y Ping que ya existían. Dos propiedades nuevas la acotan: MaxQueueSize limita cuántos mensajes pendientes puede retener una sola conexión, y OverflowPolicy dice qué hacer con el siguiente mensaje una vez alcanzado ese límite.

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á publicada en TsgcWebSocketServer, TsgcWebSocketHTTPServer y TsgcWebSocketClient, y el componente la copia a cada conexión a medida que la acepta, así que el límite es por conexión, no por servidor. Mil conexiones con MaxQueueSize = 1000 te dan un peor caso de mil mensajes pendientes en cada una, y ese peor caso es ahora un número que puedes multiplicar sobre el papel antes de desplegar.

Elegir una política de desbordamiento

Aquí no hay una respuesta universalmente correcta, y por eso hay tres. La política que quieres depende de lo que signifiquen tus mensajes.

qopDropOldest es la adecuada para un flujo en vivo en el que solo importa el valor más reciente. Una cotización, la lectura de un sensor, un indicador de un panel: el mensaje de hace cuatro segundos no vale nada en el momento en que llega el siguiente. Descartar por el principio de la cola significa que el cliente que se quedó atrás y luego se recuperó recibe la foto actual, no un atasco de fotos rancias que tiene que masticar antes de ponerse al día. Esta es la política por defecto.

qopDropNewest es la adecuada cuando los mensajes anteriores son los que importan y prefieres soltar la carga nueva. Si estás guiando a un cliente a través de una secuencia y la cabecera de esa secuencia es la parte con sentido, conserva lo que ya está encolado y rechaza al recién llegado.

qopDisconnect es la adecuada cuando un cliente que no puede seguir el ritmo es un cliente que no quieres. Es la opción honesta: en lugar de alimentar en silencio a esa conexión con una vista del flujo llena de huecos y corrompida, la cierras. El WatchDog del cliente reconecta, vuelve a suscribirse, y empieza de nuevo desde un estado conocido. Un cliente que sabe que fue desconectado puede recuperarse. Un cliente que no sabe que perdió mensajes, no.

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

Elijas lo que elijas, elígelo a propósito. Descartar mensajes es un compromiso, y el sentido de estos tres valores es que puedes elegir qué compromiso estás asumiendo en lugar de descubrirlo en producción.

Saber antes de descartar: OnSendBufferFull

Una cola que descarta mensajes en silencio es una cola que te miente. OnSendBufferFull es la vía de escape. Se dispara antes de que se descarte nada, te entrega la conexión y el tamaño actual de la cola, y el parámetro var DropMessage permite que tu código vete el 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;

Establece DropMessage := False y la política de desbordamiento se omite para ese mensaje: se encola de todos modos, por encima del límite. Usa eso con moderación, porque un veto que aplicas siempre es lo mismo que no tener límite alguno. El uso cotidiano del evento es el aburrido, y el valioso: saber que ocurrió, a qué conexión le ocurrió y cuánto se había quedado atrás. Ese número es tu aviso temprano, mucho antes de que el proceso se quede sin memoria.

OnSendBufferFull está publicado en TsgcWebSocketClient, TsgcWebSocketServer y TsgcWebSocketHTTPServer.

Qué hacer cuando no te puedes permitir descartar nada

A veces la respuesta honesta es que no se puede perder ningún mensaje. Si ese es tu caso, ninguna de las tres políticas anteriores es lo que quieres, y una cola más grande tampoco. Una cola más grande no arregla el problema, solo lo aplaza, y lo aplaza a un momento peor: más adentro de una ráfaga, más hundido en la presión de memoria, y más lejos del punto en el que todavía podrías haber hecho algo al respecto.

La respuesta es un diseño distinto. No retengas los mensajes no entregados en una cola de socket, persístelos, y deja que el cliente recupere lo que se perdió cuando pueda. Eso es exactamente lo que hace el historial de mensajes y la recuperación al reconectar: el servidor mantiene un historial acotado por canal con desplazamientos, y un cliente que se quedó atrás o que perdió su conexión reproduce los mensajes que se perdió al volver a suscribirse. Combina eso con qopDisconnect y obtienes la combinación más fuerte disponible: un cliente que no puede seguir el ritmo es soltado rápido y, cuando vuelve, recupera todo lo que se perdió en lugar de fingir que no se perdió nada.

Sin límite sigue siendo el valor por defecto

MaxQueueSize vale 0 por defecto, lo que significa sin límite, que es exactamente el comportamiento que sgcWebSockets tenía antes. Actualizar no cambia nada. Ni siquiera se entra en el camino del desbordamiento a menos que establezcas un límite positivo, así que los servidores existentes siguen encolando como siempre lo han hecho hasta que decidas otra cosa.

Dicho esto, si difundes a clientes que no controlas, y en la internet pública nunca los controlas, una cola por conexión sin límite es un compromiso de memoria abierto asumido en nombre del peor cliente que jamás se vaya a conectar contigo. Poner un límite cuesta una línea.

Disponibilidad

Los límites de la cola de envío, las tres políticas de desbordamiento y OnSendBufferFull se incluyen en sgcWebSockets 2026.7 para Delphi 7 a 13 y C++Builder, en Win32/Win64, Linux64, macOS, Android e iOS.

Los clientes con una suscripción activa pueden descargar la nueva versión desde el área de clientes. Los usuarios de la versión de prueba pueden obtener el instalador actualizado en esegece.com/products/websockets/download.

¿Preguntas, comentarios o ayuda para elegir la política adecuada para tu flujo? Ponte en contacto, y recibirás una respuesta de las personas que escribieron el código.