Java não é apenas uma empresa sangrenta, mas também aplicativos rápidos e sensíveis à latência

Faço negociações algorítmicas no Raiffeisenbank. Esta é uma área bastante específica do setor bancário. Criamos uma plataforma de negociação que funciona com atrasos baixos e previsíveis. O sucesso do aplicativo depende, inter alia, da velocidade do aplicativo, por isso temos que lidar com toda a pilha envolvida na negociação: canais de rede privada, hardware especial, configurações do SO e uma JVM especial e, é claro, o próprio aplicativo. Não podemos parar de otimizar exclusivamente o próprio aplicativo - as configurações do sistema operacional ou da rede são igualmente importantes. Isso requer conhecimento técnico e erudição para entender como os dados fluem por toda a pilha e onde pode haver um atraso.


Nem toda organização / banco pode permitir o desenvolvimento dessa classe de software. Mas tive sorte de que esse projeto tenha sido lançado dentro dos muros do Raiffeisenbank e tive uma especialização adequada - me especializei em desempenho de código no Intel Moscow Compiler Laboratory. Criamos compiladores para C, C ++ e Fortran. No Raiffeisenbank, mudei para Java. Se antes criei algum tipo de ferramenta que muitas pessoas usavam na época, agora mudei para o outro lado das barricadas e estou envolvido na análise aplicada do desempenho não apenas do código, mas de toda a pilha de aplicativos. Regularmente, a maneira de pesquisar um problema de desempenho está muito além do código, por exemplo, nas configurações de kernel ou rede.

Java não é para alta carga?


Acredita-se que o Java não seja muito adequado para o desenvolvimento de sistemas altamente carregados.
Isso só pode ser parcialmente acordado. Java é bonito em muitos aspectos. Se você compará-lo com uma linguagem como C ++, sua sobrecarga potencial pode ser maior, mas às vezes soluções funcionalmente semelhantes em C ++ podem funcionar mais lentamente. Existem otimizações que funcionam automaticamente em Java, mas não em C ++ e vice-versa. Analisando a qualidade do código que vem após o compilador Java JIT, quero acreditar que o desempenho será inferior ao que eu poderia alcançar no pico, não mais do que várias vezes. Mas, ao mesmo tempo, recebo um desenvolvimento muito rápido, excelentes ferramentas e uma ampla seleção de componentes prontos para uso.

Vamos ser sinceros: no mundo C ++, os ambientes de desenvolvimento (IDEs) estão significativamente atrás do IntelliJ e Eclipse. Se um desenvolvedor usa qualquer um desses ambientes, a velocidade de depuração, localização de bugs e escrita de lógica complexa é uma ordem de magnitude maior. Como resultado, é mais fácil arquivar Java nos lugares certos, para que funcione rápido o suficiente para fazer tudo do zero e por muito tempo em C ++. O mais engraçado é que, ao escrever código competitivo, as abordagens para sincronização em Java e C ++ são muito semelhantes: elas são primitivas no nível do SO (por exemplo, sincronizadas / std :: mutex ) ou primitivas de ferro ( Atomic * / std :: atomic <*> ) . E é muito importante ver essa semelhança.

Em geral, estamos desenvolvendo um aplicativo sem carga alta))

Qual é a diferença entre aplicativos de alta carga e baixa latência?


O termo carga alta não reflete totalmente as especificidades do nosso trabalho - estamos envolvidos em sistemas sensíveis à latência . Qual a diferença? Para sistemas altamente carregados, é importante trabalhar em média com bastante rapidez, fazendo pleno uso dos recursos de hardware. Na prática, isso significa que cada centésimo / milésimo / .. / milionésimo pedido ao sistema pode funcionar potencialmente muito devagar, porque nos concentramos em valores médios e nem sempre levamos em conta que nossos usuários sofrem significativamente com os freios.

Estamos envolvidos em sistemas para os quais o nível de atraso é crítico. Nossa tarefa é garantir que o sistema sempre tenha uma resposta previsível. Os sistemas potencialmente sensíveis à latência podem não ser muito carregados se os eventos ocorrerem raramente, mas é necessário um tempo de resposta garantido. E isso não facilita seu desenvolvimento. Muito pelo contrário! Perigos estão à espera em todos os lugares. A esmagadora maioria dos componentes de hardware e software modernos é orientada para um bom trabalho "em média", ou seja, para taxa de transferência .

Pegue pelo menos estruturas de dados. Usamos tabelas de hash e, se um re-hash de toda a estrutura de dados ocorrer em um caminho crítico, isso poderá levar a freios tangíveis para um usuário específico em uma única solicitação. Ou compilador JIT - otimiza o padrão de código executado com mais freqüência, pessimizando o padrão de código raramente executado. Mas a velocidade desse caso raro em particular pode ser muito importante para nós!

Talvez esse código processe um tipo raro de pedidos? Ou alguma situação de mercado incomum que precisa de uma resposta rápida? Estamos tentando garantir que a reação do nosso sistema a esses eventos potencialmente raros leve algum tempo previsível e, de preferência, muito curto.

Como obter um tempo de reação previsível?


Esta pergunta não pode ser respondida em duas frases. Em uma primeira aproximação, é importante entender se existe algum tipo de sincronização - sincronizada, reentrada novamente ou algo do java.util.concurrent . Muitas vezes, você precisa usar a sincronização no busy-spin'ah. O uso de qualquer primitiva de sincronização é sempre um compromisso. E é importante entender como essas primitivas de sincronização funcionam e quais compensações elas carregam. Também é importante avaliar quanto lixo um determinado código gera. A melhor maneira de combater o coletor de lixo é não acioná-lo. Quanto menos gerarmos lixo, menos executaremos um coletor de lixo e mais tempo o sistema funcionará sem a sua intervenção.

Também usamos uma ampla gama de ferramentas diferentes que nos permitem analisar não apenas indicadores médios. Temos que analisar com muito cuidado a velocidade com que o sistema funciona a cada centésimo, a cada milésima vez. Obviamente, esses indicadores serão piores que a mediana ou média. Mas é muito importante saber quanto. E ferramentas como Grafana , Prometheus , histogramas HDR e JMH ajudam a mostrar isso.

Posso remover inseguro?


Muitas vezes, você precisa usar o que os apologistas chamam de API não documentada. Eu estou falando sobre o famoso inseguro. Eu acredito que o inseguro se tornou uma parte de fato da API pública das máquinas Java. Não faz sentido negar. Inseguro usa muitos projetos que todos usamos ativamente. E se recusarmos, o que acontecerá com esses projetos? Ou eles viverão na versão antiga do Java, ou novamente terão que gastar muita energia para reescrever tudo isso. A comunidade está pronta para isso? Está pronto para potencialmente perder dezenas de por cento da produtividade? E o mais importante, em troca de quê?

Indiretamente, minha opinião confirma uma remoção muito clara dos métodos do Unsafe - no Java11, os métodos mais inúteis do Unsafe foram removidos. Eu acho que até pelo menos metade de todos os projetos que usam o Unsafe mudarem para outro, o Unsafe estará disponível de uma forma ou de outra.

Existe uma opinião: Banco + Java = empresa ossificada sangrenta?


Nossa equipe não tem esses horrores. Na primavera, provavelmente escrevemos dez linhas e, por mim)) Tentamos não usar grandes tecnologias. Preferimos fazer pequenos, limpos e rápidos, para que possamos realizá-los, controlá-los e, se necessário, modificá-los. O último é muito importante para sistemas (como o nosso), que estão sujeitos a requisitos não padronizados, que podem diferir dos requisitos de 90% dos usuários da estrutura. E, no caso de usar uma estrutura ampla, não poderemos transmitir nossas necessidades à comunidade ou corrigir o comportamento de forma independente.

Na minha opinião, os desenvolvedores sempre devem poder usar todas as ferramentas disponíveis. Eu vim para o mundo Java a partir do C ++ e estou muito impressionado com a divisão da comunidade entre aqueles que desenvolvem o tempo de execução da máquina virtual / compilador ou a própria máquina virtual e os desenvolvedores de aplicativos. Isso é claramente visto no código de classe JDK padrão. Geralmente, os autores do JDK usam uma API diferente. Potencialmente, isso significa que não podemos alcançar o desempenho máximo. Em geral, acredito que o uso da mesma API para escrever a biblioteca padrão e o código do aplicativo é um excelente indicador da maturidade da plataforma.

Mais uma coisa


Eu acho que é muito importante que todos os desenvolvedores saibam como, senão toda a pilha, pelo menos metade dela funciona: código Java, código de bytes, componentes internos do tempo de execução e montador da máquina virtual, hardware, sistema operacional, rede. Isso permite uma visão mais ampla dos problemas.
Vale a pena mencionar também o desempenho. É muito importante não se concentrar na média e sempre observar os percentis médio e alto (o pior das medições 10/100/1000 / ...).

Vou falar sobre tudo isso em uma reunião do Java User Group em 30 de maio em São Petersburgo. Encontro com Sergei Melnikov, sou apenas eu)) Você pode se registrar aqui .

Sobre o que vamos falar?

  1. Sobre criação de perfil e uso do Linux e perf profiler padrão: como conviver com eles, o que eles medem e como interpretar seus resultados. Esta será uma introdução ao perfil geral, com dicas, truques de vida, como extrair todo o possível dos criadores de perfil para que eles criem perfil com a máxima precisão e frequência.
  2. Sobre os recursos do equipamento para obter um perfil ainda mais detalhado e visualizar o perfil de eventos raros. Por exemplo, quando seu código é executado 10 vezes mais lento a cada centésima vez. Nenhum profiler dirá sobre isso. Escreveremos nosso pequeno criador de perfil usando o mecanismo padrão do kernel Linux e tentaremos ver o perfil de algum evento raro.

Venha para a reunião, será uma ótima noite, haverá muitas histórias interessantes sobre nossa plataforma e sobre nosso idioma favorito.

Até mais;)

Source: https://habr.com/ru/post/pt453688/


All Articles