FinTechSep 9, 202610 min de lectura

Datos de mercado L2 bajo ráfagas: gaps de secuencia, recuperación y fan-out a cientos de sesiones

Datos de mercadoLibro de órdenesInfraestructura de tradingFan-out
Error al cargar la imagen

Un handler que pierde y reordena actualizaciones del libro de órdenes bajo ráfagas suele ser dos fallos distintos detrás de un mismo síntoma: el manejo de la secuencia en el lado de ingesta, y una capa de distribución donde una sesión lenta cambia lo que recibe cada una de las demás. Así se construyen la secuencia, la recuperación, el fan-out y el back pressure, y aquí es donde el conflation deja de ser honesto.

Un handler que pierde y reordena actualizaciones L2 bajo ráfagas suele ser dos fallos distintos detrás de un mismo síntoma. Uno vive en el lado de ingesta, donde se sigue la secuencia del feed y un gap hay que detectarlo, no absorberlo. El otro vive en el lado de distribución, donde un libro normalizado se reparte en fan-out a muchas sesiones y un solo lector lento cambia lo que reciben los demás.

Tienen arreglos distintos, y aplicar el equivocado mueve el síntoma en lugar de eliminarlo. Lo que sigue es la forma del pipeline cuando la secuencia tiene que sobrevivir a una ráfaga: qué es en realidad un gap, cómo la recuperación empalma un snapshot con un flujo en vivo, cómo el fan-out decide el orden una sola vez y dónde el conflation es honesto.

La respuesta corta es estructural. El orden se decide en un único lugar, aguas arriba de cada sesión: un único escritor por instrumento pliega el feed numerado en un libro, y las sesiones reciben vistas derivadas de ese plegado - nunca reordenan nada por su cuenta. Lo que amBrain puede sustentar públicamente: un mini-exchange que construimos funciona en producción en la colocación de MOEX, construimos el terminal de trading Spectre Trade, y la latencia de datos de mercado que publicamos está medida: menos de 5 ms en las rutas que construimos. Esa cifra describe nuestras rutas, no un benchmark del diseño que sigue.

Un número de secuencia promete orden, no entrega

Los feeds numeran sus actualizaciones, y ese número es la única autoridad de orden que tienes. La hora de llegada no lo es: las rutas multicast reordenan, varios canales transportan un mismo instrumento, las colas de recepción se reparten entre núcleos y una ráfaga estira todo eso. Un handler que ordena por llegada solo es correcto mientras la red está tranquila - justo la condición que a nadie preocupaba.

Hay seis propiedades del feed que hay que conocer antes de escribir la lógica de recuperación. Cada una cambia lo que significa un gap.

  • La unidad que cubre la secuencia - canal, instrumento o libro. Un número por canal no te dice qué instrumento perdió una actualización, y un número por instrumento no te dice que un canal se paró
  • La regla de incremento: estrictamente consecutiva dentro de la unidad, o creciente con huecos permitidos. Las dos existen, y leer la segunda como si fuera la primera produce recuperaciones que nunca hicieron falta
  • Si los números se reinician en el límite de una sesión y qué lo marca - un reinicio leído como un gap manda todos los instrumentos a recuperación en el mismo momento
  • Si los heartbeats llevan la secuencia actual. Sin ellos, una conexión muerta y un instrumento tranquilo parecen lo mismo
  • Si existe retransmisión y sobre qué ventana. Si no existe, la recuperación por snapshot es el único camino de vuelta, y tiene que ser lo bastante barata para usarla a menudo
  • Con qué número de secuencia está alineado un snapshot. Sin él, un snapshot no se puede empalmar con un flujo en vivo en absoluto

Cuando una propiedad es realmente desconocida, mídela en lugar de codificar una suposición. Cada una de las seis se convierte en una rama de la ruta de recuperación, y una suposición equivocada ahí se descubre más tarde como un libro que discrepa en silencio con el venue.

Un gap y un reordenamiento son idénticos durante unos milisegundos

Ambos empiezan igual: la siguiente actualización no lleva el número que esperabas. La diferencia es el tiempo, así que la clasificación no se hace a la llegada, sino cuando vence una espera acotada.

  • Fuera de orden: esperabas N, recibiste N+2 y N+1 llega mientras la espera sigue abierta. No falta nada, y el único coste es la espera
  • Duplicado o retransmisión: un número igual o inferior al último aplicado. Se descarta sin tocar el libro, y se cuenta, porque una tasa de duplicados en aumento dice algo sobre la ruta
  • Gap: la espera venció y N+1 nunca llegó. El libro no puede avanzar más allá del hueco, y este instrumento pasa a recuperación
  • Obsoleto: el número correcto, demasiado tarde para servir de algo. Los bytes llegaron, y aguas abajo eso es una pérdida

Una sola regla impide que la corrupción se vuelva silenciosa: una actualización se aplica solo cuando su secuencia es exactamente la esperada. Todo lo demás va al buffer de espera o a recuperación. Un libro que acepta un delta fuera de orden sigue sirviendo precios y parece sano - la discrepancia con el venue se descubre más tarde, por un cliente, en un fill que no tenía sentido.

La espera es una estructura acotada, no una cola que crece. Guarda las actualizaciones por delante del número esperado, indexadas por secuencia, así que liberarlas es una búsqueda y no una ordenación.

  • La liberación es un bucle: aplica el número esperado y luego aplica lo que ya está en buffer mientras los números sigan siendo consecutivos
  • El plazo se expresa en tiempo, no solo en un recuento de actualizaciones pendientes - una ráfaga llena una ventana basada en recuento mucho antes de lo que pretendía el diseño
  • Todo lo que espera el buffer, lo espera cada consumidor. Dimensiona el plazo a partir del reordenamiento medido en tu propia ruta, no de un número que pareció seguro
  • El desbordamiento del buffer es en sí mismo una declaración de gap: la espera está acotada tanto en memoria como en tiempo
  • La espera es por instrumento o por canal, nunca global. Un instrumento tranquilo no debe frenar todo lo que hay a su alrededor

La recuperación empalma un snapshot con el flujo que ya guardabas en buffer

El empalme es la parte que sale mal. Un snapshot es un libro a fecha de cierto número de secuencia, y está obsoleto en el momento en que se produce; lo que lo hace utilizable es el flujo incremental guardado en buffer mientras se obtenía.

  • Almacena en buffer el flujo incremental antes de pedir el snapshot. Un snapshot sin un flujo en vivo detrás ya llega por detrás del mercado
  • Lee el número de secuencia con el que el snapshot es consistente. Si el feed no publica ninguno, en la práctica el feed es solo de snapshots, y el diseño tiene que decirlo en voz alta
  • Descarta las actualizaciones en buffer con secuencia igual o inferior a la del snapshot y aplica el resto en orden. Si la primera de ellas no es la actualización inmediatamente posterior al snapshot, el empalme falló y la recuperación vuelve a empezar
  • Si el buffer se llena antes de que llegue el snapshot, reinicia la recuperación en lugar de aplicarla a medias - una recuperación aplicada parcialmente es indistinguible de un libro sano
  • Publica el instrumento como degradado mientras se recupera, como un estado explícito en el flujo. Un libro con un hueco, servido como si fuera el actual, es peor que no tener libro
  • Verifica después del empalme: el checksum que publica el feed, si publica alguno, o la concordancia entre tu libro plegado y el siguiente snapshot

La recuperación es un evento normal, no un incidente, y su coste pertenece al plan de capacidad: cuánto tarda en obtenerse un snapshot, cuánto flujo se guarda en buffer mientras tanto y cuántos instrumentos pueden recuperarse a la vez antes de que el servicio de snapshots se convierta en el cuello de botella.

Fan-out: normaliza una vez, codifica una vez, envía a muchos

Cientos de sesiones de terminal quieren el mismo libro. El error que se multiplica en una ráfaga es hacer por sesión un trabajo que no es por sesión por naturaleza: reconstruir un libro para cada suscriptor, o serializar la misma actualización una vez por socket.

  • Un único escritor por shard de instrumentos es el dueño del libro. Los lectores nunca lo mutan, lo que elimina a la vez el lock y la pregunta de qué versión manda
  • El escritor publica actualizaciones versionadas en un ring buffer que los lectores siguen a su propio ritmo, así que un lector que se queda atrás no frena a nadie
  • Cada actualización se codifica una vez por formato de wire y se comparte entre sesiones por referencia. Solo el framing y el control de flujo son por sesión
  • Cada sesión lleva su propio número de secuencia de salida, así que el cliente puede detectar sus propias pérdidas sin saber nada del feed aguas arriba
  • El orden se garantiza por instrumento, porque esa es la garantía de la que dependen los clientes. El orden entre instrumentos o se promete explícitamente y se implementa, o no se promete en absoluto
  • Más allá de un solo proceso, el fan-out se convierte en una capa de relés: cada relé toma una suscripción aguas arriba y atiende a una parte de las sesiones, de modo que el trabajo del escritor se mantiene constante

El coste del fan-out lo decide cuántas veces se transforma una actualización, no cuántos sockets la reciben. Codificar una vez y pasar una referencia escala con las sesiones; reconstruir un libro por sesión no.

Un consumidor lento es una política que eliges, no un accidente que ocurre

En algún sitio hay una sesión en una red mala, o un terminal cuyo bucle de render se atascó, y su buffer de salida se llena. Hay cuatro comportamientos posibles, y dos de ellos solo se eligen por accidente.

  • Bloquear al escritor hasta que la sesión lenta se vacíe: nunca. Convierte una mala conexión en un evento de latencia para todos los del shard
  • Hacer crecer la cola sin límite: un consumidor lento se convierte en agotamiento de memoria y luego en una caída que nada tiene que ver con la sesión original
  • Cola acotada con conflation: correcto para el estado del libro, donde el cliente quiere la foto actual y no cada paso intermedio
  • Cola acotada con desconexión al alcanzar el umbral alto: correcto para flujos a los que no se puede aplicar conflation, donde descartar un elemento descarta significado
  • Sea cual sea la política, la cola es por sesión y el retraso se mide de forma continua - la profundidad de la cola y la distancia entre la secuencia publicada y la secuencia escrita en el socket
  • Una desconexión declara su motivo. Un cierre sin explicación se reintenta en bucle; uno explicado va seguido de una resuscripción

El back pressure es donde se encuentran los dos lados. Si la ruta de salida puede empujar hacia atrás al escritor del libro, un terminal lento acaba retrasando el plegado del feed, y la detección de gaps empieza a dispararse por motivos que nada tienen que ver con el venue. Un ring acotado entre ambos corta esa cadena.

El conflation es honesto para el estado y erróneo para los eventos

Un libro es estado: el cliente quiere los niveles actuales, y un valor ya sustituido no tiene significado propio. Una cinta de operaciones es un registro de eventos, donde cada elemento es un hecho ocurrido que no se puede resumir hasta hacerlo desaparecer.

  • Con conflation: actualizaciones de niveles de precio, top of book, profundidad agregada y estadísticas derivadas como el último precio o el volumen de la sesión
  • Sin conflation: trades y prints, informes de órdenes y de ejecución, subastas y cambios de fase, y todo lo que un cliente agregue a lo largo del tiempo - una cinta construida a partir de un flujo con conflation es un número equivocado sostenido con confianza
  • Aplica conflation por clave, no por flujo. Quedarse con la última actualización de cada nivel de precio conserva el libro; quedarse solo con la última actualización global tira todos los niveles que no cambiaron al final
  • Una actualización con conflation lleva el número de secuencia del estado que representa, así que el cliente sabe a qué punto corresponde
  • El intervalo de conflation forma parte de la latencia que reportas. Un flujo con conflation por intervalos no queda descrito por la latencia medida en el flujo sin conflation
  • Un cliente que necesita cada estado intermedio - un backtest, un registro de cumplimiento - toma el flujo sin conflation y lo paga en ancho de banda

El conflation es un cambio de forma, no un ajuste de compresión. Una vez que un flujo lleva conflation, el cliente no puede reconstruir lo que pasó entre dos actualizaciones, y no se le debe decir que el flujo está completo. Publicar los dos - un flujo del libro con conflation y un flujo de eventos sin él - es lo que mantiene correctos a ambos tipos de cliente.

Reconectar es resincronizar, y todas llegan a la vez

Cuando una sesión vuelve, el libro que conserva no vale nada salvo que el servidor pueda demostrar la continuidad. Por defecto se envía un snapshot nuevo por suscripción, con su número de secuencia, aplicado a un cliente que antes ha descartado su estado local.

  • La reanudación desde un número de secuencia solo se ofrece cuando existe un buffer de replay acotado. Cuando el número solicitado ha caducado, el servidor lo dice y recurre a un snapshot en lugar de enviar un flujo con un hueco
  • El estado de sesión a través de una reconexión es una decisión explícita: o el servidor conserva las suscripciones durante un tiempo acotado bajo un token de sesión, o el cliente las vuelve a declarar al conectar. Ambas funcionan; una mezcla implícita no
  • La entrega duplicada tras una reanudación es esperable, y el cliente descarta por secuencia. At-least-once más numeración de secuencia es más fácil de implementar correctamente que exactly-once
  • Las reconexiones llegan juntas, porque lo que desconectó a una sesión suele haber desconectado a muchas. Un backoff con jitter en el cliente y control de admisión en el servidor evitan que la recuperación se convierta en la segunda caída
  • Los snapshots para esa avalancha salen de una caché por instrumento refrescada a un ritmo fijo, de modo que el escritor serializa un snapshot según un calendario y no una vez por cada sesión que se reconecta
  • El libro del lado del cliente se reconstruye, nunca se parchea. Un terminal que conserva sus niveles antiguos y aplica encima los nuevos deltas arrastra el error previo a la desconexión hasta un libro que ahora parece nuevo

El fallo para el que merece la pena diseñar no es una reconexión suelta. Es un evento de red que devuelve cientos de sesiones en el mismo segundo, cada una pidiendo un snapshot de cada instrumento que estaba observando, mientras el lado de ingesta se recupera del gap que produjo ese mismo evento.

Lo que amBrain puede sustentar públicamente: construimos plataformas de trading de baja latencia, matching engines y sistemas de real-time bidding en Rust desde Ereván, Armenia, y la latencia de datos de mercado que publicamos - menos de 5 ms - está medida en las rutas que construimos. Si tu handler pierde la secuencia bajo ráfagas, la conversación que merece la pena es la que separa el lado de ingesta del lado de distribución antes de reescribir ninguno de los dos.

¿Tiene un diseño así sobre la mesa?

Traiga su arquitectura actual y el modo de fallo que le preocupa, y lo repasaremos juntos en media hora.

Artículos relacionados

Error al cargar la imagen
FinTech
Sep 9, 202610 min de lectura

Contratar ingenieros o traer un socio técnico: cómo calcular el coste de ambas vías

Leer artículo
Error al cargar la imagen
FinTech
Sep 8, 20269 min de lectura

Diseñar un matching engine en Rust: prioridad precio-tiempo sin pausas de GC

Leer artículo
Error al cargar la imagen
FinTech
Sep 8, 20268 min de lectura

Verificaciones de riesgo pre-trade dentro de la ruta de la orden

Leer artículo