Misturando OpenJDK e NodeJS: interações entre idiomas e arquitetura vertical

Olá Habr!

Durante muito tempo, tive a ideia de discutir o GraalVM com você, adiada até finalmente encontrar o artigo de hoje, cujo assunto vai muito além do escopo da análise de uma máquina virtual específica. O autor Mike Hearn descreve todo o paradigma de interação entre idiomas e programação multilíngue (programação poliglota). A seguir, é apresentado o famoso exemplo de escala vertical e um artigo muito longo sob o corte.



Este artigo é sobre uma maneira inovadora de escrever software que pode se tornar popular no futuro, mas provavelmente não agora. O artigo tem um código, honestamente!

Nos tempos antigos, ou seja, em 2015, escrevi por que o Kotlin seria minha próxima linguagem de programação e em 2016 escrevi sobre Graal e Truffle : dois projetos radicais de pesquisa relacionados a compiladores que não apenas aceleram significativamente o trabalho de linguagens como Ruby , mas também incorporam, na realidade, interações interlíngua perfeitas. Nesses projetos, o compilador dinâmico (JIT) ou OpenJDK é substituído por um novo que tem a capacidade de transformar intérpretes anotados em compiladores JIT de ponta ... automaticamente.

Voltando a esses tópicos em 2019, gostaria de mostrar três coisas:

  1. Como usar a pequena biblioteca que escrevi para usar quase perfeitamente módulos NPM a partir do código de programas escritos em Java ou Kotlin.
  2. Explique todas as boas razões pelas quais você pode precisar, mesmo se você acha que JavaScript / Java é a pior coisa do mundo, sem contar o óleo de peixe.
  3. Explore brevemente o conceito de arquitetura vertical que compete com o design orientado a microsserviços. Está localizado na interseção das versões mais recentes do GraalVM e do OpenJDK e requer o hardware mais avançado.

Usando NPMs de Java e Kotlin


Tomaremos apenas três etapas simples:

  1. Tome GraalVM . Este é um conjunto de correções, construído sobre o OpenJDK, que apareceu bem a tempo: ele pode executar todo o código de bytes da JVM que você possui.
  2. Pegamos meu kit de ferramentas NodeJVM do github e o adicionamos ao nosso caminho.
  3. Substitua java na linha de comandos por nodejvm . Isso é tudo!

Ok, ok. Eu admito, aqui eu desenho um pouco e exagero, até o final do artigo você terá que suportar esse estilo. Claro, nem tudo é tão simples: você ainda precisa pegar o módulo e usá-lo.

Considere como se parece:



Código de amostra usando o NodeJVM

Dê uma olhada nesta foto. Sim, é exatamente isso que parece: Kotlin com uma sequência de várias linhas incorporada na qual ocorre o preenchimento automático de JavaScript, após o qual a análise estática de JavaScript é realizada e a sintaxe é destacada corretamente. As mesmas operações funcionam com Java ou outras linguagens para a JVM que o IntelliJ entende. Para obter essas oportunidades, é necessário clicar no comutador nas configurações do IDE (leia o leia-me do NodeJVM para saber como fazer isso), mas mais tarde essa função funcionará automaticamente. Se o IntelliJ conseguir descobrir, analisando o fluxo de dados que sua sequência deve eventualmente ser passada para os métodos run ou eval , ela será tratada como JS incorporado.

Aqui vou falar sobre a API do Kotlin, já que é um pouco mais bonita e mais conveniente do que a API em Java normal, mas tudo o que descrevo abaixo também pode ser feito a partir de Java.

No código acima, preste atenção a vários dos seguintes recursos:

  • Para acessar o JavaScript, você deve usar o bloco nodejs {} . O fato é que o JavaScript é de thread único e, portanto, para executar os módulos do NPM, é necessário "inserir o fluxo do nó". O bloco nodejs {} executa essa sincronização para nós, independentemente do segmento em que estamos. Portanto, você precisa se lembrar constantemente: para executar qualquer código JS, em princípio, você precisa estar dentro desse bloco. Você pode inseri-lo novamente quantas vezes quiser, para que seja seguro usar esse bloco em qualquer lugar onde for necessário. Quaisquer retornos de chamada JavaScript serão executados no encadeamento Node e, portanto, todos os outros encadeamentos terão acesso negado ao bloco nodejs ; portanto, se você estiver preocupado com o desempenho ou com a renderização da GUI, evite executar operações de longa execução nos retornos de chamada.
  • A sintaxe var x by bind(SomeObject()) está disponível apenas no bloco nodejs e permite conectar-se à mesma variável no escopo global do JavaScript. Quando x muda do Kotlin, ele muda no JS e vice-versa. Aqui estou anexando um objeto Java File regular ao mundo JS.
  • O método eval retorna ... o que pedimos para retornar, no entanto, na forma de digitação estática. Essa é uma função genérica e, simplesmente especificando o tipo de entidade que atribuímos a ela, garantiremos que o eval transmita automaticamente o objeto JavaScript para uma classe tipicamente estatizada ou para uma interface Java / Kotlin / Scala / etc. Embora isso não tenha sido explicitamente declarado acima, MemoryUsage é um tipo de interface simples que eu defini e possui as funções rss() e heapTotal() . Eles são mapeados para as propriedades JavaScript com o mesmo nome, aplicando-as ao que você obtém da API do process.memoryUsage() . A maioria dos tipos JS pode, portanto, ser convertida em tipos Java "normais"; documentação detalhada de como funciona está disponível no site do GraalVM. Os objetos resultantes podem ser armazenados em qualquer lugar, mas o método que os chama, é claro, deve ser feito no bloco nodejs .
  • Os objetos JavaScript também podem ser considerados mapeamentos simples de uma string para um objeto, o que de muitas maneiras realmente corresponde à sua natureza. Por sua vez, esses mapeamentos de uma string para um objeto podem ser trazidos de volta para algo mais forte do tipo, que pode ser visto claramente no retorno de chamada. Use a apresentação que você preferir.
  • Você pode usar o require e ele procurará por módulos nos diretórios node_modules da maneira usual.

O trecho de código acima usa o protocolo DAT , que permite conectar-se a uma rede ponto a ponto remotamente semelhante ao BitTorrent e, em seguida, procurar pares que tenham o arquivo desejado. Uso o DAT como exemplo, uma vez que é (a) descentralizado e, portanto, exclusivamente chique e (b) para o bem ou para o mal, sua implementação de referência é escrita em JavaScript. Este não é um programa que eu poderia escrever completamente sem usar JS em um tempo razoável.

Isso também pode ser feito no Java:

 import net.plan99.nodejs.NodeJS; public class Demo { public static void main(String[] args) { int result = NodeJS.runJS(() -> NodeJS.eval("return 2 + 3 + 4").asInt() ); System.out.println(result); } } 

A API Java não fornece uma ligação variável agradável e conversão automática como a API Kotlin, mas é bastante fácil de usar. Aqui, mostramos como convertemos o resultado em um tipo inteiro Java ( int ) e o retornamos “do” fluxo Node: nesse caso, o fluxo Java principal não é o mesmo que o fluxo NodeJS, mas alternamos esses fluxos completamente sem problemas.

O NodeJVM é um invólucro muito, muito pequeno, sobre o GraalVM . Ele adiciona uma quantidade insignificante de código; portanto, não se preocupe, pois ele pode deixar de ser suportado ou desaparecer: 99,99% de todo o trabalho árduo nesse caso é realizado pela equipe do GraalVM.

Aqui estão algumas idéias óbvias para sugerir melhorias:

  • Permita que os módulos JS importem módulos Java pela coordenada Maven.
  • Formular algumas “melhores práticas” para a javização de módulos NPM. Por exemplo, um arquivo JAR pode conter um diretório node_modules (em resumo: não, pois o NodeJS ainda organiza a E / S do arquivo de sua própria maneira e não sabe nada sobre zips, long: yes, se você se esforçar).
  • Mais idiomas: Python e Ruby não precisam da “cola” para sincronização de threads necessária no NodeJS; portanto, você pode usar apenas a API regular do GraalVM Polyglot . Mas os usuários do Kotlin descobrirão que métodos de conversão / extensão e APIs para variáveis ​​de ligação seriam bons em qualquer idioma.
  • Suporte do Windows.
  • Plugin Gradle para que os programas possam ter listas de dependência em um conjunto misto de idiomas
  • Integração com a ferramenta de native-image , o chamado SubstrateVM; portanto, se você não precisar do desempenho completo do HotSpot em tempo de execução, poderá fornecer pequenos binários vinculados estaticamente no estilo Golang.
  • Talvez algum tipo de conversor para converter TypeScript em Java, para que você possa usar o DefinitelyTyped e mergulhar rapidamente no mundo estático.

Patches são bem-vindos.

Por que você precisaria disso?


Talvez você já esteja pensando: "Uau, o JavaScript, nós, desenvolvido, agora podemos estar em amor mútuo, respeito e harmonia mútuos!"



Reação Entusiasmada Idealizada

É muito possível que você esteja mais próximo deste ponto de vista:



JavaScript e Java não são apenas linguagens. Essas são culturas, e nada é tão doce para os desenvolvedores quanto as CULTURAL WARS!

É por isso que você deve pelo menos marcar esta página como referência futura, mesmo que você queira apenas pegar a arma com o simples pensamento de $ OTHER_LANG invadindo seu precioso ecossistema:

  • Se você é principalmente desenvolvedor Java , agora tem acesso a módulos JavaScript exclusivos que podem não ter um equivalente na JVM (por exemplo, o protocolo DAT). Você pode adorá-lo ou odiá-lo, mas o fato permanece: muitas pessoas escrevem módulos NPM de código aberto, e alguns desses módulos são muito bons. Você também pode reutilizar o código executado em suas interfaces da Web sem a necessidade de transportadores de idiomas. E se você estiver trabalhando com uma base de código herdada no NodeJS, que gostaria de portar gradualmente para Java, de repente esse trabalho será bastante simplificado.
  • Se você é principalmente desenvolvedor de JavaScript , agora tem acesso fácil a bibliotecas exclusivas da JVM, que em JavaScript podem não ter um equivalente direto (por exemplo, Lucene , Mapa de Crônicas ) ou podem oferecer apenas análogos pouco documentados, imaturos ou menos produtivos . Se você quiser ficar sem HTML em seu próximo projeto, poderá explorar a estrutura da GUI para uma pessoa branca . Você também obtém acesso a muitos outros idiomas , por exemplo, objetos Ruby e R. JVM podem ser compartilhados entre funcionários do NodeJS, aproveitando o multithreading na memória compartilhada , se, de acordo com o seu perfilador, essa oportunidade puder ser usada. E se você estiver trabalhando com uma base herdada de código Java que gostaria de portar gradualmente para o NodeJS, subitamente esse trabalho será bastante simplificado.
  • Se você aprender todos os idiomas de uma só vez , poderá fazer a programação multilíngue. Os programadores poliglotas não são odiadores, pelo contrário, podem fazer amizade com o melhor código disponível, independentemente da cultura de origem. Eles são como estudantes do Renascimento que estudaram imediatamente Inglês, Francês Latim ... todos esses idiomas são um para eles. Eles misturam as bibliotecas Java, Kotlin, JavaScript, Scala, Python, Ruby, Lisp, R, Rust, Smalltalk, C / C ++ e até FORTRAN, unindo puramente um inteiro puro no GraalVM.
  • Por fim, se você é um usuário feliz do NodeJS e outros idiomas não o incomodam, você ainda pode experimentar o GraalVM.

O NodeJS é baseado na V8, uma máquina virtual projetada para usar scripts de thread único de curta duração que são executados em PCs e smartphones. É exatamente isso que é financiado pelo Google, mas o V8 também é usado em servidores. O OpenJDK foi otimizado por décadas em servidores. As versões mais recentes contêm ZGC e Shenandoah , dois coletores de lixo que permitem latência mínima, ferramentas que permitem consumir terabytes de memória, com apenas alguns milissegundos de pausas. Portanto, você pode até reduzir custos usando a excelente infraestrutura e ferramentas do GraalVM , sem sequer abandonar o monolingualismo.



Exibir a pilha que contém objetos Ruby



Métricas de CPU disponíveis através de HTTP



Diagnóstico especializado ultra-profundo, demonstrando como o código foi otimizado

Arquitetura vertical


Chegamos ao último tópico que gostaria de discutir neste artigo.

Às vezes digo a alguém tudo o que foi dito acima, mas elas me respondem: “ Isso é ótimo, mas todos esses microsserviços não estão nos dando tudo isso? Por que toda essa confusão? É difícil entender por que gosto tanto de programação multilíngue, mas o fato é que me parece que as arquiteturas de microsserviços precisam de uma concorrência saudável.

Em primeiro lugar, sim, às vezes você precisa dirigir muitos serviços em uma variedade de servidores que precisam interagir. Trabalhei por mais de 7 anos no Google, lidando quase diariamente com o orquestrador de contêineres Borg. Escrevi "microsserviços", embora nem ligássemos para eles e eu os consumi. Caso contrário, não havia como lidar, porque nossas cargas de trabalho exigiam a participação de milhares de máquinas!

No entanto, para essas arquiteturas, você deve pagar um preço alto:

  1. Serialização Isso resulta imediatamente em uma diminuição da produtividade, mas, mais importante, exige que você alinhe constantemente suas estruturas de dados pelo menos parcialmente digitadas e otimizadas, transformando-as em árvores simples. Ao recorrer ao JSON, você perde a capacidade de fazer coisas simples, por exemplo, ter muitos objetos pequenos que apontam para vários objetos grandes (para evitar repetições, você deve usar seus próprios índices).
  2. Versionamento Isso é complicado. As universidades geralmente não ensinam essa disciplina difícil, mas todos os dias, no campo da engenharia de software e, mesmo que você pense que entendeu completamente a diferença entre compatibilidade direta e reversa, mesmo que tenha certeza de que entende o que é rolagem em vários estágios, pode garantir que tudo isso será entendido pela pessoa que o substituirá? Você está realizando testes de integração corretamente para várias combinações de versões que podem se desenvolver durante a implantação não atômica? Nas arquiteturas distribuídas, vi vários desastres genuínos que se resumiam ao fato de as versões se perderem.
  3. Coerência . Realizar operações atômicas no mesmo servidor é bastante fácil. É muito mais difícil garantir que os usuários, em qualquer situação, vejam uma imagem absolutamente consistente quando muitas máquinas estiverem envolvidas no sistema, especialmente se ocorrer compartilhamento de dados entre eles. É por isso que, historicamente, os mecanismos de banco de dados relacional não podem se orgulhar de boa escalabilidade. Deixe-me dizer: os melhores engenheiros do Google passaram décadas tentando simplificar a programação distribuída para suas equipes, tentando construí-lo para que pareça mais com o tradicional.
  4. Re-implementação . Como as chamadas de procedimento remoto são caras, você não fará muitas chamadas e não há mais o que fazer para resolver alguns problemas, mas reimplementar o código. O Google criou algumas bibliotecas para trabalhar com vários idiomas ao mesmo tempo, projetadas para fazer chamadas para procedimentos remotos; Há também situações em que esse código deve ser reescrito do zero.

Então, qual é a alternativa?

Simplificando, muito ferro. Esse método pode parecer absurdamente avô, mas lembre-se de que o custo do hardware está diminuindo constantemente, muitas cargas de trabalho não são chamadas de "global global" e sua intuição sobre o que você deve gastar pode falhar.

Aqui está uma lista de preços relativamente recente de um fabricante canadense:



Uma máquina nuclear de quarenta com um terabyte de RAM e quase um terabyte no disco rígido custa hoje cerca de US $ 6 mil. Imagine quanto tempo, durante toda a vida do projeto, sua equipe terá para resolver problemas com sistemas distribuídos e quanto isso lhe custará.

Sim, mas nem todas as empresas de hoje são baseadas na Web globalmente?

Em suma, não.

O mundo está cheio de empresas para as quais o seguinte é verdadeiro:

  • Eles operam em mercados estáveis.
  • Eles ganham vendendo coisas.
  • Consequentemente, sua base de clientes é de várias dezenas de milhares a dezenas de milhões de pessoas, mas não na casa dos bilhões.
  • Seus conjuntos de dados geralmente são associados a seus próprios clientes e produtos.

Um bom exemplo dessa empresa é um banco. Os bancos não experimentam "hiper-crescimento", não se tornam "virais". Seu modelo de crescimento é moderado e previsível, se alguém assumir que possui algum tipo de crescimento (os bancos são regionais e geralmente operam em mercados saturados). A base de clientes do maior banco dos EUA é de cerca de 50 milhões de usuários e, é claro, não dobra a cada seis meses. Nesse caso, a situação não é igual à do Instagram. Portanto, é de admirar que o mainframe ainda seja baseado em um sistema bancário típico? Obviamente, o mesmo vale para empresas de logística, manufaturas, etc. Este é o pão com manteiga da nossa economia.

Nesses negócios, é perfeitamente possível que as necessidades de cada aplicativo específico relacionado a eles sempre possam ser atendidas com os recursos de apenas uma máquina grande. Sim, até mesmo alguns sites públicos hoje cabem em uma única máquina. Em 2015, Maciej Tseglovsky fez uma palestra muito interessante sobre “ A crise com a obesidade dos sites ” e observou que seu próprio site com serviço de bookmarking era rentável, mas seu concorrente postou o mesmo site na AWS - e perdeu, apenas por causa dos custos diferentes equipamentos e várias suposições sobre complexidade. Em um estudo sobre a comparação das escalas vertical e horizontal, verificou-se que o PlentyOfFish roda em aproximadamente ~ um megaserver (o artigo data de 2009, para que você possa ignorar os preços dos equipamentos listados lá). O autor faz alguns cálculos e mostra que um servidor não é tão estúpido quanto parece. Por fim, se você está considerando o Hadoop e o Big Data, leia este artigo de pesquisa da Microsoft em 2013, que demonstra que muitas das cargas de trabalho do Hadoop da Microsoft, Yahoo e Facebook são realmente executadas com muito mais rapidez e eficiência em uma única máquina grande do que em um cluster. E assim foi há 6 anos! É provável que, desde então, a ênfase a favor da escala vertical tenha se tornado ainda mais pronunciada.

No entanto, a economia real não está associada a nenhum equipamento, mas à otimização do tempo de trabalho extremamente caro dos engenheiros, que é gasto na criação de vários microsserviços minúsculos que devem ser dimensionados horizontalmente com o gerenciamento elástico da demanda. Essa abordagem de engenharia é arriscada e demorada, mesmo se você usar os brinquedos mais recentes disponíveis na nuvem. Você pode perder transações SQL, SOLID, criação de perfil unificado e definitivamente perderá informações como rastreamento de pilha entre sistemas. A segurança de tipo desaparecerá sempre que você for além do servidor. Você receberá chamadas para funções que podem exceder o tempo limite, sobrecarga excessiva associada à compilação dinâmica, falhas inesperadas de contrapressão, mecanismos de orquestração complexos com formatos de configuração sofisticados e ... ah, as memórias realmente inundaram. Foi interessante trabalhar com tudo isso quando eu tinha a arquitetura proprietária do Google e um orçamento de engenharia pesado à minha disposição, mas hoje arriscaria repetir isso apenas se não tivesse outra escolha.

Segundo a experiência, é impossível trabalhar com servidores muito grandes que operam na coleta de lixo - o fato é que a coleta de lixo em si era uma tecnologia pouco desenvolvida e, portanto, esse tópico permaneceu puramente acadêmico. De qualquer forma, você tinha que dirigir vários servidores ao mesmo tempo. , ZGC Shenandoah, , 80 – . , -, – , .

, ? – , ? – : , , … .

Conclusão


NodeJVM – , GraalVM. NPM Java/Kotlin, JS , Kotlin JavaScript, JS , V8.

, JS Java, Java JS .

, 4 , – – , . .

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


All Articles