Tuesday 25 July 2017

Ring Buffer Lmax Forex


Estou tentando entender o padrão disruptor. Eu assisti o vídeo InfoQ e tentei ler o seu papel. Eu entendo que há um buffer de anel envolvido, que é inicializado como um array extremamente grande para tirar proveito da localidade de cache, eliminar a alocação de nova memória. Parece que há um ou mais inteiros atômicos que acompanham as posições. Cada evento parece ter uma identificação única e sua posição no ringue é encontrada encontrando seu módulo com relação ao tamanho do anel, etc. etc. Infelizmente, eu não tenho um senso intuitivo de como ele funciona. Eu tenho feito muitas aplicações comerciais e estudado o modelo de ator. Olhou para SEDA, etc Em sua apresentação, eles mencionaram que este padrão é basicamente como roteadores trabalho no entanto eu havent encontrado quaisquer boas descrições de como roteadores trabalho tanto. Existem alguns bons indicadores para uma melhor explicação O projeto do Google Code faz referência a um documento técnico sobre a implementação do buffer de anel, no entanto, é um pouco seca, acadêmica e difícil indo para alguém que quer aprender como ele funciona. No entanto, existem algumas postagens de blog que começaram a explicar os internos de uma forma mais legível. Há uma explicação do amortecedor do anel que é o núcleo do teste padrão do disruptor, uma descrição das barreiras do consumidor (a parte relacionada à leitura do disruptor) e alguma informação no manuseio de vários produtores disponíveis. A descrição mais simples do Disruptor é: É uma forma de enviar mensagens entre threads da maneira mais eficiente possível. Ele pode ser usado como uma alternativa para uma fila, mas também compartilha uma série de recursos com SEDA e Atores. O disruptor fornece a capacidade de passar uma mensagem para outros threads, acordá-lo se necessário (semelhante a um BlockingQueue). No entanto, existem 3 diferenças distintas. O usuário do Disruptor define como as mensagens são armazenadas estendendo a classe Entry e fornecendo uma fábrica para fazer a pré-alocação. Isso permite a reutilização de memória (cópia) ou a entrada pode conter uma referência a outro objeto. Colocar mensagens no disruptor é um processo de 2 fases, primeiro um slot é reivindicado no buffer de anel, que fornece ao usuário com a entrada que pode ser preenchido com os dados apropriados. Em seguida, a entrada deve ser confirmada, esta abordagem em duas fases é necessária para permitir a utilização flexível da memória acima mencionada. É o commit que torna a mensagem visível aos segmentos de consumo. É da responsabilidade do consumidor acompanhar as mensagens que foram consumidas a partir do buffer de anel. Movendo essa responsabilidade para fora do buffer de anel em si ajudou a reduzir a quantidade de contenção de gravação como cada thread mantém seu próprio contador. Comparado a atores O modelo de Ator é mais próximo do Disruptor do que a maioria dos outros modelos de programação, especialmente se você usar as classes BatchConsumer / BatchHandler fornecidas. Essas classes esconder todas as complexidades de manter os números de seqüência consumidos e fornecer um conjunto de callbacks simples quando ocorrem eventos importantes. No entanto, há algumas diferenças sutis. O Disruptor usa um modelo de 1 segmento - 1 consumidor, onde os atores usam um modelo N: M, ou seja, você pode ter tantos atores como você gosta e eles serão distribuídos através de um número fixo de threads (geralmente 1 por núcleo). A interface BatchHandler fornece um callback adicional (e muito importante) onEndOfBatch (). Isto permite consumidores lentos, e. Aqueles fazendo I / O para lote eventos juntos para melhorar throughput. É possível fazer batching em outras estruturas de ator, no entanto, como quase todos os outros frameworks não fornecem um retorno de chamada no final do lote você precisa usar um tempo limite para determinar o fim do lote, resultando em latência fraca. O LMAX construiu o padrão Disruptor para substituir uma abordagem baseada em SEDA. A principal melhoria que proporcionou ao longo do SEDA foi a capacidade de trabalhar em paralelo. Para fazer isso, o Disruptor suporta multi-casting as mesmas mensagens (na mesma ordem) para vários consumidores. Isso evita a necessidade de estágios da forquilha na tubulação. Também permitimos que os consumidores esperem pelos resultados de outros consumidores sem ter que colocar outro estágio entre eles. Um consumidor pode simplesmente assistir o número de seqüência de um consumidor que é dependente. Isso evita a necessidade de juntar etapas no pipeline. Comparado com Barreiras de Memória Outra maneira de pensar sobre isso é como uma barreira de memória estruturada e ordenada. Onde a barreira do produtor forma a barreira de escrita e a barreira do consumidor é a barreira de leitura. Em vez de ler "Vamos também permitir que os consumidores aguardem os resultados de outros consumidores com a necessidade de colocar outro estágio de fila entre eles". Também permitimos que os consumidores esperem no Resultados de outros consumidores sem ter que colocar outro estágio de enfileiramento entre eles (isto é, com quotwithot deve ser substituído por quotwithoutquot) ndash runeks Apr 10 13 at 17:46 Primeiro wed como para entender o modelo de programação que oferece. Há um ou mais escritores. Há um ou mais leitores. Há uma linha de entradas, totalmente ordenadas de velho para novo (retratado como da esquerda para a direita). Os escritores podem adicionar novas entradas na extremidade direita. Cada leitor lê as entradas sequencialmente da esquerda para a direita. Os leitores não podem ler escritores do passado, obviamente. Não há nenhum conceito de exclusão de entrada. Eu uso leitor em vez de consumidor para evitar a imagem de entradas sendo consumido. No entanto, entendemos que as entradas à esquerda do último leitor se tornam inúteis. Geralmente os leitores podem ler simultaneamente e independentemente. No entanto, podemos declarar dependências entre os leitores. As dependências do leitor podem ser um gráfico acíclico arbitrário. Se o leitor B depende do leitor A, o leitor B não pode ler o leitor A passado. A dependência do leitor surge porque o leitor A pode anotar uma entrada eo leitor B depende dessa anotação. Por exemplo, A faz algum cálculo em uma entrada e armazena o resultado no campo a na entrada. A, em seguida, avançar, e agora B pode ler a entrada, eo valor de um A armazenado. Se o leitor C não depende de A, C não deve tentar ler a. Este é realmente um interessante modelo de programação. Independentemente do desempenho, o modelo sozinho pode beneficiar lotes de aplicações. Claro, o principal objetivo da LMAX é o desempenho. Ele usa um anel pré-alocado de entradas. O anel é grande o suficiente, mas seu limite de modo que o sistema não será carregado além da capacidade de design. Se o anel estiver cheio, o (s) escritor (es) esperará até que os leitores mais lentos avancem e criem espaço. Objetos de entrada são pré-alocados e vivem para sempre, para reduzir o custo de coleta de lixo. Não inserimos novos objetos de entrada ou excluímos objetos de entrada antigos, em vez disso, um escritor solicita uma entrada pré-existente, preencher seus campos e notificar os leitores. Esta aparente ação de 2 fases é realmente simplesmente uma ação atômica. A pré-alocação de entradas também significa entradas adjacentes (muito prováveis) localizadas em células de memória adjacentes e porque os leitores lêem entradas sequencialmente, isso é importante para utilizar caches de CPU. E muitos esforços para evitar bloqueio, CAS, mesmo a barreira de memória (por exemplo, use uma variável de seqüência não-volátil se theres apenas um escritor) Para desenvolvedores de leitores: leitores de anotação diferentes devem escrever em diferentes campos, para evitar a contenção de escrita. (Na verdade, eles devem escrever em diferentes linhas de cache.) Um leitor de anotações não deve tocar em nada que outros leitores não dependentes possam ler. É por isso que eu digo que esses leitores anotam entradas, em vez de modificar entradas. Eu realmente tomei o tempo para estudar a fonte real, por pura curiosidade, ea idéia por trás disso é bastante simples. A versão mais recente no momento de escrever este post é 3.2.1. Há um buffer armazenando eventos pré-alocados que manterão os dados para os consumidores lerem. O buffer é respaldado por uma matriz de sinalizadores (matriz de inteiros) de seu comprimento que descreve a disponibilidade dos slots de buffer (veja mais detalhes). A matriz é acessada como um javaAtomicIntegerArray, portanto, para o propósito deste explenation você pode bem assumi-lo para ser um. Pode haver qualquer número de produtores. Quando o produtor quer escrever para o buffer, um número longo é gerado (como em chamar AtomicLonggetAndIncrement, o Disruptor realmente usa sua própria implementação, mas funciona da mesma maneira). Vamos chamar isso gerado por muito tempo um productCallId. De forma semelhante, um consumerCallId é gerado quando um consumidor ENDS lê um slot de um buffer. O consumerCallId mais recente é acessado. (Se houver muitos consumidores, a chamada com o menor id é escolhida.) Esses ids são então comparados, e se a diferença entre os dois é menor que o lado do buffer, o produtor tem permissão para escrever. (Se o productorCallId for maior do que o recente consumerCallId bufferSize, isso significa que o buffer está cheio eo produtor é forçado a esperar por um barramento até que um ponto fique disponível). O produtor é então atribuído o slot no buffer com base em seu callId (Que é prducerCallId modulo bufferSize, mas uma vez que o bufferSize é sempre uma potência de 2 (limite reforçado na criação de buffer), a operação actuall usada é productCallId amp (bufferSize - 1)). É então livre para modificar o evento nesse slot. (O algoritmo real é um pouco mais complicado, envolvendo caching recente consumerId em uma referência atômica separada, para fins de otimização.) Quando o evento foi modificado, a alteração é publicada. Ao publicar o respectivo intervalo na matriz de sinalizadores é preenchido com o sinalizador actualizado. O valor de sinalizador é o número do loop (productCallId dividido por bufferSize (novamente, uma vez que bufferSize é potência de 2, a operação real é um turno à direita). De forma semelhante, pode haver qualquer número de consumidores. Acessar o buffer, um consumerCallId é gerado (dependendo de como os consumidores foram adicionados ao disruptor o atômico usado em id geração pode ser compartilhada ou separada para cada um deles.) Este consumerCallId é então comparado com o mais recente producentCallId, e se É menor dos dois, o leitor é permitido para progredir (Da mesma forma, se o producerCallId é mesmo para o consumerCallId, isso significa que o buffer é empety eo consumidor é forçado a esperar. A maneira de espera é definida por um WaitStrategy durante disruptor Criação). Para os consumidores individuais (aqueles com seu próprio gerador de id), a próxima coisa verificada é a capacidade de lote consumir. Os entalhes no buffer são examinados em ordem do respectivo para o consumerCallId (o índice é determinado no Da mesma forma que para os produtores), para o respectivo produtor recente. Eles são examinados em um loop, comparando o valor de sinalizador escrito na matriz de sinalizadores, contra um valor de sinalizador gerado para o consumerCallId. Se as bandeiras corresponderem significa que os produtores que preenchem as faixas horárias comprometeram as suas alterações. Se não, o loop é interrompido, eo mais alto commited changeId é retornado. Os slots do ConsumerCallId para recebidos em changeId podem ser consumidos em lote. Se um grupo de consumidores leu em conjunto (aqueles com gerador de id compartilhado), cada um só leva um único callId, e apenas o slot para esse único callId é verificado e retornado. O padrão disruptor é uma fila batching suportada por um arranjo circular (isto é, o buffer de anel) cheio de objetos de transferência pré-alocados que usa barreiras de memória para sincronizar produtores e consumidores através de sequências. As barreiras de memória são um pouco difíceis de explicar e o blog Trishas fez a melhor tentativa na minha opinião com este post: mechanitis. blogspot / 2011/08 / dissecting-disruptor-why-its-so-fast. html Mas se você não quiser Para mergulhar nos detalhes de nível baixo você pode apenas saber que as barreiras de memória em Java são implementadas através da palavra-chave volátil ou através do java. util. concurrent. AtomicLong. As seqüências de padrão disruptor são AtomicLong s e são comunicadas de um lado para outro entre produtores e consumidores através de barreiras de memória em vez de bloqueios. Eu acho mais fácil entender um conceito através de código, então o código abaixo é um helloworld simples de CoralQueue. Que é uma implementação de padrão disruptor feito por CoralBlocks com o qual eu sou afiliado. No código abaixo você pode ver como o padrão disruptor implementa lote e como o buffer de anel (ou seja, matriz circular) permite a comunicação sem lixo entre dois segmentos: respondido Jun 16 14 às 22:38 A LMAX Arquitetura Conteúdo Durante os últimos anos Continuamos ouvindo que o almoço gratuito é over1 - não podemos esperar aumentos na velocidade de CPU individual. Então, para escrever código rápido precisamos usar explicitamente vários processadores com software concorrente. Esta não é uma boa notícia - escrever código concorrente é muito difícil. Os bloqueios e semáforos são difíceis de raciocinar e difíceis de testar - o que significa que estamos gastando mais tempo preocupando-nos em satisfazer o computador do que estamos solucionando o problema de domínio. Vários modelos de concorrência, como Actors e Software Transactional Memory, visam tornar isso mais fácil - mas ainda existe um fardo que introduz bugs e complexidade. Então fiquei fascinado ao ouvir falar de uma conversa no QCon London em março do ano passado do LMAX. LMAX é uma nova plataforma de negociação financeira de varejo. Sua inovação empresarial é que é uma plataforma de varejo - permitindo que qualquer pessoa negocie com uma gama de produtos financeiros derivados2. Uma plataforma de negociação como esta precisa de latência muito baixa - os comércios têm que ser processados ​​rapidamente porque o mercado está se movendo rapidamente. Uma plataforma de varejo acrescenta complexidade porque tem que fazer isso para muitas pessoas. Portanto, o resultado é mais usuários, com muitos negócios, todos os quais precisam ser processados ​​rapidamente.3 Dada a mudança para o pensamento multi-core, este tipo de desempenho exigente naturalmente sugeriria um modelo de programação explicitamente concorrente - e de fato este era o seu ponto de partida. Mas a coisa que got peoples atenção no QCon foi que este wasnt onde terminou. Na verdade, eles acabaram fazendo toda a lógica de negócios para sua plataforma: todos os comércios, de todos os clientes, em todos os mercados - em um único segmento. Um segmento que processará 6 milhões de pedidos por segundo usando hardware commodity.4 Processamento de lotes de transações com baixa latência e nenhuma das complexidades do código concorrente - como posso resistir a cavar em que Felizmente outra diferença LMAX tem para outras empresas financeiras é que Eles estão muito felizes em falar sobre suas decisões tecnológicas. Então, agora LMAX tem sido em produção por um tempo seu tempo para explorar seu design fascinante. Estrutura Geral Figura 1: Arquitetura LMAXs em três blobs Em um nível superior, a arquitetura tem três partes processador de lógica de negócios5 disruptores de saída de disruptores de entrada Como seu nome indica, o processador de lógica de negócios lida com toda a lógica de negócios no aplicativo. Como eu indiquei acima, ele faz isso como um programa java single-threaded que reage a chamadas de método e produz eventos de saída. Consequentemente, é um programa java simples que não requer nenhuma estrutura de plataforma para ser executada além da própria JVM, o que permite que ela seja executada facilmente em ambientes de teste. Embora o Business Logic Processor possa ser executado em um ambiente simples para testes, há uma coreografia bastante mais envolvida para que ele seja executado em uma configuração de produção. As mensagens de entrada precisam ser retiradas de um gateway de rede e unmarshaled, replicado e registrado no diário. As mensagens de saída precisam ser empacotadas para a rede. Essas tarefas são tratadas pelos disruptores de entrada e saída. Diferentemente do Business Logic Processor, estes são componentes concorrentes, uma vez que envolvem operações de E / S que são lentas e independentes. Eles foram projetados e construídos especialmente para LMAX, mas eles (como a arquitetura geral) são aplicáveis ​​em outro lugar. Processador Lógico de Negócios Mantendo tudo na memória O Processador de Lógica Empresarial toma as mensagens de entrada seqüencialmente (na forma de uma invocação de método), executa a lógica de negócios nele e emite eventos de saída. Ele opera inteiramente em memória, não há banco de dados ou outro armazenamento persistente. Manter todos os dados na memória tem dois benefícios importantes. Em primeiro lugar o seu rápido - não há banco de dados para fornecer IO lento para acessar, nem há qualquer comportamento transacional para executar uma vez que todo o processamento é feito seqüencialmente. A segunda vantagem é que simplifica a programação - não há mapeamento objeto / relacional para fazer. Todo o código pode ser escrito usando o modelo de objeto Javas sem ter que fazer quaisquer compromissos para o mapeamento para um banco de dados. Usar uma estrutura em memória tem uma conseqüência importante - o que acontece se tudo falhar Mesmo os sistemas mais resistentes são vulneráveis ​​a alguém que está puxando o poder. O coração de lidar com isso é o Sourcing de Eventos - o que significa que o estado atual do Processador Lógico de Negócios é inteiramente derivável processando os eventos de entrada. Contanto que o fluxo de eventos de entrada seja mantido em um armazenamento durável (que é um dos trabalhos do disruptor de entrada), você sempre pode recriar o estado atual do mecanismo de lógica de negócios repetindo os eventos. Uma boa maneira de entender isso é pensar em um sistema de controle de versão. Sistemas de controle de versão são uma seqüência de compromissos, a qualquer momento você pode construir uma cópia de trabalho, aplicando os compromissos. VCSs são mais complicados do que o Business Logic Processor, porque eles devem suportar ramificação, enquanto o Business Logic Processor é uma seqüência simples. Portanto, em teoria, você sempre pode reconstruir o estado do Processador Lógico Empresarial reprocessando todos os eventos. Na prática, no entanto, isso levaria muito tempo se você precisar girar um. Assim, assim como com os sistemas de controle de versão, o LMAX pode fazer instantâneos do estado do Processador de Lógica de Negócios e restaurá-los a partir dos instantâneos. Eles tomam um instantâneo todas as noites durante os períodos de baixa atividade. Reiniciar o Business Logic Processor é rápido, uma reinicialização completa - incluindo a reinicialização da JVM, o carregamento de um instantâneo recente e a repetição de dias de revistas - leva menos de um minuto. Os instantâneos tornam mais rápida a inicialização de um novo Processador Lógico Empresarial, mas não o suficiente se um Processador de Lógica Empresarial travar às 14h. Como resultado, o LMAX mantém múltiplos Processadores Lógicos de Negócios em execução o tempo todo6. Cada evento de entrada é processado por vários processadores, mas todos menos um processador tem sua saída ignorada. Se o processador live falhar, o sistema alternará para outro. Essa capacidade de lidar com o fail-over é outro benefício de usar o Sourcing de eventos. Por evento de abastecimento em réplicas que podem alternar entre os processadores em questão de micro-segundos. Além de tirar instantâneos todas as noites, eles também reiniciam os processadores de lógica de negócios todas as noites. A replicação permite que eles façam isso sem tempo de inatividade, então eles continuam a processar os comércios 24/7. Para obter mais informações sobre o Sourcing de eventos, consulte o modelo de rascunho no meu site há alguns anos. O artigo está mais focado no tratamento de relações temporais do que nos benefícios que o LMAX usa, mas explica a idéia central. Event Sourcing é valioso porque permite que o processador funcione inteiramente em memória, mas tem outra vantagem considerável para o diagnóstico. Se ocorrer algum comportamento inesperado, a equipe copia a seqüência de eventos para seu ambiente de desenvolvimento e os reproduz lá. Isso lhes permite examinar o que aconteceu com muito mais facilidade do que é possível na maioria dos ambientes. Esse recurso de diagnóstico se estende aos diagnósticos de negócios. Existem algumas tarefas de negócios, como no gerenciamento de riscos, que exigem uma computação significativa que não é necessária para o processamento de pedidos. Um exemplo é obter uma lista dos 20 principais clientes por perfil de risco com base em suas posições atuais de negociação. A equipe lida com isso girando um modelo de domínio replicado e executando a computação lá, onde ele não interferirá com o processamento da ordem do núcleo. Esses modelos de domínio de análise podem ter modelos de dados variantes, manter conjuntos de dados diferentes na memória e executar em máquinas diferentes. Desempenho de sintonia Até agora, Ive explicou que a chave para a velocidade do Processador de Lógica Empresarial está fazendo tudo seqüencialmente, na memória. Basta fazer isso (e nada realmente estúpido) permite que os desenvolvedores para escrever o código que pode processar 10K TPS7. Eles então descobriram que concentrar-se nos elementos simples de código bom poderia trazer isso para a faixa de 100K TPS. Isso só precisa de código bem-factored e pequenos métodos - essencialmente, isso permite Hotspot para fazer um trabalho melhor de otimização e CPUs para ser mais eficiente no cache do código como sua execução. Demorou um pouco mais inteligência para subir outra ordem de magnitude. Há várias coisas que a equipe LMAX achou útil para chegar lá. Um deles era escrever implementações personalizadas das coleções java que foram projetadas para ser cache-friendly e cuidadoso com o garbage8. Um exemplo disto é usar primitivos java longs como chaves hashmap com uma matriz especialmente escrita backed Map implementação (LongToObjectHashMap). Em geral, eles descobriram que a escolha de estruturas de dados muitas vezes faz uma grande diferença. A maioria dos programadores simplesmente pega qualquer Lista que usou na última vez, em vez de pensar qual implementação é a correta para esse contexto.9 Outra técnica para alcançar esse nível máximo de desempenho é colocar Atenção em testes de desempenho. Ive longo observado que as pessoas falam muito sobre técnicas para melhorar o desempenho, mas a única coisa que realmente faz a diferença é testá-lo. Mesmo os programadores bons são muito bons em construir argumentos do desempenho que terminam acima de ser errado, assim que os melhores programadores preferem profileers e casos do teste à especulação. A equipe de LMAX encontrou também que os testes da escrita primeiramente são uma disciplina muito eficaz para testes de desempenho. Modelo de Programação Esse estilo de processamento introduz algumas restrições na forma como você escreve e organiza a lógica de negócios. A primeira delas é que você tem que provocar qualquer interação com serviços externos. Uma chamada de serviço externa vai ser lenta, e com um único thread irá parar toda a máquina de processamento de pedidos. Como resultado, você não pode fazer chamadas para serviços externos dentro da lógica de negócios. Em vez disso, você precisa concluir essa interação com um evento de saída e aguardar outro evento de entrada para selecioná-lo novamente. Ill usar um exemplo simples não-LMAX para ilustrar. Imagine que você está fazendo uma ordem para feijões de geléia por cartão de crédito. Um sistema de varejo simples levaria suas informações de pedido, usaria um serviço de validação de cartão de crédito para verificar seu número de cartão de crédito e, em seguida, confirmar sua ordem - tudo em uma única operação. O segmento de processamento de sua ordem seria bloquear enquanto espera para o cartão de crédito a ser verificado, mas esse bloco wouldnt ser muito longo para o usuário, eo servidor sempre pode executar outro thread no processador enquanto a sua espera. Na arquitetura LMAX, você dividiria esta operação em dois. A primeira operação capturaria a informação da ordem e terminaria por produzir um evento (validação de cartão de crédito solicitada) para a empresa de cartão de crédito. O Processador Lógico de Negócio continuaria a processar eventos para outros clientes até receber um evento validado por cartão de crédito no seu fluxo de eventos de entrada. Ao processar esse evento, realizaria as tarefas de confirmação para essa ordem. Trabalhar nesse tipo de estilo assíncrono movido por eventos é algo incomum - embora usar a assincronia para melhorar a capacidade de resposta de uma aplicação seja uma técnica familiar. Ele também ajuda o processo de negócios a ser mais resistente, como você tem que ser mais explícito em pensar sobre as diferentes coisas que podem acontecer com o aplicativo remoto. Uma segunda característica do modelo de programação reside no tratamento de erros. O modelo tradicional de sessões e transações de banco de dados fornece um recurso de manipulação de erros útil. Se algo der errado, é fácil jogar fora tudo o que aconteceu até agora na interação. Os dados da sessão são transitórios, e podem ser descartados, ao custo de alguma irritação ao usuário se no meio de algo complicado. Se ocorrer um erro no lado do banco de dados, você pode reverter a transação. LMAXs em estruturas de memória são persistentes em eventos de entrada, por isso, se houver um erro é importante não deixar essa memória em um estado inconsistente. No entanto não há nenhuma facilidade automatizada de rollback. Como conseqüência, a equipe LMAX coloca muita atenção em assegurar que os eventos de entrada são totalmente válidos antes de fazer qualquer mutação do estado persistente na memória. Eles descobriram que o teste é uma ferramenta fundamental para liberar esses tipos de problemas antes de entrar em produção. Disruptores de entrada e saída Embora a lógica de negócios ocorra em um único segmento, há um número de tarefas a serem executadas antes de podermos invocar um método de objeto de negócios. A entrada original para processamento sai do fio na forma de uma mensagem, esta mensagem precisa ser unmarshaled em um formulário conveniente para Business Logic Processor para usar. O Sourcing de eventos depende de manter um diário durável de todos os eventos de entrada, de modo que cada mensagem de entrada precisa ser registrada em uma loja durável. Finalmente, a arquitetura depende de um cluster de Processadores Lógicos de Negócios, portanto precisamos replicar as mensagens de entrada nesse cluster. Do mesmo modo, no lado da saída, os eventos de saída precisam ser empacotados para transmissão pela rede. Figura 2: As atividades realizadas pelo disruptor de entrada (usando a notação de diagrama de atividade UML) O replicador e o journaler envolvem IO e, portanto, são relativamente lentos. Afinal, a idéia central do Business Logic Processor é que evita fazer qualquer IO. Além disso, essas três tarefas são relativamente independentes, todas elas precisam ser feitas antes que o Business Logic Processor funcione em uma mensagem, mas eles podem ser feitos em qualquer ordem. Assim, ao contrário do Business Logic Processor, onde cada troca altera o mercado para negociações subseqüentes, há um ajuste natural para a concorrência. Para lidar com esta concorrência, a equipe LMAX desenvolveu um componente de concorrência especial, que eles chamam de Disruptor 11. A equipe LMAX lançou o código-fonte para o Disruptor com uma licença de código aberto. Em um nível bruto você pode pensar de um disruptor como um gráfico de multicast de filas onde os produtores colocar objetos sobre ele que são enviados para todos os consumidores para o consumo paralelo através de filas separadas downstream. Quando você olha dentro de você vê que esta rede de filas é realmente uma única estrutura de dados - um buffer de anel. Cada produtor e consumidor tem um contador de seqüência para indicar qual slot no buffer está trabalhando atualmente. Cada produtor / consumidor escreve seu próprio contador de seqüência, mas pode ler os outros contadores de seqüência. Desta forma, o produtor pode ler os contadores de consumidores para garantir que o slot que deseja gravar está disponível sem bloqueios nos contadores. Da mesma forma, um consumidor pode garantir que ele só processa mensagens quando outro consumidor é feito com ele assistindo os contadores. Figura 3: O disruptor de entrada coordena um produtor e quatro consumidores Os disruptores de saída são semelhantes, mas eles só têm dois consumidores seqüenciais para empacotamento e saída.12 Eventos de saída são organizados em vários tópicos, de modo que as mensagens podem ser enviadas apenas para os receptores interessados neles. Cada tópico tem seu próprio disruptor. Os disruptores Ive descritos são usados ​​em um estilo com um produtor e múltiplos consumidores, mas isso não é uma limitação do design do disruptor. O disruptor pode trabalhar com vários produtores também, neste caso, ele ainda não precisa de bloqueios.13 Um benefício do design disruptor é que torna mais fácil para os consumidores apanhar rapidamente se eles correm em um problema e ficam para trás. Se o unmarshaler tem um problema ao processar no slot 15 e retorna quando o receptor está no slot 31, ele pode ler dados de slots 16-30 em um lote para recuperar o atraso. Este lote de leitura dos dados do disruptor torna mais fácil para os consumidores atrasados ​​para apanhar rapidamente, reduzindo assim a latência geral. Ive descrito as coisas aqui, com um cada do journalista, replicador e unmarshaler - este é realmente o que LMAX faz. Mas o projeto permitiria que vários desses componentes fossem executados. Se você funcionasse dois journalers então um faria exame dos entalhes uniformes eo journalista outro faria exame dos entalhes impares. Isto permite simultaneidade adicional destas operações de E / S se isto se tornar necessário. Os buffers de anel são grandes: 20 milhões de slots para buffer de entrada e 4 milhões de slots para cada um dos buffers de saída. Os contadores de seqüência são inteiros de 64 bits inteiros que aumentam monotonicamente mesmo quando os retângulos de anel se envolvem.14 O buffer é configurado para um tamanho que é uma potência de dois para que o compilador possa fazer uma operação de módulo eficiente para mapear a partir do número do contador de seqüência para o número de slot . Como o resto do sistema, os disruptores são saltados durante a noite. Este salto é feito principalmente para limpar a memória de modo que há menos chance de um evento de coleta de lixo caro durante a negociação. (Eu também acho que é um bom hábito para reiniciar regularmente, para que você ensaiar como fazê-lo para emergências.) O trabalho jornalistas é armazenar todos os eventos em um formulário durável, de modo que eles podem ser repetidos se algo der errado. LMAX não usa um banco de dados para isso, apenas o sistema de arquivos. Elas transmitem os eventos para o disco. Em termos modernos, os discos mecânicos são horrivelmente lentos para acesso aleatório, mas muito rápidos para streaming - daí o disco de tag-line é a nova fita.15 Anteriormente eu mencionei que o LMAX executa várias cópias de seu sistema em um cluster para suportar failover rápido . O replicador mantém esses nós sincronizados. Todas as comunicações em LMAX usam o multicasting do IP, assim que os clientes não necessitam saber qual o IP address é o nó mestre. Somente o nó mestre escuta diretamente os eventos de entrada e executa um replicador. O replicador transmite os eventos de entrada para os nós escravos. Se o nó mestre desce, sua falta de pulsação será notada, outro nó se tornará mestre, iniciará o processamento de eventos de entrada e iniciará seu replicador. Cada nó tem seu próprio disruptor de entrada e, portanto, tem seu próprio diário e faz o seu próprio unmarshaling. Mesmo com multicast IP, a replicação ainda é necessária porque as mensagens IP podem chegar em uma ordem diferente em nós diferentes. O nó mestre fornece uma seqüência determinística para o resto do processamento. O unmarshaler transforma os dados de evento do fio em um objeto java que pode ser usado para invocar o comportamento no Business Logic Processor. Portanto, ao contrário dos outros consumidores, ele precisa modificar os dados no buffer de anel para que ele possa armazenar esse objeto unmarshaled. A regra aqui é que os consumidores são autorizados a escrever para o buffer de anel, mas cada campo gravável só pode ter um consumidor paralelo que é permitido escrever para ele. Isso preserva o princípio de ter apenas um único escritor. Figura 4: A arquitetura LMAX com os disruptores expandidos O disruptor é um componente de uso geral que pode ser usado fora do sistema LMAX. Normalmente, as empresas financeiras são muito segredo sobre seus sistemas, mantendo-se quieto mesmo sobre itens que não são pertinentes para o seu negócio. Não só o LMAX tem sido aberto sobre sua arquitetura geral, eles têm open-sourced o código disruptor - um ato que me faz muito feliz. Not just will this allow other organizations to make use of the disruptor, it will also allow for more testing of its concurrency properties. Queues and their lack of mechanical sympathy The LMAX architecture caught peoples attention because its a very different way of approaching a high performance system to what most people are thinking about. So far Ive talked about how it works, but havent delved too much into why it was developed this way. This tale is interesting in itself, because this architecture didnt just appear. It took a long time of trying more conventional alternatives, and realizing where they were flawed, before the team settled on this one. Most business systems these days have a core architecture that relies on multiple active sessions coordinated through a transactional database. The LMAX team were familiar with this approach, and confident that it wouldnt work for LMAX. This assessment was founded in the experiences of Betfair - the parent company who set up LMAX. Betfair is a betting site that allows people to bet on sporting events. It handles very high volumes of traffic with a lot of contention - sports bets tend to burst around particular events. To make this work they have one of the hottest database installations around and have had to do many unnatural acts in order to make it work. Based on this experience they knew how difficult it was to maintain Betfairs performance and were sure that this kind of architecture would not work for the very low latency that a trading site would require. As a result they had to find a different approach. Their initial approach was to follow what so many are saying these days - that to get high performance you need to use explicit concurrency. For this scenario, this means allowing orders to be processed by multiple threads in parallel. However, as is often the case with concurrency, the difficulty comes because these threads have to communicate with each other. Processing an order changes market conditions and these conditions need to be communicated. The approach they explored early on was the Actor model and its cousin SEDA. The Actor model relies on independent, active objects with their own thread that communicate with each other via queues. Many people find this kind of concurrency model much easier to deal with than trying to do something based on locking primitives. The team built a prototype exchange using the actor model and did performance tests on it. What they found was that the processors spent more time managing queues than doing the real logic of the application. Queue access was a bottleneck. When pushing performance like this, it starts to become important to take account of the way modern hardware is constructed. The phrase Martin Thompson likes to use is mechanical sympathy. The term comes from race car driving and it reflects the driver having an innate feel for the car, so they are able to feel how to get the best out of it. Many programmers, and I confess I fall into this camp, dont have much mechanical sympathy for how programming interacts with hardware. Whats worse is that many programmers think they have mechanical sympathy, but its built on notions of how hardware used to work that are now many years out of date. One of the dominant factors with modern CPUs that affects latency, is how the CPU interacts with memory. These days going to main memory is a very slow operation in CPU-terms. CPUs have multiple levels of cache, each of which of is significantly faster. So to increase speed you want to get your code and data in those caches. At one level, the actor model helps here. You can think of an actor as its own object that clusters code and data, which is a natural unit for caching. But actors need to communicate, which they do through queues - and the LMAX team observed that its the queues that interfere with caching. The explanation runs like this: in order to put some data on a queue, you need to write to that queue. Similarly, to take data off the queue, you need to write to the queue to perform the removal. This is write contention - more than one client may need to write to the same data structure. To deal with the write contention a queue often uses locks. But if a lock is used, that can cause a context switch to the kernel. When this happens the processor involved is likely to lose the data in its caches. The conclusion they came to was that to get the best caching behavior, you need a design that has only one core writing to any memory location17. Multiple readers are fine, processors often use special high-speed links between their caches. But queues fail the one-writer principle. This analysis led the LMAX team to a couple of conclusions. Firstly it led to the design of the disruptor, which determinedly follows the single-writer constraint. Secondly it led to idea of exploring the single-threaded business logic approach, asking the question of how fast a single thread can go if its freed of concurrency management. The essence of working on a single thread, is to ensure that you have one thread running on one core, the caches warm up, and as much memory access as possible goes to the caches rather than to main memory. This means that both the code and the working set of data needs to be as consistently accessed as possible. Also keeping small objects with code and data together allows them to be swapped between the caches as a unit, simplifying the cache management and again improving performance. An essential part of the path to the LMAX architecture was the use of performance testing. The consideration and abandonment of an actor-based approach came from building and performance testing a prototype. Similarly much of the steps in improving the performance of the various components were enabled by performance tests. Mechanical sympathy is very valuable - it helps to form hypotheses about what improvements you can make, and guides you to forward steps rather than backward ones - but in the end its the testing gives you the convincing evidence. Performance testing in this style, however, is not a well-understood topic. Regularly the LMAX team stresses that coming up with meaningful performance tests is often harder than developing the production code. Again mechanical sympathy is important to developing the right tests. Testing a low level concurrency component is meaningless unless you take into account the caching behavior of the CPU. One particular lesson is the importance of writing tests against null components to ensure the performance test is fast enough to really measure what real components are doing. Writing fast test code is no easier than writing fast production code and its too easy to get false results because the test isnt as fast as the component its trying to measure. Should you use this architecture At first glance, this architecture appears to be for a very small niche. After all the driver that led to it was to be able to run lots of complex transactions with very low latency - most applications dont need to run at 6 million TPS. But the thing that fascinates me about this application, is that they have ended up with a design which removes much of the programming complexity that plagues many software projects. The traditional model of concurrent sessions surrounding a transactional database isnt free of hassles. Theres usually a non-trivial effort that goes into the relationship with the database. Object/relational mapping tools can help much of the pain of dealing with a database, but it doesnt deal with it all. Most performance tuning of enterprise applications involves futzing around with SQL. These days, you can get more main memory into your servers than us old guys could get as disk space. More and more applications are quite capable of putting all their working set in main memory - thus eliminating a source of both complexity and sluggishness. Event Sourcing provides a way to solve the durability problem for an in-memory system, running everything in a single thread solves the concurrency issue. The LMAX experience suggests that as long as you need less than a few million TPS, youll have enough performance headroom. There is a considerable overlap here with the growing interest in CQRS. An event sourced, in-memory processor is a natural choice for the command-side of a CQRS system. (Although the LMAX team does not currently use CQRS.) So what indicates you shouldnt go down this path This is always a tricky questions for little-known techniques like this, since the profession needs more time to explore its boundaries. A starting point, however, is to think of the characteristics that encourage the architecture. One characteristic is that this is a connected domain where processing one transaction always has the potential to change how following ones are processed. With transactions that are more independent of each other, theres less need to coordinate, so using separate processors running in parallel becomes more attractive. LMAX concentrates on figuring the consequences of how events change the world. Many sites are more about taking an existing store of information and rendering various combinations of that information to as many eyeballs as they can find - eg think of any media site. Here the architectural challenge often centers on getting your caches right. Another characteristic of LMAX is that this is a backend system, so its reasonable to consider how applicable it would be for something acting in an interactive mode. Increasingly web application are helping us get used to server systems that react to requests, an aspect that does fit in well with this architecture. Where this architecture goes further than most such systems is its absolute use of asynchronous communications, resulting in the changes to the programming model that I outlined earlier. These changes will take some getting used to for most teams. Most people tend to think of programming in synchronous terms and are not used to dealing with asynchrony. Yet its long been true that asynchronous communication is an essential tool for responsiveness. It will be interesting to see if the wider use of asynchronous communication in the javascript world, with AJAX and node. js, will encourage more people to investigate this style. The LMAX team found that while it took a bit of time to adjust to asynchronous style, it soon became natural and often easier. In particular error handling was much easier to deal with under this approach. The LMAX team certainly feels that the days of the coordinating transactional database are numbered. The fact that you can write software more easily using this kind of architecture and that it runs more quickly removes much of the justification for the traditional central database. For my part, I find this a very exciting story. Much of my goal is to concentrate on software that models complex domains. An architecture like this provides good separation of concerns, allowing people to focus on Domain-Driven Design and keeping much of the platform complexity well separated. The close coupling between domain objects and databases has always been an irritation - approaches like this suggest a way out. if you found this article useful, please share it. I appreciate the feedback and encouragementDissecting the Disruptor: Whats so special about a ring buffer Recently we open sourced the LMAX Disruptor. the key to what makes our exchange so fast. Why did we open source it Well, weve realised that conventional wisdom around high performance programming is. a bit wrong. Weve come up with a better, faster way to share data between threads, and it would be selfish not to share it with the world. Plus it makes us look dead clever. On the site you can download a technical article explaining what the Disruptor is and why its so clever and fast. I even get a writing credit on it, which is gratifying when all I really did is insert commas and re-phrase sentences I didnt understand. However I find the whole thing a bit much to digest all at once, so Im going to explain it in smaller pieces, as suits my NADD audience. First up - the ring buffer. Initially I was under the impression the Disruptor was just the ring buffer. But Ive come to realise that while this data structure is at the heart of the pattern, the clever bit about the Disruptor is controlling access to it. What on earth is a ring buffer Well, it does what it says on the tin - its a ring (its circular and wraps), and you use it as a buffer to pass stuff from one context (one thread) to another: (OK, I drew it in Paint. Im experimenting with sketch styles and hoping my OCD doesnt kick in and demand perfect circles and straight lines at precise angles). So basically its an array with a pointer to the next available slot. As you keep filling up the buffer (and presumable reading from it too), the sequence keeps incrementing, wrapping around the ring: To find the slot in the array that the current sequence points to you use a mod operation: sequence mod array length array index So for the above ring buffer (using Java mod syntax): 12 10 2. Easy. Actually it was a total accident that the picture had ten slots. Powers of two work better because computers think in binary. So what If you look at Wikipedias entry on Circular Buffers. youll see one major difference to the way weve implemented ours - we dont have a pointer to the end. We only have the next available sequence number. This is deliberate - the original reason we chose a ring buffer was so we could support reliable messaging. We needed a store of the messages the service had sent, so when another service sent a nak to say they hadnt received some messages, it would be able to resend them. The ring buffer seems ideal for this. It stores the sequence to show where the end of the buffer is, and if it gets a nak it can replay everything from that point to the current sequence: The difference between the ring buffer as weve implemented it, and the queues we had traditionally been using, is that we dont consume the items in the buffer - they stay there until they get over-written. Which is why we dont need the end pointer you see in the Wikipedia version. Deciding whether its OK to wrap or not is managed outside of the data structure itself (this is part of the producer and consumer behaviour - if you cant wait for me to get round to blogging about it, check out the Disruptor site ). And its so great because. So we use this data structure because it gives us some nice behaviour for reliable messaging. It turns out though that it has some other nice characteristics. Firstly, its faster than something like a linked list because its an array, and has a predictable pattern of access. This is nice and CPU-cache-friendly - at the hardware level the entries can be pre-loaded, so the machine is not constantly going back to main memory to load the next item in the ring. Secondly, its an array and you can pre-allocate it up front, making the objects effectively immortal. This means the garbage collector has pretty much nothing to do here. Again, unlike a linked list which creates objects for every item added to the list - these then all need to be cleaned up when the item is no longer in the list. The missing pieces I havent talked about how to prevent the ring wrapping, or specifics around how to write stuff to and read things from the ring buffer. Youll also notice Ive been comparing it to a data structure like a linked list, which I dont think anyone believes is the answer to the worlds problems. The interesting part comes when you compare the Disruptor with an implementation like a queue. Queues usually take care of all the stuff like the start and end of the queue, adding and consuming items, and so forth. All the stuff I havent really touched on with the ring buffer. Thats because the ring buffer itself isnt responsible for these things, weve moved these concerns outside of the data structure. For more details youre just going to have to read the paper or check out the code. Or watch Mike and Martin at QCon San Francisco last year. Or wait for me to have a spare five minutes to get my head around the rest of it. If you don39t consume elements from your ring buffer then you39re keeping them reachable and preventing them from being deallocated. This can obviously have an adverse effect on the throughput and latency of the garbage collector. Writing references into different locations in your ring buffer incurs the write barrier, which can also adversely affect throughput and latency. I wonder what the trade-offs are concerning these disadvantages and when they come into play. With regards to use of memory, no real trade offs are made by the Disruptor. Unlike a queue, you have a choice about how to make use of memory. If solution is a soft real-time system, reducing GC pauses is paramount. Therefore you can re-use the entries in the ring buffer, e. g. copying byte arrays to and from network I/O buffers in and out of the ring buffer (our most common usage pattern). As the amount of memory used by the system remains static is reduces the frequency of garbage collection. It is also possible to implement an Entry that contains a reference to an immutable object. However in that situation it may be necessary for the consumer to null out the message object to reduce the amount of memory that needs to be promoted from Eden. So a little more effort is required from the programmer to build the most appropriate solution. We believe that the flexibility provided justifies this small bit of extra effort. Considering the write barrier, the primary goal of the Disruptor is to pass messages between threads. We make no trade offs regarding ordering or consistency, therefore it is necessary to use memory barriers in the appropriate places. We39ve done our utmost to keep this to a minimum. However, we are many times faster than the popular alternatives as most of them use locks provide consistency. How does this approach compare to the Pool approach and other approaches used here: cacm. acm. org/magazines/2011/3/105308-data-structures-in-the-multicore-age/fulltext Why not use a Pool instead of a queue Is the LIFO requirement essential Unfortunately I can39t read that article because I don39t have an account at that site. FIFO (not LIFO) is absolutely essential - our exchange depends upon predictable ordering, and if you play the same events into it you will always get the same outcome. The Disruptor ensures this ordering without taking the performance penalties usually associated with FIFO structures. Flying Frog Consultancy said quotIf you don39t consume elements from your ring buffer then you39re keeping them reachable and preventing them from being deallocated. This can obviously have an adverse effect on the throughput and latency of the garbage collector. quot The whole point is to not to invoke the garbage collector. The Disruptor pattern allows data to be passed between CPU39s at pretty much the theoretical maximum of the hardware - its been well thought out I39m new to Disruptor pattern. I have a very basic question. How do I add messages to the Ring Buffer from a Multi Threaded Producer. Should the add calls to the Ring Buffer be synchronized Generally the aim is not to run anything multi-threaded. Producers and consumers should be single-threaded. But you can have more than one producer: mechanitis. blogspot/2011/07/dissecting-disruptor-writing-to-ring. html - this is a slightly out of date post, the naming conventions have changed and the producer barrier is now managed by the ring buffer, but I think this might be a good place to start to think about how to solve your problem. Thanks for interesting the article. I am not sure if I understand, but the concept of holding on to memory and reusing already allocated objects to avoid GC pauses does not seem to be new. How is the ring buffer different from an object pool Avoiding GC is not the main aim of the RingBuffer, although it does help towards the speed of the Disruptor. The interesting characteristics of the RingBuffer are that it39s FIFO, and it enables some really nice batching when you read from it. The RingBuffer is not the secret sauce in the Disruptor39s performance, in fact in the current version of the Disruptor you don39t need it at all. It39s worth noting that there39s nothing new in the Disruptor at all, in fact many of the ideas have been around for years. But I don39t think there are any other frameworks in Java that pull together these concepts in this way to give the kind of performance we see when using the Disruptor. Hi Trisha, From a few days I discovered the LMAX architecture and disruptor also, It39s not so clearly to my how exactly the consumers extract the messages from RingBuffer and how exactly a consumer, for example C1, know which messages are for it and not for other consumer, C2. Thanks Sorin. Actually the messages are for both consumers. The default behaviour is that all consumers (or EventHandlers as they are now) read all messages in the RingBuffer. If you have different types of events that are handled by different consumers, then it39s up to the consumer to decide whether to ignore the event or not. So, if C1 handles all blue messages and C2 handles all red ones (over simplification of course) then C1 needs to check it39s a blue message before proceeding. In terms of extracting the messages - you don39t. Messages live on the ring buffer to be read by (and processed by) all consumers, until every consumer has done what it needs to do with it (i. e. every consumer has incremented its sequence number to at least that message39s number) then it will get over-written when the ring wraps. If you want to do something with that message, then you simply read it and do whatever you want with it, even if that39s passing it on to another Disruptor or another part of the system. Hi Trisha, Thanks for this and other presentations. I have a question regarding the disruptor which is rather basic. The consumers (event processors) are not implementing any of the Callable or Runnable interfaces they implement EventHandler, Then how can they run in parallel, so for example I have a disruptor implementation where there is a diamond pattern like this P1 - c1,c2,c3 - c4 - c5 Where c1 to c3 can work in parallel after p1, and C4 and C5 work after them. So conventionally I39d have something like this (with P1 and C1-C5 being runnables/callables) But in case of the Disruptor none of my event handlers implement Runnable or Callable, so how39d the disruptor framework end up running them in parallel Take following sceanrio: My consumer C2 requires to make a webservice call for some annotation to the Event, In SEDA I can startup 10 threads for such 10 C2 requests for pulling the message out of queue make Webservice Call and update the next SEDA Queue and that will make sure that I don39t sequentially wait for a web service response for each of the 10 requests where as in this case my eventprocessor C2 (if) being the single instance would wait sequentially for 10 C2 requests. In Java, creating an array of Java objects does not allocate memory for the objects. It only allocates memory for references to the objects. How does an array of object references help improve CPU caching efficiency because the actual objects are still scattered in the heap You39re absolutely right, which is why in the LMAX case we have an array of byte arrays, not an array of objects - at least for the high performance instances of the disruptor. An array of object references is still valuable in a lot of cases, but as you say it doesn39t necessarily give you the cache line affinity. This has come up several times in the Google Group discussions (groups. google/forum/forum/lmax-disruptor), I think you39ll find more detailed discussion there. I know I am /very/ late, but an array of byte arrays is by definition an array of objects. Bidimensional byte arrays don39t guarantee locality, especially after a GC pass (the single dimensional arrays that make up the bidimensional one are objects in the heap, so they get moved around). Locality inside the unidimensional byte arrays might be preserved, but not in the whole bidimensional array (i. e. it39s preserve intra-array, lost inter-array). I39ve just spend a good part of my day going through your disruptor and I can see from your example the hundreds of millions of ops per second and also from the presentation 6 million trades per second. I39ve just written an example with the producer retrieving the operation request from a webservice with 2 consumers one for marshaling and the other for business logic and my throughput is just over 1000 ops per second source (github/ejosiah/activemq-vs-distruptor) My question does any of your metrics include I/O operations from other consumers such as those for (Journalling, replication, serialization, etc) No, the metrics quoted are for the business logic only, not for I/O etc. I think if you check the Google Group history you39ll find more specific information about what was measured and how, this question has definitely come up before:

No comments:

Post a Comment