Como melhorar significativamente o desempenho do Java?

23

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?

Dakotah North
fonte
11
"Que outras arquiteturas, bibliotecas, técnicas ou" coisas "são úteis para atingir esses níveis de desempenho?" Porque perguntar? Essa citação é a lista definitiva. Existem muitas outras coisas, nenhuma das quais tem o tipo de impacto dos itens dessa lista. Qualquer outra coisa que alguém possa nomear não será tão útil quanto essa lista. Por que pedir más idéias quando você citou uma das melhores listas de otimização já produzidas?
31511 S.Lott
Seria bom saber quais ferramentas eles usaram para ver como o código gerado era executado no sistema.
1
Eu ouvi falar de pessoas que juram por todo tipo de técnica. O que eu achei mais eficaz é a criação de perfil no nível do sistema. Pode mostrar gargalos nas maneiras como seu programa e sua carga de trabalho estão exercitando o sistema. Eu sugeriria aderir a diretrizes bem conhecidas sobre desempenho e escrever código modular para que você possa ajustá-lo facilmente mais tarde ... Acho que você não pode dar errado com a criação de perfil do sistema.
Ritesh

Respostas:

21

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:

  1. 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.

  2. 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.

  3. 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:

  1. 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.

  2. 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 .

  3. 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.

Aaronaught
fonte
Discutir os princípios é fundamental, é excelente e seu comentário é excelente e ... a menos que o artigo de Fowler não tenha tido uma referência em uma nota de rodapé para armazenar em cache algoritmos alheios en.wikipedia.org/wiki/Cache-oblivious_algorithm (que se encaixa perfeitamente no categoria número 1 que você tem acima) Eu nunca os encontraria. Então ... com relação a cada categoria que você tem acima, você sabe das 3 principais coisas que uma pessoa deve saber?
Dakotah North
@ Dakotah: eu nem começaria a me preocupar com a localização do cache, a menos e até que eu tivesse eliminado completamente a E / S do disco, que é onde a grande maioria do tempo é gasta esperando na grande maioria dos aplicativos. Além disso, o que você quer dizer com "três principais coisas que uma pessoa deve saber"? Top 3 o que, para saber sobre o que?
Aaronaught 29/07
O salto da latência de acesso à RAM (~ 10 ^ -9s) para a latência do disco magnético (caso médio de ~ 10 ^ -3s) é outra ordem de magnitude maior que 1000x. Até os SSDs ainda têm tempos de acesso medidos em centenas de microssegundos.
Sedate Estrangeiro
@Sedate: Latência, sim, mas isso é mais uma questão de taxa de transferência do que de latência bruta, e quando você obtém tempos de acesso passados ​​e velocidade total de transferência, os discos não são tão ruins. Por isso fiz a distinção entre acesso aleatório e seqüencial; para cenários de acesso aleatório que se tornou essencialmente um problema de latência.
Aaronaught
@Aaronaught: Após a releitura, suponho que você esteja correto. Talvez seja necessário enfatizar que todo acesso a dados deve ser o mais seqüencial possível; benefícios significativos também podem ser obtidos ao acessar dados em ordem a partir da RAM.
Sedate Estrangeiro
10

Eu acho que a maior lição a aprender com isso é que você precisa começar com o básico:

  • Bons algoritmos, estruturas de dados apropriadas e não fazer nada "realmente estúpido"
  • Código bem fatorado
  • Teste de performance

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.

Adam Jaskiewicz
fonte
7

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.

  • Use a estrutura de dados correta e crie uma personalizada, se necessário - o design correto da estrutura de dados diminui a melhoria que você obterá das micro-otimizações, faça isso primeiro. Se o seu algoritmo depende do desempenho de várias leituras rápidas de acesso aleatório O (1), verifique se você possui uma estrutura de dados que suporte isso! Vale a pena percorrer alguns obstáculos para fazer isso direito, por exemplo, encontrar uma maneira de representar seus dados em uma matriz para explorar leituras indexadas O (1) muito rápidas.
  • A CPU é mais rápida que o acesso à memória - você pode fazer bastante cálculo no tempo necessário para fazer uma memória aleatória ser lida se a memória não estiver no cache L1 / L2. Geralmente, vale a pena fazer um cálculo se você economizar uma leitura de memória.
  • 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:

    • O compilador pode assumir que uma classe final não possui subclasses, portanto, pode transformar chamadas de método virtual em chamadas de método estático
    • O compilador pode tratar os campos finais estáticos como uma constante para uma boa melhoria de desempenho, especialmente se a constante for usada em cálculos que podem ser calculados em tempo de compilação.
    • Se um campo que contém um objeto Java for inicializado como final, o otimizador poderá eliminar a verificação nula e o envio do método virtual. Agradável.
  • 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.

  • Evite alocar memória - isso pode reduzir a velocidade geral, pois a coleta de lixo da JVM é incrivelmente eficiente, mas é muito útil se você estiver tentando obter uma latência extremamente baixa e precisar minimizar as pausas do GC. Existem estruturas de dados especiais que você pode usar para evitar alocações - a biblioteca http://javolution.org/ em particular é excelente e notável por elas.
Mikera
fonte
Não concordo em tornar os métodos finais . O JIT é capaz de descobrir que um método nunca é substituído. Além disso, caso uma subclasse seja carregada mais tarde, ela poderá desfazer a otimização. Observe também que "evitar alocar memória" também pode dificultar o trabalho do GC e, assim, atrasá-lo - portanto, use com cuidado.
Maaartinus
@maaartinus: em relação a finalalguns 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.
Mikera # 6/13
4

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 .

  • Para um desenvolvedor acostumado a consultas e bloqueios tradicionais , a codificação de uma nova abordagem pode parecer como andar a cavalo. Pelo menos essa foi a minha própria experiência ao experimentar a Phaser, conceito mencionado no artigo técnico da LMAX. Nesse sentido, eu diria que essa abordagem comercializa a contenção de bloqueio pela contenção do cérebro do desenvolvedor .

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.

mosquito
fonte