Um handler que perde e reordena atualizações do order book sob rajadas costuma ser duas falhas separadas por trás de um sintoma: o tratamento da sequência no lado da ingestão e uma camada de distribuição em que uma sessão lenta muda o que todas as outras recebem. É assim que a sequência, a recuperação, o fan-out e o backpressure são construídos - e onde a conflation deixa de ser honesta.
Um handler que perde e reordena atualizações L2 sob rajadas costuma ser duas falhas separadas por trás de um sintoma. Uma fica no lado da ingestão, onde a sequência do feed é acompanhada e um gap precisa ser detectado, e não absorvido. A outra fica no lado da distribuição, onde um book normalizado é distribuído por fan-out para muitas sessões e um único leitor lento muda o que os demais recebem.
As correções são diferentes, e aplicar a errada muda o sintoma de lugar em vez de removê-lo. O que vem a seguir é o formato do pipeline quando a sequência precisa sobreviver a uma rajada: o que é de fato um gap, como a recuperação une um snapshot a um stream ao vivo, como o fan-out decide a ordenação uma única vez e onde a conflation é honesta.
A resposta curta é estrutural. A ordem é decidida em exatamente um lugar, a montante de toda sessão: um único escritor por instrumento consolida o feed numerado em um book, e as sessões recebem visões derivadas dessa consolidação - elas nunca reordenam nada por conta própria. O que a amBrain pode sustentar publicamente: uma mini-exchange que construímos roda em produção na colocation da MOEX, construímos o terminal de trading Spectre Trade, e a latência de dados de mercado que publicamos é medida - menos de 5 ms nos caminhos que construímos. Esse número descreve os nossos caminhos, não um benchmark do design descrito abaixo.
O número de sequência promete ordem, não entrega
Os feeds numeram suas atualizações, e esse número é a única autoridade de ordenação que você tem. O horário de chegada não é: caminhos multicast reordenam, vários canais carregam um mesmo instrumento, filas de recepção ficam espalhadas por vários núcleos e uma rajada estica tudo isso. Um handler que ordena por chegada só está correto enquanto a rede está calma - justamente a condição com que ninguém se preocupava.
Seis propriedades do feed precisam ser conhecidas antes de escrever a lógica de recuperação. Cada uma muda o que um gap significa.
- A unidade que a sequência cobre - canal, instrumento ou book. Um número por canal não diz qual instrumento perdeu uma atualização, e um número por instrumento não diz que um canal parou
- A regra de incremento: estritamente consecutiva dentro da unidade, ou crescente com buracos permitidos. As duas existem, e ler a segunda como se fosse a primeira produz recuperações que nunca foram necessárias
- Se os números reiniciam na virada de sessão e o que marca isso - um reinício lido como gap manda todos os instrumentos para recuperação no mesmo instante
- Se os heartbeats carregam a sequência atual. Sem eles, uma conexão morta e um instrumento parado parecem idênticos
- Se existe retransmissão e em que janela. Se não existe, a recuperação por snapshot é o único caminho de volta, e ela precisa ser barata o bastante para ser usada com frequência
- Com qual número de sequência um snapshot está alinhado. Sem isso, um snapshot simplesmente não pode ser unido a um stream ao vivo
Quando uma propriedade é de fato desconhecida, meça em vez de codificar um palpite. Cada uma das seis vira um ramo no caminho de recuperação, e uma suposição errada ali aparece depois como um book que discorda silenciosamente da venue.
Um gap e um reordenamento são idênticos por alguns milissegundos
Os dois começam igual: a próxima atualização não traz o número que você esperava. A diferença é o tempo, então a classificação não é feita na chegada, e sim quando uma espera limitada expira.
- Fora de ordem: você esperava N, recebeu N+2, e N+1 chega enquanto a espera ainda está aberta. Nada está faltando, e o único custo é a espera
- Duplicata ou retransmissão: um número igual ou menor que o último aplicado. Descartado sem tocar no book, e contabilizado, porque uma taxa de duplicatas em alta diz algo sobre o caminho
- Gap: a espera expirou e N+1 nunca chegou. O book não pode avançar além do buraco, e esse instrumento vai para recuperação
- Atrasada: o número certo, tarde demais para ser útil. Os bytes chegaram, e a jusante isso é uma perda
Uma regra impede que a corrupção fique silenciosa: uma atualização só é aplicada quando a sequência dela é exatamente a esperada. Todo o resto vai para o buffer de espera ou para a recuperação. Um book que aceita um delta fora de ordem continua servindo preços e parece saudável - a divergência em relação à venue é descoberta depois, por um cliente, em uma execução que não fez sentido.
A espera é uma estrutura limitada, não uma fila que cresce. Ela guarda atualizações à frente do número esperado, indexadas pela sequência, então liberá-las é uma busca e não uma ordenação.
- A liberação é um laço: aplique o número esperado e, em seguida, aplique o que já está no buffer enquanto os números seguirem consecutivos
- O prazo é expresso em tempo, não apenas em uma contagem de atualizações pendentes - uma rajada enche uma janela baseada em contagem muito antes do que o design pretendia
- O que o buffer espera, todo consumidor espera. Dimensione o prazo pelo reordenamento medido no seu próprio caminho, não por um número que pareceu seguro
- O estouro do buffer é, por si só, a declaração de um gap: a espera é limitada em memória, além de ser limitada no tempo
- A espera é por instrumento ou por canal, nunca global. Um instrumento parado não pode segurar tudo o que está em volta
Recuperação é um snapshot unido a um stream que você já bufferizava
A junção é a parte que dá errado. Um snapshot é o book em um determinado número de sequência, e ele já nasce atrasado; o que o torna utilizável é o stream incremental que ficou no buffer enquanto ele era buscado.
- Comece a bufferizar o stream incremental antes de pedir o snapshot. Um snapshot sem um stream ao vivo por trás já chega atrasado em relação ao mercado
- Leia o número de sequência com o qual o snapshot é consistente. Se o feed não publica nenhum, na prática esse feed é snapshot-only, e o design precisa dizer isso em voz alta
- Descarte as atualizações no buffer com número igual ou menor que o do snapshot e aplique o resto em ordem. Se a primeira delas não for a atualização imediatamente posterior ao snapshot, a junção falhou e a recuperação recomeça
- Se o buffer encher antes de o snapshot chegar, reinicie a recuperação em vez de aplicar parte dela - uma recuperação aplicada pela metade é indistinguível de um book saudável
- Publique o instrumento como degradado enquanto ele se recupera, como um estado explícito no stream. Um book com um buraco, servido como atual, é pior do que book nenhum
- Verifique depois da junção: o checksum que o feed publica, se ele publicar algum, ou a concordância entre o seu book consolidado e o snapshot seguinte
A recuperação é um evento normal, não um incidente, e o custo dela pertence ao plano de capacidade: quanto tempo leva para buscar um snapshot, quanto de stream fica no buffer enquanto isso e quantos instrumentos podem se recuperar ao mesmo tempo antes de o serviço de snapshot virar o gargalo.
Fan-out: normalizar uma vez, codificar uma vez, enviar a muitos
Centenas de sessões de terminal querem o mesmo book. O erro que se multiplica sob uma rajada é fazer por sessão um trabalho que não é por sessão: reconstruir um book para cada assinante ou serializar a mesma atualização uma vez por socket.
- Um único escritor por shard de instrumentos é dono do book. Os leitores nunca o alteram, o que elimina tanto o lock quanto a dúvida sobre qual versão é a autoritativa
- O escritor publica atualizações versionadas em um ring buffer que os leitores acompanham no próprio ritmo, então um leitor que fica para trás não atrasa ninguém
- Cada atualização é codificada uma vez por formato de wire e compartilhada entre as sessões por referência. Só o enquadramento e o controle de fluxo são por sessão
- Cada sessão carrega o próprio número de sequência de saída, então o cliente detecta as próprias perdas sem saber nada sobre o feed upstream
- A ordenação é garantida por instrumento, porque é dessa garantia que os clientes dependem. A ordenação entre instrumentos ou é prometida explicitamente e implementada, ou não é prometida
- Além de um único processo, o fan-out vira uma camada de relays: cada relay faz uma assinatura upstream e atende uma parcela das sessões, então o trabalho do escritor permanece constante
O custo do fan-out é decidido por quantas vezes uma atualização é transformada, não por quantos sockets a recebem. Codificar uma vez e passar uma referência escala com o número de sessões; reconstruir um book por sessão não escala.
Consumidor lento é política que você escolhe, não acidente
Em algum lugar existe uma sessão em uma rede ruim, ou um terminal cujo laço de renderização travou, e o buffer de saída dela enche. Há quatro comportamentos possíveis, e dois deles só são escolhidos por acidente.
- Bloquear o escritor até a sessão lenta esvaziar: nunca. Isso transforma uma conexão ruim em um evento de latência para todo mundo no shard
- Crescer a fila sem limite: um consumidor lento vira esgotamento de memória e, depois, uma queda sem relação com a sessão original
- Fila limitada com conflation: correto para o estado do book, em que o cliente quer a foto atual e não cada passo intermediário
- Fila limitada com desconexão em um high watermark: correto para streams que não admitem conflation, em que descartar um item descarta significado
- Seja qual for a política, a fila é por sessão, e o atraso é medido continuamente - a profundidade da fila e a distância entre a sequência publicada e a sequência escrita no socket
- Uma desconexão declara o motivo. Um fechamento sem explicação vira retry em laço; um fechamento explicado é seguido de uma nova assinatura
O backpressure é onde os dois lados se encontram. Se o caminho de saída consegue empurrar de volta o escritor do book, um terminal lento acaba atrasando a consolidação do feed, e a detecção de gaps passa a disparar por motivos que nada têm a ver com a venue. Um ring limitado entre os dois interrompe essa corrente.
Conflation é honesta para estado e errada para eventos
O book é estado: o cliente quer os níveis atuais, e um valor já substituído não carrega significado próprio. Um tape de negócios é um log de eventos, em que cada item é um fato que aconteceu e não pode ser resumido até desaparecer.
- Com conflation: atualizações de nível de preço, topo do book, profundidade agregada e estatísticas derivadas, como último preço ou volume da sessão
- Sem conflation: negócios e prints, relatórios de ordem e de execução, leilões e mudanças de fase, e tudo o que o cliente agrega ao longo do tempo - um tape montado a partir de um stream com conflation é um número errado sustentado com confiança
- Aplique conflation por chave, não por stream. Guardar a última atualização de cada nível de preço preserva o book; guardar só a última atualização geral joga fora todo nível que não foi o último a mudar
- Uma atualização com conflation carrega o número de sequência do estado que ela representa, então o cliente sabe a que ponto ela corresponde
- O intervalo de conflation faz parte da latência que você reporta. Um stream com conflation em um intervalo não é descrito pela latência medida no stream sem conflation
- Um cliente que precisa de cada estado intermediário - um backtest, um registro de compliance - assina o stream sem conflation e paga em banda
Conflation é uma mudança de forma, não um ajuste de compressão. Depois que um stream passa por conflation, o cliente não consegue reconstruir o que aconteceu entre duas atualizações, e não se pode dizer a ele que o stream está completo. Publicar os dois - um stream de book com conflation e um stream de eventos sem conflation - é o que mantém corretos os dois tipos de cliente.
Reconexão é ressincronização, e todas chegam de uma vez
Quando uma sessão volta, o book que ela guarda não vale nada a menos que o servidor consiga provar continuidade. O padrão é um snapshot novo por assinatura, com o número de sequência dele, aplicado a um cliente que descartou o estado local antes.
- A retomada a partir de um número de sequência só é oferecida onde existe um buffer de replay limitado. Quando o número pedido já expirou, o servidor diz isso e recorre a um snapshot, em vez de enviar um stream com um buraco
- O estado da sessão através de uma reconexão é uma decisão explícita: ou o servidor mantém as assinaturas por um tempo limitado sob um token de sessão, ou o cliente as declara de novo ao conectar. Os dois funcionam; uma mistura implícita não
- Entrega duplicada depois de um resume é esperada, e o cliente descarta pela sequência. At-least-once mais numeração de sequência é mais fácil de implementar corretamente do que exactly-once
- As reconexões chegam juntas, porque o que desconectou uma sessão costuma ter desconectado muitas. Backoff com jitter no cliente e controle de admissão no servidor impedem que a recuperação vire a segunda queda
- Os snapshots para essa multidão vêm de um cache por instrumento atualizado em cadência, então o escritor serializa um snapshot em uma agenda fixa, e não uma vez por sessão que reconecta
- O book do lado do cliente é reconstruído, nunca remendado. Um terminal que guarda os níveis antigos e aplica deltas novos por cima carrega o erro anterior à desconexão para um book que agora parece novo
A falha que vale projetar não é uma reconexão isolada. É um evento de rede que devolve centenas de sessões no mesmo segundo, cada uma pedindo um snapshot de todos os instrumentos que acompanhava, enquanto o lado da ingestão se recupera do gap que esse mesmo evento produziu.
O que a amBrain pode sustentar publicamente: construímos plataformas de trading de baixa latência, matching engines e sistemas de real-time bidding em Rust a partir de Yerevan, Armênia, e a latência de dados de mercado que publicamos - menos de 5 ms - é medida nos caminhos que construímos. Se o seu handler está perdendo sequência sob rajadas, a conversa que vale ter é a que separa o lado da ingestão do lado da distribuição antes de reescrever qualquer um dos dois.