Imagine um sistema HFT hipotético em Java, exigindo (muito) baixa latência, com muitos objetos pequenos de curta duração um pouco devido à imutabilidade (Scala), milhares de conexões por segundo e um número obsceno de mensagens que circulam em um evento - arquitetura avançada (akka e amqp). Para os especialistas lá fora, o que (hipoteticamente) seria o melhor ajuste para a JVM 7. Que tipo de código faria feliz. Scala e Akka estarão prontos para este tipo de sistemas. Nota: houve algumas perguntas semelhantes, como esta. Mas ainda tenho que encontrar uma cobertura Scala (que tem sua própria pegada idiossincrática na JVM). Perguntou 30 de março 12 às 23:15 É possível alcançar um desempenho muito bom em Java. No entanto, a questão precisa ser mais específica para fornecer uma resposta credível. Suas principais fontes de latência virão da lista não-exaustiva: a quantidade de lixo que você cria e o trabalho do GC para coletá-la e promovê-la. Projetos imutáveis na minha experiência não se encaixam bem com baixa latência. O ajuste do GC precisa ser um grande foco. Aqueça a JVM para que as classes sejam carregadas e o JIT tenha tido tempo para fazer seu trabalho. Desenhe seus algoritmos para ser O (1) ou pelo menos O (log2 n), e ter testes de desempenho que afirmam isso. Seu design precisa ser livre de bloqueio e seguir o Princípio do Único Writer. Um esforço significativo deve ser levado a entender a pilha inteira e demonstrar simpatia mecânica em seu uso. Elabore seus algoritmos e estruturas de dados para manter o cache amigável. As falhas de cache agora são o maior custo. Isso está intimamente relacionado com a afinidade do processo que, se não for configurado corretamente, pode resultar e poluição significativa do cache. Isso envolverá simpatia pelo sistema operacional e até mesmo algum código JNI em alguns casos. Certifique-se de ter núcleos suficientes para que qualquer thread que precise ser executado tenha um núcleo disponível sem ter que esperar. Recentemente bloguei sobre um estudo de caso de tal exercício. Você pode achar que o uso de um buffer de toque para a passagem de mensagens superará o que pode ser feito com o Akka. A principal implementação do buffer de anel que as pessoas usam na JVM para aplicações financeiras é chamada de Disruptor, que é cuidadosamente sintonizado para eficiência (potência de dois tamanhos), para a JVM (sem GC, sem bloqueios) e para CPUs modernas (sem compartilhamento falso de Linhas de cache). Aqui está uma apresentação de introdução do ponto de vista de Scala scala-phase. orgtalksjamie-allen-sdisruptorindex. html1 e existem links no último slide para o material LMAX original. Respondeu Jul 3 12 às 3:52 Muito interessante Obrigado por compartilhar. Ndash Hugo Sereno Ferreira 10 de julho 12 às 19:20 Sua resposta 2017 Stack Exchange, Inc Meus colegas estão desenvolvendo um sistema comercial que processa um fluxo bastante pesado de transações recebidas. Cada transação abrange um Instrumento (acho uma ligação ou estoque) e tem algumas (agora) propriedades sem importância. Eles estão presos com o Java (lt 8), então vamos ficar com ele: o instrumento será usado mais tarde como uma chave no HashMap. Então, para o futuro, implementamos pró-ativamente o ComparableltInstrumentgt. Este é o nosso domínio, agora os requisitos: as transações entram no sistema e precisam ser processadas (seja o que for que isso signifique), o mais rápido possível. Podemos processá-los gratuitamente em qualquer ordem. No entanto, as transações para o mesmo instrumento precisam ser processadas sequencialmente exatamente na mesma ordem em que elas vieram. A implementação inicial foi direta - coloque todas as transações recebidas em uma fila (por exemplo, ArrayBlockingQueue) com um único consumidor. Isso satisfaz o último requisito, uma vez que a fila preserva o pedido de FIFO rigoroso em todas as transações. Mas essa arquitetura impede o processamento simultâneo de transações não relacionadas para diferentes instrumentos, desperdiçando assim uma melhoria da taxa de transferência. Não surpreendentemente, esta implementação, sem dúvida simples, tornou-se um gargalo. A primeira idéia era dividir as transações recebidas por instrumento e instrumentos de processo individualmente. Nós apresentamos a seguinte estrutura de dados: Yuck Mas o pior ainda está por vir. Como você se certifica de que, no máximo, um segmento processa cada fila de cada vez. Depois de tudo, caso contrário, dois segmentos poderiam pegar itens de uma fila (um instrumento) e processá-los em ordem inversa, o que não é permitido. O caso mais simples é ter um Thread por fila - esta escala não vai, pois esperamos dezenas de milhares de instrumentos diferentes. Então, podemos dizer N threads e deixar cada um deles lidar com um subconjunto de filas, p. Instrument. hashCode () N nos diz qual thread cuida de uma fila dada. Mas ainda não é perfeito por três razões: um segmento deve observar muitas filas, muito provavelmente ocupadas em espera, iterando sobre elas o tempo todo. Alternativamente, a fila pode despertar o seu thread pai de alguma forma. No pior dos casos, todos os instrumentos terão códigos de hash conflitantes, visando apenas um tópico - o que efetivamente é o mesmo que a nossa solução inicial. É apenas um imenso complexo O código bonito não é complexo. É possível implementar esta monstruosidade, Mas difícil e propenso a erros. Além disso, há outro requisito não funcional: os instrumentos vão e vem e há centenas de milhares deles ao longo do tempo. Depois de um tempo, devemos remover entradas no nosso mapa que representam instrumentos que não foram vistos ultimamente. Caso contrário, obtenha um vazamento de memória. Se você conseguir uma solução mais simples, avise-me. Enquanto isso, deixe-me dizer o que eu sugeri aos meus colegas. Como você pode adivinhar, foi Akka - e acabou por ser embaraçosamente simples. Precisamos de dois tipos de atores: Dispatcher e Processor. Dispatcher tem uma instância e recebe todas as transações recebidas. Sua responsabilidade é encontrar ou gerar o ator processador do trabalhador para cada Instrumento e enviar transação para ele: isso é simples. Uma vez que nosso ator Dispatcher é efetivamente unido, nenhuma sincronização é necessária. Nós apenas recebemos Transações. Pesquisa ou crie processador e passe a transação ainda mais. É assim que a implementação do processador pode parecer: Isso é interessante. A nossa implementação Akka é quase idêntica à nossa primeira idéia com o mapa das filas. Afinal, um ator é apenas uma fila e um tópico (lógico) processando itens nessa fila. A diferença é: Akka gerencia pool de threads limitado e compartilha entre talvez centenas de milhares de atores. E porque cada instrumento tem seu próprio ator dedicado (e single-threaded), o processamento seqüencial de transações por instrumento é garantido. Mais uma coisa. Conforme mencionado anteriormente, há uma enorme quantidade de instrumentos e não queremos manter atores para instrumentos que não foram vistos há bastante tempo. Digamos que se um processador não recebeu nenhuma transação dentro de uma hora, ele deve ser interrompido e lixo coletado. Se mais tarde recebemos novas transações para esse instrumento, podemos sempre recriá-lo. Este é bastante complicado - devemos garantir que, se a transação chegar quando o processador decidiu se excluir, não podemos perder essa transação. Em vez de se parar, o processador sinaliza que seu pai estava ocioso por muito tempo. Dispatcher enviará PoisonPill para ele. Como as mensagens ProcessorIdle e Transaction são processadas sequencialmente, não há risco de uma transação ser enviada para um ator não mais existente. Cada ator gerencia seu ciclo de vida de forma independente agendando o tempo limite usando setReceiveTimeout: Claramente, quando o Processador não recebeu nenhuma mensagem por um período de uma hora, ele sintetiza suavemente isso para o pai (Dispatcher). Mas o ator ainda está vivo e pode lidar com as transações se elas acontecem precisamente após uma hora. O que o Dispatcher faz é que ele mata o processador e o remove de um mapa: houve um leve inconveniente. InstrumentProcessors costumava ser um MapltInstrument, ActorRefgt. Isso provou ser insuficiente, já que de repente temos que remover uma entrada neste mapa por valor. Em outras palavras, precisamos encontrar uma chave (Instrumento) que mapeie para um determinado ActorRef (Processor). Existem diferentes maneiras de lidar com isso (por exemplo, o processador ocioso pode enviar um Instrumnt ele lida), mas, em vez disso, usei o BiMapltK, o Vgt da goiaba. Isso funciona porque tanto Instrumentos como AtorRef são apontados como únicos (ator por instrumento). Tendo BiMap, eu poderia simplesmente inverter () o mapa (de BiMapltInstrument, ActorRefgt para BiMapltActorRef, Instrumentgt e tratar ActorRef como chave. Este exemplo de Akka não é mais do que o mundo do mundo. Mas, em comparação com a solução complicada, teríamos que escrever usando filas simultâneas, Bloqueios e piscinas de threads, é perfeito. Meus companheiros de equipe estavam tão entusiasmados que, no final do dia, decidiram reescrever toda a sua aplicação para a Akka. Mais um outro Akka Benchmark eu exerci minhas habilidades Scala e Akka criando um aplicativo de exemplo no Domínio comercial. Tenho experiência com o desenvolvimento de sistemas de negociação, por isso, mesmo que o exemplo seja simplificado, ele está enraizado na arquitetura do mundo real. Achei interessante comparar o desempenho de diferentes soluções técnicas com esse aplicativo de exemplo. Portanto, várias implementações concretas da negociação O sistema é implementado e comparado entre si. Invocações de método síncrono ordinário. Atores de Scala Atores de Akka Eu posso te dizer direito Que a performance da Akka Actors é excelente em comparação com os atores Scala. Antes de olhar para o benchmark, descreverei brevemente o aplicativo de exemplo. Um sistema de negociação é essencialmente sobre como combinar pedidos de compra e venda. Um pedido de limite é uma ordem para comprar uma garantia em não mais, ou vender a não menos, do que um preço específico. Por exemplo, se um investidor quiser comprar um estoque, mas não quer pagar mais de 20 por isso, o investidor pode fazer uma ordem limite para comprar o estoque em 20 8220 ou melhor8221. Existem muitos outros tipos de pedidos e restrições especiais. A amostra apenas está gerenciando ordens de limite simples. As encomendas que estão longe do melhor preço atual no mercado são coletadas em um livro de pedidos para a segurança, para posterior execução. Um motor correspondente gerencia um ou mais cadernos de encomendas, ou seja, o mercado é destruído por um livro de pedidos. Os mecanismos de correspondência mantêm o estado atual dos livros de encomendas. Os clientes se conectam a um serviço de recebedor de pedidos, que é responsável por rotear a ordem para o mecanismo de correspondência correto. O destinatário da ordem é sem estado e os clientes podem usar qualquer receptor de pedidos independente do livro de encomendas. Para redundância, os motores correspondentes funcionam em pares. Cada ordem é processada por ambos os motores correspondentes. O pedido também é armazenado em um registro de transações persistente, por ambos os mecanismos correspondentes. Em uma configuração real, os mecanismos de correspondência primária e de espera geralmente são implantados em centros de dados separados. Agora, até o ponto de referência. O cenário de teste colocou encomendas de compra e venda em 15 livros de pedidos, divididos em 3 motores correspondentes. As ordens estão em diferentes níveis de preços, portanto, uma profundidade de livro de pedidos é construída, mas no final todas as encomendas são negociadas e isso é verificado pelo teste JUnit executando o benchmark. O cenário foi executado em uma carga diferente, variando o número de clientes simulados de 1 a 40. Os resultados de referência aqui ilustrados foram realizados em uma caixa real de 8 núcleos (máquina Xeon 5500 dupla, 2,26 Ghz por núcleo). Aqui estão o resultado do processamento de pedidos 750000 em cada nível de carga. A solução básica usa invocações de métodos síncronos comuns. É extremamente rápido, mas não é uma opção para uma solução escalável verdadeira. A passagem de mensagens assíncronas é uma alternativa melhor para escalar em nós múltiplos ou múltiplos. Nas soluções Scala e Akka Actors, os clientes enviam cada mensagem de ordem a um destinatário e aguardam a resposta Future (operador em Scala e. In Akka). O receptor de pedidos encaminha a solicitação ao motor correspondente responsável pelo caderno de pedidos, ou seja, o processador de pedidos do host pode ser usado imediatamente para o próximo pedido. O mecanismo de correspondência envia a mensagem da ordem para o modo de espera e ambos os mecanismos correspondentes processam a lógica correspondente e o log de transações em paralelo. Confirmação é respondida ao cliente quando ambos estão concluídos. Os resultados de referência mostram que os Akka Actors são capazes de processar três vezes mais pedidos em comparação com os Atores Scala na mesma carga. Resultado semelhante com latência. A latência dos Akka Actors é um terço dos atores Scala. Isto é válido para baixa carga também. A latência média nem sempre é a melhor medida, então deixe-nos olhar alguns percentis. As operações que aguardam a conclusão de um Futuro foram usadas ao enviar mensagens. Isso tem uma etiqueta de preço de escalabilidade, já que o segmento está bloqueado enquanto espera que o Futuro seja concluído. Uma melhor escalabilidade pode ser alcançada com a passagem de mensagens unidirecional, que é ilustrada pelas soluções unidirecionais do ScalaAkka Actor. Ele usa a operação bang () para enviar todas as mensagens. Os mecanismos de correspondência escrevem cada ordem para um arquivo de log de transações. Este é um bloqueio de bloqueio de EI. Para empurrar o teste da mensagem passando um passo adiante, o benchmark também foi executado sem o log de transações. A solução Akka brilha ainda mais. Mais de três vezes maior taxa de transação em comparação com Scala Atores na mesma carga, ao usar a solução com o envio de mensagens e aguardando a resposta. Para as soluções de passagem de mensagens unidireccionais, os atores Akka são duas vezes mais rápidos do que os atores Scala. A Akka tem uma grande flexibilidade quando se trata de especificações de diferentes mecanismos de despacho. O Akka Actor hawt está incluído no benchmark como uma comparação com a solução unidirecional Akka Actor. Ele usa a biblioteca Threading HawtDispatch, que é um clone Java de libdispatch. O último teste sem registro de transações mostra que o HawtDispatcher possui um desempenho ligeiramente melhor do que o despachante baseado em eventos que foi usado para Akka Actor de um jeito. O código-fonte completo para o aplicativo de exemplo está localizado em: githubpatriknwakka-sample-trading Para executar o benchmark você mesmo, você pode baixar e descompactar a distribuição, contendo todos os arquivos jar necessários. O README incluído descreve como executar os testes. Nota de atualização 15 de agosto: Adicionado a solução One-Way de Scala Actor e uma nova descrição de como executar o benchmark. Nota de atualização 22 de agosto: Novo benchmark executado na caixa real de 8 núcleos.
No comments:
Post a Comment