A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

Esta é a continuación dunha longa historia sobre o noso espiñento camiño para crear un sistema potente e de alta carga que garanta o funcionamento do Exchange. A primeira parte está aquí: habr.com/en/post/444300

Erro misterioso

Despois de numerosas probas, púxose en funcionamento o sistema de negociación e compensación actualizado e atopamos un erro sobre o que poderiamos escribir unha historia detective-mística.

Pouco despois do lanzamento no servidor principal, unha das transaccións foi procesada cun erro. Non obstante, todo estaba ben no servidor de copia de seguridade. Resultou que unha simple operación matemática de calcular o expoñente no servidor principal deu un resultado negativo do argumento real. Continuamos coa nosa investigación, e no rexistro SSE2 atopamos unha diferenza nun bit, que se encarga de redondear cando se traballa con números de coma flotante.

Escribimos unha utilidade de proba sinxela para calcular o expoñente co conxunto de bits de redondeo. Resultou que na versión de RedHat Linux que usamos, houbo un erro ao traballar coa función matemática cando se inseriu o bit malogrado. Informamos diso a RedHat, despois dun tempo recibimos un parche deles e lanzámolo. O erro xa non se produciu, pero non estaba claro de onde procedía este bit? A función encargouse diso fesetround da linguaxe C. Analizamos coidadosamente o noso código na procura do suposto erro: comprobamos todas as situacións posibles; mirou todas as funcións que usaban o redondeo; intentou reproducir unha sesión fallida; utilizou diferentes compiladores con diferentes opcións; Utilizáronse análises estáticas e dinámicas.

Non se puido atopar a causa do erro.

Despois comezaron a comprobar o hardware: realizaron probas de carga dos procesadores; comprobou a memoria RAM; Incluso realizamos probas para o escenario moi improbable dun erro de varios bits nunha cela. De nada serviu.

Ao final, asentámonos nunha teoría do mundo da física de alta enerxía: unha partícula de alta enerxía voou ao noso centro de datos, atravesou a parede da caixa, golpeou o procesador e fixo que o pestillo do gatillo se pegara nese mesmo anaco. Esta teoría absurda chamábase o "neutrino". Se estás lonxe da física de partículas: os neutrinos case non interactúan co mundo exterior e, por suposto, non son capaces de afectar o funcionamento do procesador.

Dado que non foi posible atopar a causa do fallo, o servidor "ofensivo" foi eliminado por se acaso.

Despois dun tempo, comezamos a mellorar o sistema de copia de seguridade en quente: introducimos as chamadas "reservas quentes" (cálidas): réplicas asíncronas. Recibiron un fluxo de transaccións que se podían localizar en diferentes centros de datos, pero os quentes non interactuaron activamente con outros servidores.

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

Por que se fixo isto? Se o servidor de copia de seguranza falla, a nova copia de seguranza será unida ao servidor principal. É dicir, despois dun fallo, o sistema non permanece cun servidor principal ata o final da sesión de negociación.

E cando se probou e puxo en funcionamento a nova versión do sistema, volveuse a producir o erro do bit de redondeo. Ademais, co aumento do número de servidores quentes, o erro comezou a aparecer con máis frecuencia. Ao mesmo tempo, o vendedor non tiña nada que mostrar, xa que non había probas concretas.

Durante a seguinte análise da situación, xurdiu unha teoría de que o problema podería estar relacionado co SO. Escribimos un programa sinxelo que chama a unha función nun bucle infinito fesetround, lembra o estado actual e compróbao a través do modo de suspensión, e isto faise en moitos fíos competidores. Despois de seleccionar os parámetros de suspensión e o número de fíos, comezamos a reproducir de forma consistente o fallo do bit despois duns 5 minutos de execución da utilidade. Non obstante, o soporte de Red Hat non puido reproducilo. As probas dos nosos outros servidores demostraron que só aqueles con certos procesadores son susceptibles ao erro. Ao mesmo tempo, cambiar a un novo núcleo resolveu o problema. Ao final, simplemente substituímos o sistema operativo e a verdadeira causa do erro non estaba clara.

E de súpeto o ano pasado publicouse un artigo sobre Habré “Como atopei un erro nos procesadores Intel Skylake" A situación descrita nel era moi semellante á nosa, pero o autor levou a investigación máis alá e expuxo unha teoría de que o erro estaba no microcódigo. E cando se actualizan os núcleos de Linux, os fabricantes tamén actualizan o microcódigo.

Desenvolvemento posterior do sistema

Aínda que nos libramos do erro, esta historia obrigounos a reconsiderar a arquitectura do sistema. Despois de todo, non estabamos protexidos da repetición de tales erros.

Os seguintes principios constituíron a base para as próximas melloras do sistema de reservas:

  • Non podes confiar en ninguén. É posible que os servidores non funcionen correctamente.
  • Reserva maioritaria.
  • Garantir o consenso. Como complemento lóxico á reserva maioritaria.
  • Os dobres fallos son posibles.
  • Vitalidade. O novo esquema de espera en quente non debería ser peor que o anterior. O comercio debe continuar de forma ininterrompida ata o último servidor.
  • Lixeiro aumento da latencia. Calquera tempo de inactividade implica enormes perdas financeiras.
  • Interacción de rede mínima para manter a latencia o máis baixa posible.
  • Seleccionando un novo servidor mestre en segundos.

Ningunha das solucións dispoñibles no mercado nos conviña, e o protocolo Raft aínda estaba na súa etapa inicial, polo que creamos a nosa propia solución.

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

Rede

Ademais do sistema de reservas, comezamos a modernizar a interacción da rede. O subsistema de E/S constaba de moitos procesos, que tiñan o peor impacto sobre a jitter e a latencia. Con centos de procesos que manexan conexións TCP, vímonos obrigados a cambiar constantemente entre eles, e a escala de microsegundos esta é unha operación que leva bastante tempo. Pero o peor é que cando un proceso recibía un paquete para procesala, enviábao a unha cola SystemV e despois esperaba un evento doutra cola SystemV. Non obstante, cando hai un gran número de nodos, a chegada dun novo paquete TCP nun proceso e a recepción de datos na cola noutro representan dous eventos en competencia para o SO. Neste caso, se non hai procesadores físicos dispoñibles para ambas tarefas, procesarase un e o segundo colocarase nunha cola de espera. É imposible prever as consecuencias.

En tales situacións, pódese utilizar o control dinámico de prioridades do proceso, pero isto requirirá o uso de chamadas ao sistema con uso intensivo de recursos. Como resultado, cambiamos a un fío usando epoll clásico, isto aumentou moito a velocidade e reduciu o tempo de procesamento da transacción. Tamén nos libramos dos procesos de comunicación de rede separados e da comunicación a través de SystemV, reducimos significativamente o número de chamadas ao sistema e comezamos a controlar as prioridades das operacións. Só no subsistema de E/S, foi posible aforrar entre 8 e 17 microsegundos, dependendo do escenario. Este esquema de fío único usouse sen cambios desde entón; un fío epoll cunha marxe é suficiente para atender todas as conexións.

Procesamento de transaccións

A crecente carga do noso sistema requiriu actualizar case todos os seus compoñentes. Pero, por desgraza, o estancamento no crecemento das velocidades de reloxo dos procesadores nos últimos anos xa non permitiu escalar os procesos de forma directa. Por iso, decidimos dividir o proceso do motor en tres niveis, sendo o máis ocupado deles o sistema de verificación de riscos, que avalía a dispoñibilidade de fondos nas contas e crea as propias transaccións. Pero o diñeiro pode estar en diferentes moedas, e foi necesario descubrir en que base debe dividirse o procesamento das solicitudes.

A solución lóxica é dividilo por moeda: un servidor cotiza en dólares, outro en libras e un terceiro en euros. Pero se, con tal esquema, envíanse dúas transaccións para comprar moedas diferentes, entón xurdirá o problema da desincronización da carteira. Pero a sincronización é difícil e cara. Polo tanto, sería correcto dividir por separado por carteiras e por instrumentos por separado. Por certo, a maioría das bolsas occidentais non teñen a tarefa de comprobar os riscos tan agudamente como nós, polo que a maioría das veces faise fóra de liña. Necesitabamos implementar a verificación en liña.

Imos explicar cun exemplo. Un comerciante quere comprar $ 30 e a solicitude vai á validación da transacción: comprobamos se este comerciante ten permiso para este modo de negociación e se ten os dereitos necesarios. Se todo está en orde, a solicitude diríxese ao sistema de verificación de riscos, é dicir. para comprobar a suficiencia dos fondos para concluír unha transacción. Hai unha nota de que a cantidade necesaria está actualmente bloqueada. A solicitude remítese entón ao sistema de negociación, que aproba ou desaproba a transacción. Digamos que a transacción está aprobada; entón o sistema de verificación de risco marca que o diñeiro está desbloqueado e os rublos convértense en dólares.

En xeral, o sistema de comprobación de riscos contén algoritmos complexos e realiza unha gran cantidade de cálculos moi intensivos en recursos, e non simplemente verifica o "saldo da conta", como podería parecer a primeira vista.

Cando comezamos a dividir o proceso do motor en niveis, atopamos un problema: o código que estaba dispoñible nese momento utilizaba activamente a mesma matriz de datos nas etapas de validación e verificación, o que requiría reescribir toda a base de código. Como resultado, tomamos prestada unha técnica para procesar instrucións dos procesadores modernos: cada unha delas divídese en pequenas etapas e realízanse varias accións en paralelo nun ciclo.

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

Despois dunha pequena adaptación do código, creamos unha canalización para o procesamento de transaccións paralelas, na que a transacción se dividiu en 4 etapas da canalización: interacción da rede, validación, execución e publicación do resultado.

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

Vexamos un exemplo. Temos dous sistemas de procesamento, en serie e en paralelo. Chega a primeira transacción e envíase a validación en ambos os sistemas. A segunda transacción chega inmediatamente: nun sistema paralelo inmediatamente lévase a traballar, e nun sistema secuencial ponse nunha cola á espera de que a primeira transacción pase pola fase de procesamento actual. É dicir, a principal vantaxe do procesamento de pipeline é que procesamos a cola de transaccións máis rápido.

Así foi como creamos o sistema ASTS+.

É certo, tampouco todo é tan suave con transportadores. Digamos que temos unha transacción que afecta a matrices de datos nunha transacción veciña; esta é unha situación típica para un intercambio. Tal transacción non se pode executar nunha canalización porque pode afectar a outros. Esta situación chámase perigo de datos, e tales transaccións simplemente procesan por separado: cando se esgotan as transaccións "rápidas" na cola, a canalización detense, o sistema procesa a transacción "lenta" e despois inicia a canalización de novo. Afortunadamente, a proporción de tales transaccións no fluxo global é moi pequena, polo que o gasoduto se detén tan raramente que non afecta o rendemento xeral.

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

Entón comezamos a resolver o problema de sincronizar tres fíos de execución. O resultado foi un sistema baseado nun buffer anular con celas de tamaño fixo. Neste sistema, todo está suxeito á velocidade de procesamento; os datos non se copian.

  • Todos os paquetes de rede entrantes entran na fase de asignación.
  • Colocámolos nunha matriz e marcámolos como dispoñibles para a fase #1.
  • Chegou a segunda transacción, volve estar dispoñible para a etapa no 1.
  • O primeiro fío de procesamento ve as transaccións dispoñibles, procesaas e móvenas á seguinte fase do segundo fío de procesamento.
  • Despois procesa a primeira transacción e marca a cela correspondente deleted - agora está dispoñible para novos usos.

Toda a cola procédese deste xeito.

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

O procesamento de cada etapa leva unidades ou decenas de microsegundos. E se usamos esquemas de sincronización do SO estándar, perderemos máis tempo na propia sincronización. Por iso comezamos a usar spinlock. Non obstante, isto é moi malo nun sistema en tempo real, e RedHat non recomenda estrictamente facelo, polo que aplicamos un spinlock durante 100 ms e despois cambiamos ao modo semáforo para eliminar a posibilidade de un punto morto.

Como resultado, conseguimos un rendemento duns 8 millóns de transaccións por segundo. E literalmente dous meses despois Artigo sobre LMAX Disruptor vimos unha descrición dun circuíto coa mesma funcionalidade.

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

Agora podería haber varios fíos de execución nunha etapa. Todas as transaccións foron procesadas unha por unha, na orde en que foron recibidas. Como resultado, o rendemento máximo aumentou de 18 mil a 50 mil transaccións por segundo.

Sistema de xestión do risco de cambio

Non hai límite para a perfección, e pronto comezamos de novo a modernización: no marco de ASTS+, comezamos a trasladar os sistemas de xestión e liquidación de riscos a compoñentes autónomos. Desenvolvemos unha arquitectura moderna flexible e un novo modelo de risco xerárquico, e tentamos utilizar a clase sempre que foi posible fixed_point en vez de double.

Pero inmediatamente xurdiu un problema: como sincronizar toda a lóxica empresarial que leva moitos anos funcionando e transferila ao novo sistema? Como resultado, a primeira versión do prototipo do novo sistema tivo que ser abandonada. A segunda versión, que está a traballar actualmente en produción, baséase no mesmo código, que funciona tanto nas partes de negociación como de risco. Durante o desenvolvemento, o máis difícil foi combinar git entre dúas versións. O noso compañeiro Evgeniy Mazurenok realizou esta operación todas as semanas e cada vez maldixo durante moito tempo.

Ao seleccionar un novo sistema, inmediatamente tivemos que resolver o problema da interacción. Ao elixir un bus de datos, era necesario garantir unha fluctuación estable e unha latencia mínima. A rede InfiniBand RDMA era a máis adecuada para iso: o tempo medio de procesamento é 4 veces menor que nas redes Ethernet 10 G. Pero o que realmente nos cativou foi a diferenza de percentiles: 99 e 99,9.

Por suposto, InfiniBand ten os seus retos. En primeiro lugar, unha API diferente: ibverbs en lugar de sockets. En segundo lugar, case non hai solucións de mensaxería de código aberto amplamente dispoñibles. Tentamos facer o noso propio prototipo, pero resultou moi difícil, polo que escollemos unha solución comercial: Confinity Low Latency Messaging (antes IBM MQ LLM).

Entón xurdiu a tarefa de dividir correctamente o sistema de riscos. Se simplemente eliminas o motor de risco e non creas un nodo intermedio, pódense mesturar transaccións de dúas fontes.

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

As chamadas solucións de Ultra Low Latency teñen un modo de reordenación: as transaccións de dúas fontes pódense organizar na orde requirida despois da súa recepción; isto realízase mediante unha canle separada para intercambiar información sobre o pedido. Pero aínda non usamos este modo: complica todo o proceso e en varias solucións non se admite en absoluto. Ademais, a cada transacción habería que asignarlle as marcas de tempo correspondentes, e no noso esquema este mecanismo é moi difícil de implementar correctamente. Polo tanto, utilizamos o esquema clásico cun corredor de mensaxes, é dicir, cun despachador que distribúe mensaxes entre o motor de risco.

O segundo problema estaba relacionado co acceso do cliente: se hai varias Pasarelas de Risco, o cliente debe conectarse a cada unha delas, e iso requirirá cambios na capa de cliente. Queriamos fuxir disto neste momento, polo que o deseño actual de Risk Gateway procesa todo o fluxo de datos. Isto limita moito o rendemento máximo, pero simplifica moito a integración do sistema.

Duplicación

O noso sistema non debe ter un único punto de fallo, é dicir, todos os compoñentes deben estar duplicados, incluído o corredor de mensaxes. Resolvemos este problema mediante o sistema CLLM: contén un clúster RCMS no que dous despachadores poden traballar en modo mestre-escravo e, cando un falla, o sistema cambia automaticamente ao outro.

Traballando cun centro de datos de copia de seguridade

InfiniBand está optimizado para funcionar como unha rede local, é dicir, para conectar equipos de montaxe en rack, e unha rede InfiniBand non se pode colocar entre dous centros de datos distribuídos xeograficamente. Polo tanto, implementamos un bridge/dispatcher, que se conecta ao almacenamento de mensaxes a través de redes Ethernet habituais e transmite todas as transaccións a unha segunda rede IB. Cando necesitemos migrar desde un centro de datos, podemos escoller con que centro de datos traballar agora.

Resultados de

Todo o anterior non se fixo á vez; foron necesarias varias iteracións para desenvolver unha nova arquitectura. Creamos o prototipo nun mes, pero tardou máis de dous anos en poñelo en condicións. Tentamos conseguir o mellor compromiso entre aumentar o tempo de procesamento das transaccións e aumentar a fiabilidade do sistema.

Dado que o sistema foi moi actualizado, implementamos a recuperación de datos de dúas fontes independentes. Se o almacén de mensaxes non funciona correctamente por algún motivo, podes sacar o rexistro de transaccións dunha segunda fonte: o motor de risco. Este principio obsérvase en todo o sistema.

Entre outras cousas, puidemos preservar a API do cliente para que nin os corredores nin ninguén esixan unha reelaboración significativa para a nova arquitectura. Tivemos que cambiar algunhas interfaces, pero non houbo que facer cambios significativos no modelo operativo.

Chamamos a versión actual da nosa plataforma Rebus, como abreviatura das dúas innovacións máis notables na arquitectura, Risk Engine e BUS.

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

Inicialmente, queriamos asignar só a parte de limpeza, pero o resultado foi un enorme sistema distribuído. Agora os clientes poden interactuar con Trade Gateway, Clearing Gateway ou con ambos.

O que finalmente conseguimos:

A evolución da arquitectura do sistema de negociación e compensación da Bolsa de Moscova. Parte 2

Reduciuse o nivel de latencia. Cun pequeno volume de transaccións, o sistema funciona igual que a versión anterior, pero ao mesmo tempo pode soportar unha carga moito maior.

O rendemento máximo aumentou de 50 mil a 180 mil transaccións por segundo. Un novo aumento vese obstaculizado polo único fluxo de coincidencia de pedidos.

Hai dúas formas de mellorar aínda máis: paralelizar a correspondencia e cambiar a forma en que funciona con Gateway. Agora todas as pasarelas funcionan segundo un esquema de replicación que, baixo tal carga, deixa de funcionar normalmente.

Por último, podo dar algúns consellos a aqueles que están ultimando sistemas empresariais:

  • Estea preparado para o peor en todo momento. Os problemas sempre xorden de forma inesperada.
  • Normalmente é imposible refacer rapidamente a arquitectura. Especialmente se precisa acadar a máxima fiabilidade en varios indicadores. Cantos máis nodos, máis recursos necesarios para o apoio.
  • Todas as solucións personalizadas e propietarias requirirán recursos adicionais para investigación, soporte e mantemento.
  • Non postergue a resolución de problemas de fiabilidade e recuperación do sistema despois de fallos; téñaos en conta na fase de deseño inicial.

Fonte: www.habr.com

Engadir un comentario