A equipe da LMAX tem uma apresentação sobre como conseguiu realizar 100k TPS a menos de 1 ms de latência . Eles fizeram backup dessa apresentação com um blog , um documento técnico (PDF) e o próprio código-fonte .
Recentemente, Martin Fowler publicou um excelente artigo sobre a arquitetura LMAX e menciona que agora eles são capazes de lidar com seis milhões de pedidos por segundo e destaca algumas das etapas que a equipe tomou para subir outra ordem de magnitude no desempenho.
Até agora, expliquei que a chave para a velocidade do Business Logic Processor é fazer tudo sequencialmente, na memória. Só fazer isso (e nada realmente estúpido) permite que os desenvolvedores escrevam código que possa processar 10K TPS.
Eles descobriram que a concentração nos elementos simples do bom código poderia trazer isso para a faixa de 100K TPS. Isso só precisa de código bem fatorado e métodos pequenos - essencialmente, isso permite que o Hotspot faça um trabalho melhor de otimização e que as CPUs sejam mais eficientes no cache do código durante a execução.
Levou um pouco mais de esperteza para subir outra ordem de magnitude. Há várias coisas que a equipe do LMAX achou úteis para chegar lá. Uma era escrever implementações personalizadas das coleções Java que foram projetadas para serem amigáveis ao cache e cuidadosas com o lixo.
Outra técnica para atingir esse nível superior de desempenho é dar atenção aos testes de desempenho. Há muito tempo percebo que as pessoas falam muito sobre técnicas para melhorar o desempenho, mas a única coisa que realmente faz a diferença é testá-lo
Fowler mencionou que existem várias coisas que foram encontradas, mas ele apenas mencionou algumas.
Existem outras arquiteturas, bibliotecas, técnicas ou "coisas" que são úteis para atingir esses níveis de desempenho?
fonte
Respostas:
Existem todos os tipos de técnicas para processamento de transações de alto desempenho, e a do artigo de Fowler é apenas uma das muitas no limite. Em vez de listar várias técnicas que podem ou não ser aplicáveis à situação de qualquer pessoa, acho melhor discutir os princípios básicos e como o LMAX aborda um grande número deles.
Para um sistema de processamento de transações de alta escala, você deseja fazer o seguinte, tanto quanto possível:
Minimize o tempo gasto nas camadas de armazenamento mais lentas. Do mais rápido ao mais lento em um servidor moderno, você tem: CPU / L1 -> L2 -> L3 -> RAM -> Disco / LAN -> WAN. O salto do disco magnético moderno mais rápido para a RAM mais lenta ultrapassa 1000x para acesso seqüencial ; acesso aleatório é ainda pior.
Minimize ou elimine o tempo gasto em espera . Isso significa compartilhar o menor estado possível e, se o estado precisar ser compartilhado, evitar bloqueios explícitos sempre que possível.
Espalhe a carga de trabalho. CPUs não tenha chegado muito mais rápido nos últimos anos, mas eles têm -se menor, e 8 núcleos é bastante comum em um servidor. Além disso, você pode até espalhar o trabalho por várias máquinas, que é a abordagem do Google; O melhor disso é que ele escala tudo, incluindo E / S.
Segundo Fowler, o LMAX adota a seguinte abordagem para cada um deles:
Mantenha todo o estado na memória o tempo todo . A maioria dos mecanismos de banco de dados fará isso de qualquer maneira, se o banco de dados inteiro puder caber na memória, mas eles não querem deixar nada ao acaso, o que é compreensível em uma plataforma de negociação em tempo real. Para fazer isso sem aumentar muito o risco, eles tiveram que criar um monte de infraestrutura leve de backup e failover.
Use uma fila sem bloqueio ("disruptor") para o fluxo de eventos de entrada. O contraste com as filas de mensagens duráveis tradicionais que definitivamente não são bloqueadas e, de fato, geralmente envolvem transações distribuídas muito lentas .
Não muito. O LMAX joga este embaixo do barramento com base em que as cargas de trabalho são interdependentes; o resultado de um altera os parâmetros para os outros. Esta é uma advertência crítica e que Fowler explicitamente chama. Eles fazem algum uso de concorrência, a fim de fornecer recursos de failover, mas toda a lógica de negócio é processado em um único segmento .
LMAX não é a única abordagem para OLTP de alta escala. E, embora seja bastante brilhante por si só, você não precisa usar técnicas avançadas para obter esse nível de desempenho.
De todos os princípios acima, o número 3 é provavelmente o mais importante e o mais eficaz, porque, francamente, o hardware é barato. Se você pode particionar adequadamente a carga de trabalho em meia dúzia de núcleos e várias dúzias de máquinas, então o céu é o limite para as técnicas convencionais de computação paralela . Você ficaria surpreso com a quantidade de produtividade que pode obter com nada além de um monte de filas de mensagens e um distribuidor de rodízio. Obviamente, não é tão eficiente quanto o LMAX - na verdade, nem próximo -, mas a taxa de transferência, a latência e a relação custo-benefício são preocupações separadas, e aqui estamos falando especificamente sobre a taxa de transferência.
Se você tem o mesmo tipo de necessidades especiais que o LMAX possui - em particular, um estado compartilhado que corresponde à realidade dos negócios, em oposição a uma opção de design apressada -, sugiro experimentar o componente deles, porque não vi muito caso contrário, adequado a esses requisitos. Mas se estamos simplesmente falando sobre alta escalabilidade, recomendamos que você faça mais pesquisas em sistemas distribuídos, porque elas são a abordagem canônica usada pela maioria das organizações atualmente (Hadoop e projetos relacionados, ESB e arquiteturas relacionadas, CQRS que Fowler também menções e assim por diante).
Os SSDs também se tornarão um divisor de águas; sem dúvida, eles já são. Agora você pode ter armazenamento permanente com tempos de acesso semelhantes à RAM e, embora os SSDs de nível de servidor ainda sejam terrivelmente caros, eles eventualmente cairão de preço assim que as taxas de adoção aumentarem. Ele foi pesquisado extensivamente e os resultados são surpreendentes e só melhorarão com o tempo, portanto todo o conceito "manter tudo na memória" é muito menos importante do que costumava ser. Então, mais uma vez, tentaria me concentrar na simultaneidade sempre que possível.
fonte
Eu acho que a maior lição a aprender com isso é que você precisa começar com o básico:
Durante o teste de desempenho, você cria um perfil do seu código, encontra os gargalos e os corrige um por um.
Muitas pessoas pulam direto para a parte "conserte-as uma a uma". Eles gastam muito tempo escrevendo "implementações personalizadas das coleções java", porque eles sabem que todo o motivo do seu sistema ficar lento é devido a falhas no cache. Isso pode ser um fator que contribui, mas se você passar direto para ajustar códigos de baixo nível como esse, provavelmente perderá o problema maior de usar um ArrayList quando deveria usar um LinkedList ou o motivo real do seu sistema. lento é porque seu ORM está carregando filhos preguiçosamente de uma entidade e, assim, fazendo 400 viagens separadas ao banco de dados para cada solicitação.
fonte
Não comentarei particularmente o código LMAX porque acho que é amplamente descrito, mas aqui estão alguns exemplos de coisas que fiz que resultaram em melhorias significativas no desempenho mensurável.
Como sempre, essas são técnicas que devem ser aplicadas depois que você souber que tem um problema e precisa melhorar o desempenho - caso contrário, é provável que você esteja apenas otimizando prematuramente.
Ajude o compilador JIT com finalização de campos, métodos e classes final permite otimizações específicas que realmente ajudam o compilador JIT. Exemplos específicos:
Substituir classes de coleção por matrizes - isso resulta em código menos legível e é mais difícil de manter, mas é quase sempre mais rápido, pois remove uma camada de indireção e se beneficia de muitas otimizações de acesso à matriz. Normalmente, é uma boa ideia no código sensível ao desempenho / loops internos depois de identificá-lo como um gargalo, mas evite o contrário por questões de legibilidade!
Use primitivas sempre que possível - as primitivas são fundamentalmente mais rápidas que seus equivalentes baseados em objetos. Em particular, o boxe adiciona uma enorme quantidade de sobrecarga e pode causar pausas desagradáveis no GC. Não permita que nenhum primitivo seja encaixotado se você se preocupa com desempenho / latência.
Minimize o bloqueio de baixo nível - os bloqueios são muito caros em um nível baixo. Encontre maneiras de evitar o bloqueio completo ou em um nível de granularidade grossa, para que você precise bloquear com pouca frequência sobre grandes blocos de dados e o código de baixo nível possa prosseguir sem ter que se preocupar com problemas de bloqueio ou simultaneidade.
fonte
final
alguns JITs, isso pode acontecer, outros não. Depende da implementação (como muitas dicas de ajuste de desempenho). Concorde com as alocações - você precisa fazer um benchmark disso. Normalmente, achei melhor eliminar alocações, mas YMMV.Além do que já foi dito em uma excelente resposta do Aaronaught , gostaria de observar que um código como esse pode ser bastante difícil de desenvolver, entender e depurar. "Embora seja muito eficiente ... é muito fácil estragar tudo", como um dos membros mencionados no blog LMAX .
Diante do exposto, acho que aqueles que escolhem o Disruptor e abordagens semelhantes melhor se certificam de que possuem recursos de desenvolvimento suficientes para manter sua solução.
No geral, a abordagem do Disruptor parece bastante promissora para mim. Mesmo que sua empresa não possa utilizá-lo, por exemplo, pelas razões mencionadas acima, considere convencer sua gerência a "investir" algum esforço em estudá-lo (e na SEDA em geral) - porque se não o fizerem, existe a chance de um dia seus clientes os deixarão em favor de uma solução mais competitiva que exige 4x, 8x, etc, menos servidores.
fonte