Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

Esta é a continuação de uma longa história sobre nosso caminho espinhoso para criar um sistema poderoso e de alta carga que garanta a operação da Bolsa. A primeira parte está aqui: habr.com/en/post/444300

Erro misterioso

Após vários testes, o sistema atualizado de negociação e compensação foi colocado em operação e encontramos um bug sobre o qual poderíamos escrever uma história mística de detetive.

Logo após o lançamento no servidor principal, uma das transações foi processada com erro. No entanto, tudo estava bem no servidor de backup. Descobriu-se que uma simples operação matemática de cálculo do expoente no servidor principal deu um resultado negativo do argumento real! Continuamos nossa pesquisa e no registro SSE2 encontramos uma diferença de um bit, que é responsável pelo arredondamento ao trabalhar com números de ponto flutuante.

Escrevemos um utilitário de teste simples para calcular o expoente com o bit de arredondamento definido. Acontece que na versão do RedHat Linux que usamos, houve um bug no trabalho com a função matemática quando o malfadado bit foi inserido. Relatamos isso ao RedHat, depois de um tempo recebemos um patch deles e o lançamos. O erro não ocorreu mais, mas não estava claro de onde veio esse bit. A função foi responsável por isso fesetround da linguagem C. Analisamos cuidadosamente nosso código em busca do suposto erro: verificamos todas as situações possíveis; examinou todas as funções que usavam arredondamento; tentou reproduzir uma sessão com falha; usou diferentes compiladores com diferentes opções; Foram utilizadas análises estática e dinâmica.

A causa do erro não foi encontrada.

Depois começaram a verificar o hardware: fizeram testes de carga dos processadores; verifiquei a RAM; Até executamos testes para o cenário muito improvável de um erro de vários bits em uma célula. Para nenhum proveito.

No final, decidimos por uma teoria do mundo da física de alta energia: alguma partícula de alta energia voou para dentro do nosso data center, perfurou a parede do gabinete, atingiu o processador e fez com que a trava do gatilho ficasse presa naquela mesma parte. Essa teoria absurda foi chamada de “neutrino”. Se você está longe da física de partículas: os neutrinos quase não interagem com o mundo exterior e certamente não são capazes de afetar o funcionamento do processador.

Como não foi possível encontrar a causa da falha, o servidor “infrator” foi retirado de operação por precaução.

Depois de algum tempo, começamos a melhorar o sistema de backup a quente: introduzimos as chamadas “reservas quentes” (quentes) - réplicas assíncronas. Eles receberam um fluxo de transações que poderiam estar localizadas em diferentes data centers, mas os Warms não interagiram ativamente com outros servidores.

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

Por que isso foi feito? Se o servidor de backup falhar, o hot vinculado ao servidor principal se tornará o novo backup. Ou seja, após uma falha, o sistema não permanece com um servidor principal até o final do pregão.

E quando a nova versão do sistema foi testada e colocada em operação, o erro de arredondamento ocorreu novamente. Além disso, com o aumento do número de servidores quentes, o erro começou a aparecer com mais frequência. Ao mesmo tempo, o vendedor não tinha nada a mostrar, uma vez que não havia provas concretas.

Durante a próxima análise da situação, surgiu uma teoria de que o problema poderia estar relacionado ao sistema operacional. Escrevemos um programa simples que chama uma função em um loop infinito fesetround, lembra o estado atual e o verifica durante a suspensão, e isso é feito em muitos threads concorrentes. Depois de selecionar os parâmetros de suspensão e o número de threads, começamos a reproduzir consistentemente a falha do bit após cerca de 5 minutos de execução do utilitário. No entanto, o suporte da Red Hat não conseguiu reproduzi-lo. Os testes de nossos outros servidores mostraram que apenas aqueles com determinados processadores são suscetíveis ao erro. Ao mesmo tempo, mudar para um novo kernel resolveu o problema. No final, simplesmente substituímos o sistema operacional e a verdadeira causa do bug permaneceu obscura.

E de repente, no ano passado, um artigo foi publicado no Habré “Como encontrei um bug nos processadores Intel Skylake" A situação nele descrita era muito semelhante à nossa, mas o autor levou a investigação mais longe e apresentou a teoria de que o erro estava no microcódigo. E quando os kernels do Linux são atualizados, os fabricantes também atualizam o microcódigo.

Desenvolvimento adicional do sistema

Embora tenhamos nos livrado do erro, essa história nos obrigou a reconsiderar a arquitetura do sistema. Afinal, não estávamos protegidos da repetição de tais bugs.

Os seguintes princípios formaram a base para as próximas melhorias no sistema de reservas:

  • Você não pode confiar em ninguém. Os servidores podem não funcionar corretamente.
  • Reserva majoritária.
  • Garantindo o consenso. Como um acréscimo lógico à reserva da maioria.
  • Falhas duplas são possíveis.
  • Vitalidade. O novo esquema de hot standby não deve ser pior que o anterior. A negociação deve prosseguir ininterruptamente até o último servidor.
  • Ligeiro aumento na latência. Qualquer tempo de inatividade acarreta enormes perdas financeiras.
  • Interação mínima de rede para manter a latência o mais baixa possível.
  • Selecionando um novo servidor mestre em segundos.

Nenhuma das soluções disponíveis no mercado nos convinha e o protocolo Raft ainda estava nos seus primórdios, por isso criámos a nossa própria solução.

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

Rede

Além do sistema de reservas, iniciamos a modernização da interação na rede. O subsistema de E/S consistia em muitos processos, que tiveram o pior impacto no jitter e na latência. Com centenas de processos lidando com conexões TCP, fomos forçados a alternar constantemente entre eles e, em uma escala de microssegundos, essa é uma operação bastante demorada. Mas a pior parte é que quando um processo recebe um pacote para processamento, ele o envia para uma fila SystemV e depois espera por um evento de outra fila SystemV. Porém, quando há um grande número de nós, a chegada de um novo pacote TCP em um processo e o recebimento de dados na fila em outro representam dois eventos concorrentes para o SO. Neste caso, se não houver processadores físicos disponíveis para ambas as tarefas, um será processado e o segundo será colocado em fila de espera. É impossível prever as consequências.

Em tais situações, o controle dinâmico de prioridade do processo pode ser usado, mas isso exigirá o uso de chamadas de sistema que consomem muitos recursos. Como resultado, mudamos para um thread usando o epoll clássico, o que aumentou bastante a velocidade e reduziu o tempo de processamento da transação. Também nos livramos dos processos separados de comunicação de rede e da comunicação através do SystemV, reduzimos significativamente o número de chamadas do sistema e começamos a controlar as prioridades das operações. Somente no subsistema de E/S, foi possível economizar cerca de 8 a 17 microssegundos, dependendo do cenário. Este esquema de thread único tem sido usado inalterado desde então; um thread epoll com margem é suficiente para atender todas as conexões.

Processamento de transações

A carga crescente em nosso sistema exigiu a atualização de quase todos os seus componentes. Mas, infelizmente, a estagnação no crescimento das velocidades de clock dos processadores nos últimos anos não tornou mais possível escalar processos de frente. Por isso, decidimos dividir o processo do Engine em três níveis, sendo o mais movimentado deles o sistema de verificação de risco, que avalia a disponibilidade de recursos nas contas e cria as próprias transações. Mas o dinheiro pode estar em moedas diferentes, e foi necessário descobrir em que base deveria ser dividido o processamento dos pedidos.

A solução lógica é dividi-lo por moeda: um servidor negocia em dólares, outro em libras e um terceiro em euros. Mas se, com tal esquema, duas transações forem enviadas para comprar moedas diferentes, surgirá o problema de dessincronização da carteira. Mas a sincronização é difícil e cara. Portanto, seria correto fragmentar separadamente por carteiras e separadamente por instrumentos. A propósito, a maioria das bolsas ocidentais não tem a tarefa de verificar os riscos de forma tão precisa quanto nós, por isso, na maioria das vezes, isso é feito offline. Precisávamos implementar a verificação online.

Vamos explicar com um exemplo. Um trader quer comprar $30, e a solicitação vai para a validação da transação: verificamos se esse trader tem permissão para esta modalidade de negociação e se ele possui os direitos necessários. Se tudo estiver em ordem, a solicitação vai para o sistema de verificação de risco, ou seja. para verificar a suficiência de fundos para concluir uma transação. Há uma observação de que o valor necessário está atualmente bloqueado. A solicitação é então encaminhada ao sistema de negociação, que aprova ou desaprova a transação. Digamos que a transação foi aprovada - então o sistema de verificação de risco marca que o dinheiro está desbloqueado e os rublos se transformam em dólares.

Em geral, o sistema de verificação de risco contém algoritmos complexos e realiza uma grande quantidade de cálculos que consomem muitos recursos, e não verifica simplesmente o “saldo da conta”, como pode parecer à primeira vista.

Quando começamos a dividir o processo do Engine em níveis, encontramos um problema: o código que estava disponível naquele momento usava ativamente a mesma matriz de dados nos estágios de validação e verificação, o que exigia reescrever toda a base de código. Como resultado, pegamos emprestada uma técnica de processamento de instruções dos processadores modernos: cada um deles é dividido em pequenos estágios e várias ações são executadas em paralelo em um ciclo.

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

Após uma pequena adaptação do código, criamos um pipeline para processamento paralelo de transações, no qual a transação foi dividida em 4 etapas do pipeline: interação na rede, validação, execução e publicação do resultado

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

Vejamos um exemplo. Temos dois sistemas de processamento, serial e paralelo. A primeira transação chega e é enviada para validação nos dois sistemas. A segunda transação chega imediatamente: em um sistema paralelo ela é imediatamente colocada em funcionamento, e em um sistema sequencial é colocada em uma fila aguardando que a primeira transação passe pela etapa atual de processamento. Ou seja, a principal vantagem do processamento de pipeline é que processamos a fila de transações com mais rapidez.

Foi assim que criamos o sistema ASTS+.

É verdade que nem tudo é tão tranquilo com os transportadores. Digamos que temos uma transação que afeta matrizes de dados em uma transação vizinha; esta é uma situação típica de uma exchange. Tal transação não pode ser executada em um pipeline porque pode afetar outras pessoas. Essa situação é chamada de perigo de dados, e tais transações são simplesmente processadas separadamente: quando as transações “rápidas” na fila acabam, o pipeline para, o sistema processa a transação “lenta” e então inicia o pipeline novamente. Felizmente, a proporção dessas transações no fluxo geral é muito pequena, de modo que o pipeline para tão raramente que não afeta o desempenho geral.

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

Então começamos a resolver o problema de sincronização de três threads de execução. O resultado foi um sistema baseado em um buffer circular com células de tamanho fixo. Neste sistema tudo está sujeito à velocidade de processamento, os dados não são copiados.

  • Todos os pacotes de rede recebidos entram no estágio de alocação.
  • Nós os colocamos em uma matriz e os marcamos como disponíveis para o estágio 1.
  • A segunda transação chegou, está novamente disponível para a etapa nº 1.
  • O primeiro thread de processamento vê as transações disponíveis, processa-as e as move para o próximo estágio do segundo thread de processamento.
  • Em seguida, ele processa a primeira transação e sinaliza a célula correspondente deleted - agora está disponível para novo uso.

A fila inteira é processada dessa maneira.

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

O processamento de cada estágio leva unidades ou dezenas de microssegundos. E se usarmos esquemas padrão de sincronização do sistema operacional, perderemos mais tempo na própria sincronização. É por isso que começamos a usar o spinlock. No entanto, isso é muito ruim em um sistema em tempo real, e a RedHat estritamente não recomenda fazer isso, então aplicamos um spinlock por 100 ms e depois mudamos para o modo semáforo para eliminar a possibilidade de um impasse.

Como resultado, alcançamos um desempenho de cerca de 8 milhões de transações por segundo. E literalmente dois meses depois em статье sobre o LMAX Disruptor vimos a descrição de um circuito com a mesma funcionalidade.

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

Agora pode haver vários threads de execução em um estágio. Todas as transações foram processadas uma a uma, na ordem em que foram recebidas. Como resultado, o desempenho máximo aumentou de 18 mil para 50 mil transações por segundo.

Sistema de gerenciamento de risco cambial

Não há limite para a perfeição e logo recomeçamos a modernização: no âmbito do ASTS+, começamos a transformar os sistemas de gestão de risco e operações de liquidação em componentes autônomos. Desenvolvemos uma arquitetura moderna e flexível e um novo modelo hierárquico de risco, e tentamos usar a classe sempre que possível fixed_point ao invés de double.

Mas imediatamente surgiu um problema: como sincronizar toda a lógica de negócio que funciona há muitos anos e transferi-la para o novo sistema? Com isso, a primeira versão do protótipo do novo sistema teve que ser abandonada. A segunda versão, que atualmente está em produção, é baseada no mesmo código, que funciona tanto na parte de negociação quanto na de risco. Durante o desenvolvimento, a coisa mais difícil de fazer foi mesclar o git entre duas versões. Nosso colega Evgeniy Mazurenok realizava essa operação todas as semanas e cada vez xingava por muito tempo.

Ao selecionar um novo sistema, tivemos que resolver imediatamente o problema de interação. Ao escolher um barramento de dados, foi necessário garantir jitter estável e latência mínima. A rede InfiniBand RDMA foi a mais adequada para isso: o tempo médio de processamento é 4 vezes menor do que nas redes Ethernet 10 G. Mas o que realmente nos cativou foi a diferença nos percentis – 99 e 99,9.

Claro, o InfiniBand tem seus desafios. Em primeiro lugar, uma API diferente - ibverbs em vez de sockets. Em segundo lugar, quase não existem soluções de mensagens de código aberto amplamente disponíveis. Tentamos fazer nosso próprio protótipo, mas acabou sendo muito difícil, então escolhemos uma solução comercial - Confinity Low Latency Messaging (anteriormente IBM MQ LLM).

Surgiu então a tarefa de dividir adequadamente o sistema de risco. Se você simplesmente remover o Risk Engine e não criar um nó intermediário, as transações de duas fontes poderão ser misturadas.

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

As chamadas soluções Ultra Low Latency possuem um modo de reordenamento: as transações de duas fontes podem ser organizadas na ordem necessária no momento do recebimento; isso é implementado por meio de um canal separado para troca de informações sobre o pedido. Mas ainda não utilizamos este modo: complica todo o processo e em várias soluções não é suportado de todo. Além disso, cada transação teria que receber carimbos de data/hora correspondentes e, em nosso esquema, esse mecanismo é muito difícil de implementar corretamente. Portanto, utilizamos o esquema clássico com message broker, ou seja, com um despachante que distribui mensagens entre o Risk Engine.

O segundo problema estava relacionado ao acesso do cliente: se houver vários Risk Gateways, o cliente precisará se conectar a cada um deles, e isso exigirá alterações na camada do cliente. Queríamos fugir disso neste estágio, para que o design atual do Risk Gateway processe todo o fluxo de dados. Isso limita bastante o rendimento máximo, mas simplifica bastante a integração do sistema.

Duplicação

Nosso sistema não deve ter um único ponto de falha, ou seja, todos os componentes devem ser duplicados, inclusive o message broker. Resolvemos esse problema usando o sistema CLLM: ele contém um cluster RCMS no qual dois despachantes podem trabalhar em modo mestre-escravo e, quando um falha, o sistema muda automaticamente para o outro.

Trabalhando com um data center de backup

O InfiniBand é otimizado para operação como uma rede local, ou seja, para conectar equipamentos montados em rack, e uma rede InfiniBand não pode ser instalada entre dois data centers distribuídos geograficamente. Portanto, implementamos uma ponte/despachante, que se conecta ao armazenamento de mensagens através de redes Ethernet regulares e retransmite todas as transações para uma segunda rede IB. Quando precisamos migrar de um data center, podemos escolher com qual data center trabalharemos agora.

Resultados de

Todos os itens acima não foram feitos de uma só vez; foram necessárias várias iterações para desenvolver uma nova arquitetura. Criamos o protótipo em um mês, mas demorou mais de dois anos para colocá-lo em condições de funcionamento. Tentamos alcançar o melhor compromisso entre aumentar o tempo de processamento de transações e aumentar a confiabilidade do sistema.

Como o sistema foi bastante atualizado, implementamos a recuperação de dados de duas fontes independentes. Se o armazenamento de mensagens não estiver funcionando corretamente por algum motivo, você poderá obter o log de transações de uma segunda fonte - do Risk Engine. Este princípio é observado em todo o sistema.

Entre outras coisas, conseguimos preservar a API do cliente para que nem os corretores nem qualquer outra pessoa precisassem de retrabalho significativo para a nova arquitetura. Tivemos que alterar algumas interfaces, mas não houve necessidade de fazer alterações significativas no modelo operacional.

Chamamos a versão atual de nossa plataforma de Rebus – como uma abreviatura para as duas inovações mais notáveis ​​na arquitetura, Risk Engine e BUS.

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

Inicialmente queríamos alocar apenas a parte de compensação, mas o resultado foi um enorme sistema distribuído. Os clientes agora podem interagir com o Trade Gateway, o Clearing Gateway ou ambos.

O que finalmente alcançamos:

Evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou. Parte 2

Reduziu o nível de latência. Com um pequeno volume de transações, o sistema funciona da mesma forma que a versão anterior, mas ao mesmo tempo suporta uma carga muito maior.

O desempenho máximo aumentou de 50 mil para 180 mil transações por segundo. Um aumento adicional é dificultado pelo único fluxo de correspondência de pedidos.

Existem duas maneiras de melhorar ainda mais: paralelizar a correspondência e mudar a forma como funciona com o Gateway. Agora todos os Gateways operam de acordo com um esquema de replicação, que, sob tal carga, deixa de funcionar normalmente.

Por fim, posso dar alguns conselhos para quem está finalizando sistemas empresariais:

  • Esteja preparado para o pior em todos os momentos. Os problemas sempre surgem inesperadamente.
  • Geralmente é impossível refazer rapidamente a arquitetura. Especialmente se você precisar alcançar o máximo de confiabilidade em vários indicadores. Quanto mais nós, mais recursos serão necessários para suporte.
  • Todas as soluções personalizadas e proprietárias exigirão recursos adicionais para pesquisa, suporte e manutenção.
  • Não adie a resolução de problemas de confiabilidade e recuperação do sistema após falhas; leve-os em consideração na fase inicial do projeto.

Fonte: habr.com

Adicionar um comentário