Hola Habr!
Durante mucho tiempo tuve la idea de discutir
GraalVM con usted, posponerlo hasta que finalmente encontré el artículo de hoy, cuyo tema va más allá del alcance de una máquina virtual específica. El autor Mike Hearn describe todo el paradigma de la interacción multilingüe y la programación multilingüe (programación políglota). El siguiente es el famoso ejemplo de escala vertical y un artículo muy largo debajo del corte.

Este artículo trata sobre una forma innovadora de escribir software que puede volverse popular en el futuro, pero probablemente no ahora. ¡El artículo tiene un código, sinceramente!
En la antigüedad, es decir, en 2015, escribí
por qué Kotlin será mi próximo lenguaje de programación , y en 2016 escribí sobre
Graal y Truffle : dos proyectos de investigación radicales relacionados con compiladores que no solo aceleran significativamente el trabajo de lenguajes como Ruby , pero también encarnan en realidad interacciones interlenguaje sin interrupciones. En estos proyectos, el compilador dinámico (JIT) o OpenJDK se reemplaza por uno nuevo que tiene la capacidad de convertir a los intérpretes anotados en compiladores JIT de vanguardia ... automáticamente.
Volviendo a estos temas en 2019, me gustaría mostrarle tres cosas:
- Cómo usar la pequeña biblioteca que escribí para usar casi a la perfección los módulos NPM del código de los programas escritos en Java o Kotlin.
- Explique todas las buenas razones por las que podría necesitarlo, incluso si cree que JavaScript / Java es lo peor del mundo, sin contar el aceite de pescado.
- Explore brevemente el concepto de arquitectura vertical que compite con el diseño orientado a microservicios. Se encuentra en la intersección de las últimas versiones de GraalVM y OpenJDK, y requiere el hardware más avanzado.
Usando NPMs de Java y Kotlin
Tomaremos solo tres pasos simples:
- Toma GraalVM . Este es un conjunto de parches, construido sobre OpenJDK, que apareció justo a tiempo: puede ejecutar todo el código de bytes JVM que tenga.
- Tomamos mi kit de herramientas NodeJVM de github y lo agregamos a nuestro camino.
- Reemplace
java
en la línea de comando con nodejvm
. Eso es todo!
Vale, vale. Admito, aquí dibujo un poco y exagero, hasta el final del artículo tendrás que soportar ese estilo. Por supuesto, todo no es tan simple en absoluto: aún tiene que tomar el módulo y usarlo.
Considere cómo se ve:

Código de muestra con NodeJVM
Echa un vistazo de cerca a esta imagen. Sí, eso es exactamente lo que parece: Kotlin con una cadena de varias líneas incorporada en la que se produce el autocompletado de JavaScript, después de lo cual se realiza un análisis estático de JavaScript y la sintaxis se resalta correctamente. Las mismas operaciones funcionan desde Java u otros lenguajes para la JVM que IntelliJ entiende. Para obtener tales oportunidades, debe hacer clic en el interruptor en la configuración de IDE (lea el archivo Léame de NodeJVM para saber cómo hacerlo), pero más tarde esta función funcionará automáticamente. Si IntelliJ logra averiguar analizando el flujo de datos que su cadena eventualmente se
run
a los métodos de
run
o
eval
, entonces se tratará como JS incrustado.
Aquí voy a hablar sobre la API para Kotlin, ya que es un poco más bonita y más conveniente que la API en Java normal, pero todo lo que describo a continuación también se puede hacer desde Java.
En el código anterior, preste atención a varias de las siguientes características:
- Para acceder a JavaScript, debe usar el bloque
nodejs {}
. El hecho es que JavaScript es de un solo subproceso y, en consecuencia, para ejecutar módulos NPM, debe "ingresar a la secuencia de Nodo". El bloque nodejs {}
realiza tal sincronización para nosotros, sin importar en qué hilo estemos. Por lo tanto, debe recordar constantemente: para ejecutar cualquier código JS, en principio, debe estar dentro de dicho bloque. Puede volver a ingresarlo tantas veces como lo desee, por lo que será seguro usar dicho bloque en cualquier lugar donde lo necesitemos. Todas las devoluciones de llamada de JavaScript se ejecutarán en el subproceso Node y, por lo tanto, a todos los demás subprocesos se les negará el acceso al bloque nodejs
, por lo que si le preocupa el rendimiento o la representación fluida de la GUI, evite realizar operaciones de larga duración en devoluciones de llamada. - La sintaxis
var x by bind(SomeObject())
solo está disponible en el bloque nodejs
y le permite conectarse a la misma variable en el ámbito global de JavaScript. Cuando x cambia de Kotlin, cambiará en JS y viceversa. Aquí adjunto un objeto de File
Java normal al mundo JS. - El método
eval
devuelve ... lo que le pedimos que devuelva, sin embargo, en forma de tipeo estático. Esta es una función genérica y, simplemente especificando el tipo de entidad que le asignamos, nos aseguraremos de que eval convierta automáticamente el objeto JavaScript en una clase estática o una interfaz Java / Kotlin / Scala / etc. Aunque esto no se indicó explícitamente anteriormente, MemoryUsage
es un tipo de interfaz simple que MemoryUsage
, y tiene las funciones rss()
y heapTotal()
. Se asignan a las propiedades de JavaScript del mismo nombre, aplicándolas a lo que obtienes del Node API process.memoryUsage()
. Por lo tanto, la mayoría de los tipos JS se pueden convertir a tipos Java "normales"; La documentación detallada de cómo funciona está disponible en el sitio web de GraalVM. Los objetos resultantes pueden almacenarse en cualquier lugar, pero las llamadas al método en ellos, por supuesto, deben hacerse en el bloque nodejs
. - Los objetos de JavaScript también pueden considerarse asignaciones simples de una cadena a un objeto, que en muchos aspectos realmente coincide con su naturaleza. A su vez, tales asignaciones de una cadena a un objeto se pueden devolver a una cosa tipada más fuerte, que se puede ver claramente en la devolución de llamada. Utiliza la presentación que prefieras.
- Puede usar
require
y buscará módulos en los directorios node_modules
de la manera habitual.
El fragmento de código anterior usa el protocolo
DAT , que le permite conectarse a una red punto a punto que se parece remotamente a BitTorrent y luego buscar pares que tengan el archivo deseado. Uso DAT como ejemplo, ya que está (a) descentralizado y, por lo tanto, es especialmente elegante y (b) para bien o para mal, su implementación de referencia está escrita en JavaScript. Este no es un programa que podría escribir completamente sin usar JS en un tiempo razonable.
Esto también se puede hacer desde 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); } }
La API de Java no le proporciona un enlace variable tan agradable y una transmisión automática como la API de Kotlin, pero es bastante fácil de usar. Aquí mostramos cómo convertimos el resultado a un tipo entero Java (
int
) y lo devolvemos “desde” la secuencia Node: en este caso, la secuencia Java principal
no es
la misma que la secuencia NodeJS, pero cambiamos entre estos flujos completamente sin problemas.
NodeJVM es un contenedor muy, muy pequeño en la parte superior de
GraalVM . Agrega una cantidad insignificante de código, así que no se preocupe si puede dejar de ser compatible o desaparecer: el 99.99% de todo el trabajo duro en este caso es realizado por el equipo de GraalVM.
Aquí hay algunas ideas obvias para sugerir mejoras:
- Permita que los módulos JS importen módulos Java por coordenadas Maven.
- Formular algunas "mejores prácticas" para la javisación de módulos NPM. Por ejemplo, ¿puede un archivo JAR contener un directorio node_modules (en resumen: no, ya que NodeJS todavía organiza las E / S del archivo a su manera y no sabe nada acerca de las cremalleras, largo: sí, si lo intenta duro).
- Más idiomas: Python y Ruby no necesitan el "pegamento" para la sincronización de subprocesos que se necesita en NodeJS, por lo que puede usar la API de GraalVM Polyglot normal . Pero los usuarios de Kotlin encontrarán que sería bueno tener métodos de conversión / extensión y API para variables de enlace en cualquier idioma.
- Soporte de Windows.
- Complemento Gradle para que los programas puedan tener listas de dependencias en un conjunto mixto de idiomas
- Integración con la herramienta de
native-image
, la llamada SubstrateVM; así que si no necesita el rendimiento completo de HotSpot en tiempo de ejecución, puede suministrar pequeños binarios estáticamente vinculados en estilo Golang. - Tal vez algún tipo de convertidor para convertir TypeScript a Java, para que pueda usar DefinitelyTyped y sumergirse rápidamente en el mundo estático.
Los parches son bienvenidos.
¿Por qué necesitarías esto?
Quizás ya esté pensando: "¡Guau, JavaScript, nosotros, desarrollados, ahora podemos estar en amor mutuo, respeto mutuo y armonía!"

Reacción entusiasta idealizada
Es muy posible que esté más cerca de este punto de vista:

JavaScript y Java no son solo lenguajes. ¡Estas son culturas, y nada es tan dulce para los desarrolladores como CULTURAL WARS!
Esta es la razón por la que al menos debería marcar esta página como referencia futura, incluso si solo desea tomar el arma con el solo pensamiento de que $ OTHER_LANG invada su precioso ecosistema:
- Si es principalmente un desarrollador de Java , ahora tiene acceso a módulos JavaScript únicos que pueden no tener un equivalente en la JVM (por ejemplo, el protocolo DAT). Puede adorarlo u odiarlo, pero el hecho es que: muchas personas escriben módulos NPM de código abierto, y algunos de estos módulos son muy buenos. También puede reutilizar el código que se ejecuta en sus interfaces web sin la necesidad de transportadores de idiomas. Y si está trabajando con una base de código heredada en NodeJS, que le gustaría transferir gradualmente a Java, entonces, de repente, este trabajo se simplifica enormemente.
- Si es principalmente un desarrollador de JavaScript , ahora tiene fácil acceso a bibliotecas JVM únicas, que en JavaScript pueden no tener un equivalente directo (por ejemplo, Lucene , Chronicle Map ) o pueden ofrecer solo análogos poco documentados, inmaduros o menos productivos . Si desea prescindir de HTML en su próximo proyecto, puede explorar el marco de la GUI para una persona blanca . También tiene acceso a muchos otros idiomas , por ejemplo, los objetos de Ruby y R. JVM se pueden compartir entre los empleados de NodeJS, aprovechando el subprocesamiento múltiple en la memoria compartida , si, de acuerdo con su generador de perfiles, se puede utilizar esta oportunidad. Y si está trabajando con una base heredada de código Java que le gustaría transferir gradualmente a NodeJS, entonces, de repente, este trabajo se simplifica enormemente.
- Si aprende todos los idiomas a la vez , puede hacer programación multilingüe. Los programadores de Polyglot no odian, por el contrario, pueden hacerse amigos con el mejor código disponible, sin importar de qué cultura provenga. Son como estudiantes del renacimiento que inmediatamente estudiaron inglés, francés y latín ... todos estos idiomas son uno para ellos. Combinan las bibliotecas Java, Kotlin, JavaScript, Scala, Python, Ruby, Lisp, R, Rust, Smalltalk, C / C ++ e incluso FORTRAN, uniendo simplemente un entero ordenado sobre GraalVM.
- Finalmente, si eres un usuario feliz de NodeJS y otros idiomas no te molestan en absoluto, entonces quizás quieras experimentar con GraalVM.
NodeJS se basa en V8, una máquina virtual diseñada para usar scripts de un solo subproceso de corta duración que se ejecutan en PC y teléfonos inteligentes. Esto es exactamente lo que financia Google, pero V8 también se usa en servidores. OpenJDK se ha optimizado durante décadas en servidores. Las últimas versiones contienen
ZGC y
Shenandoah , dos recolectores de basura que permiten una latencia mínima, herramientas que le permiten consumir terabytes de memoria, con solo unos pocos milisegundos de pausas. Por lo tanto, incluso puede reducir costos utilizando la
excelente infraestructura y herramientas GraalVM , sin siquiera abandonar el monolingüismo.

Ver el montón que contiene objetos Ruby

Métricas de CPU disponibles a través de HTTP

Diagnósticos expertos ultra profundos que demuestran cómo se optimizó el código
Arquitectura vertical
Llegamos al último tema que me gustaría discutir en este artículo.
A veces le digo a alguien todo lo anterior, pero me responden: “
Esto es genial, pero ¿no nos están dando todos estos microservicios todo esto? ¿Por qué es todo el alboroto? Es difícil apreciar por qué me gusta tanto la programación multilingüe, pero la cuestión es que me parece que las arquitecturas de microservicios necesitan una competencia saludable.
En primer lugar, sí, a veces necesita manejar muchos servicios en una variedad de servidores que necesitan interactuar. Trabajé durante más de 7 años en Google, casi a diario trataba con su orquestador de contenedores Borg. Escribí "microservicios", aunque ni siquiera los llamamos, y los consumí. De lo contrario, no había forma de hacer frente, ¡porque nuestras cargas de trabajo requerían la participación de miles de máquinas!
Sin embargo, para tales arquitecturas hay que pagar un precio alto:
- Serialización Inmediatamente resulta en una disminución de la productividad, pero, lo que es más importante, requiere que alinee constantemente sus estructuras de datos optimizadas y al menos parcialmente, convirtiéndolas en árboles simples. Al recurrir a JSON, pierde la capacidad de hacer cosas simples, por ejemplo, tener muchos objetos pequeños que apuntan a varios objetos grandes (para evitar la repetición, debe usar sus propios índices).
- Versionado Esto es complicado Las universidades a menudo no enseñan esta disciplina difícil pero cotidiana desde el campo de la ingeniería de software e incluso si crees que has entendido completamente la diferencia entre la compatibilidad directa y la compatibilidad con versiones anteriores, incluso si estás seguro de que entiendes lo que es el rodamiento de etapas múltiples, ¿puedes garantizar? que todo esto lo entenderá la persona que lo reemplazará? ¿Está realizando pruebas de integración correctamente para varias combinaciones de versiones que pueden desarrollarse durante el despliegue no atómico? En arquitecturas distribuidas, vi varios desastres genuinos que se redujeron al hecho de que las versiones se extraviaron.
- Coherencia Realizar operaciones atómicas dentro del mismo servidor es bastante fácil. Es mucho más difícil garantizar que los usuarios en cualquier situación vean una imagen absolutamente coherente cuando muchas máquinas están involucradas en el sistema, especialmente si se produce una división de datos entre ellas. Es por eso que históricamente, los motores de bases de datos relacionales no pueden presumir de una buena escalabilidad. Déjame decirte: los mejores ingenieros de Google han pasado décadas intentando simplificar la programación distribuida para sus equipos, tratando de construirla para que se parezca más a una tradicional.
- Reimplementación . Como las llamadas a procedimientos remotos son costosas, no hará muchas de esas llamadas, y no queda nada que hacer para resolver algunos problemas, sino volver a implementar el código. Google ha creado algunas bibliotecas para trabajar con varios idiomas a la vez, diseñadas para realizar llamadas a procedimientos remotos; También hay situaciones en las que dicho código debe reescribirse desde cero.
Entonces, ¿cuál es la alternativa?
En pocas palabras, mucho hierro. Este método puede parecer absurdamente abuelo, pero tenga en cuenta que el costo del hardware está disminuyendo constantemente, muchas cargas de trabajo no se llaman "global global", y su intuición sobre lo que debe gastar puede fallarle.
Aquí hay una
lista de precios relativamente nueva de un fabricante canadiense:

Una máquina de cuarenta nucleares con un terabyte de RAM y casi un terabyte en el disco duro cuesta alrededor de $ 6k hoy. Imagine cuánto tiempo tendrá su equipo durante toda la vida del proyecto para resolver problemas con sistemas distribuidos y cuánto le costará.
Sí, pero ¿no están todas las empresas actuales basadas en la web global?
En resumen, no.
El mundo está lleno de compañías para las cuales lo siguiente es cierto:
- Operan en mercados estables.
- Ganan vendiendo cosas.
- En consecuencia, su base de clientes es de varias decenas de miles a decenas de millones de personas, pero no en miles de millones.
- Sus conjuntos de datos generalmente están asociados con sus propios clientes y productos.
Un buen ejemplo de tal empresa es un banco. Los bancos no experimentan "hipercrecimiento", no se vuelven "virales". Su modelo de crecimiento es moderado y predecible, si se supone que tienen algún tipo de crecimiento (los bancos son regionales y generalmente operan en mercados saturados). La base de clientes del banco más grande de los Estados Unidos es de aproximadamente 50 millones de usuarios y, por supuesto, no se duplica cada seis meses. En este caso, la situación no es la misma que con Instagram. Por lo tanto, ¿es de extrañar que el mainframe todavía se base en un sistema bancario típico? Por supuesto, lo mismo es cierto para las empresas de logística, empresas de fabricación, etc. Este es el pan de cada día de nuestra economía.
En tales negocios, es completamente posible que las necesidades de cada aplicación específica relacionada con ellas siempre se puedan satisfacer con los recursos de una sola máquina grande. Sí, incluso algunos sitios públicos de hoy caben en una sola máquina. En 2015, Maciej Tseglovsky dio una conferencia muy interesante sobre "
La crisis con la obesidad de los sitios " y señaló que su propio sitio con servicio de marcadores era rentable, pero su competidor publicó el mismo sitio en AWS, y perdió, solo debido a diferentes costos equipo y varios supuestos sobre la complejidad. En un estudio sobre la comparación de
escala vertical y horizontal, se descubrió que PlentyOfFish se ejecuta en aproximadamente ~ un mega servidor (el artículo data de 2009, por lo que puede ignorar los precios de los equipos enumerados allí). El autor hace algunos cálculos y muestra que un servidor no es tan estúpido como podría parecer. Finalmente, si está considerando Hadoop y Big Data, lea este
artículo de investigación de Microsoft 2013, que demuestra que muchas de las cargas de trabajo de Hadoop de Microsoft, Yahoo y Facebook en realidad se ejecutan mucho más rápido y de manera más eficiente en una sola máquina grande, en lugar de en un clúster. ¡Y así fue hace 6 años! Es probable que desde entonces el énfasis a favor de la escala vertical se haya vuelto aún más pronunciado.
Sin embargo, los ahorros reales no están asociados con el equipo en absoluto, sino con la optimización del tiempo de trabajo extremadamente costoso de los ingenieros, que se gasta en crear un montón de pequeños microservicios que deben escalarse horizontalmente con una gestión elástica de la demanda. Tal enfoque de ingeniería es arriesgado y requiere mucho tiempo, incluso si usa los últimos juguetes disponibles en la nube. Puede perder SQL, transacciones SÓLIDAS, perfiles unificados, y definitivamente perderá información como el seguimiento de la pila entre sistemas. La seguridad de tipos desaparecerá cada vez que vaya más allá del servidor. Recibirá llamadas a funciones que pueden exceder el tiempo de espera, sobrecarga excesiva asociada con la compilación dinámica, fallas inesperadas de contrapresión, motores de orquestación complejos con formatos de configuración sofisticados y ... oh, los recuerdos realmente se inundaron. Fue interesante trabajar con todo esto cuando tenía la arquitectura patentada de Google y un grueso presupuesto de ingeniería a mi disposición, pero hoy me aventuraría a repetir esto solo si no tuviera otra opción.
Según la experiencia, es imposible trabajar con servidores muy grandes que operan en la recolección de basura: la recolección de basura en sí era una tecnología poco desarrollada y, por lo tanto, este tema ha permanecido durante mucho tiempo puramente académico. En cualquier caso, tenía que manejar varios servidores a la vez. , ZGC Shenandoah, , 80 – . , -, – , .
, ? – , ? – : , , … .
Conclusión
NodeJVM – , GraalVM. NPM Java/Kotlin, JS , Kotlin JavaScript, JS , V8.
, JS Java, Java JS .
, 4 , – – , . .