Declaración del problema.
Periódicamente, tengo la tarea de compartir archivos en una red local, por ejemplo, con un colega del proyecto.
Puede haber muchas soluciones para esto: Samba / FTP / scp. Simplemente puede cargar el archivo en un lugar público como Google Drive, adjuntarlo a una tarea en Jira o incluso enviarlo por correo electrónico.
Pero todo esto, en un grado u otro, es inflexible, en algún lugar requiere un ajuste preliminar y tiene sus propias limitaciones (por ejemplo, el tamaño máximo de inversión).
Y quieres algo más ligero y flexible.
Siempre me sorprendió gratamente la oportunidad en Linux, utilizando los medios disponibles, de construir rápidamente una solución práctica.
Digamos, a menudo resolví la tarea mencionada usando el sistema python con la siguiente línea simple
$ python3 -mhttp.server Serving HTTP on 0.0.0.0 port 8000 ...
Este comando inicia el servidor web en la carpeta actual y le permite obtener una lista de archivos y descargarlos a través de la interfaz web. Más de estas cosas se pueden tirar aquí .
Hay varios inconvenientes. Ahora, para transferir el enlace de descarga a un colega, debe conocer su dirección IP en la red.
Es conveniente usar el comando
$ ifconfig -a
Y luego, de la lista resultante de interfaces de red, seleccione la apropiada y redacte manualmente un enlace del formulario http: // IP: 8000 , que puede enviar.
Segundo inconveniente: este servidor tiene un solo subproceso. Esto significa que mientras uno de sus colegas descarga el archivo, el segundo ni siquiera podrá descargar la lista de archivos.
En tercer lugar, es inflexible. Si necesita transferir solo un archivo, se abrirá la carpeta completa, es decir Tendrá que realizar tales gestos (y luego limpiar la basura):
$ mkdir tmp1 $ cp file.zip tmp1 $ cd tmp1 $ python3 -mhttp.server
El cuarto inconveniente: no hay una manera fácil de descargar todo el contenido de una carpeta.
Para transferir el contenido de una carpeta, generalmente usan una técnica llamada tar pipe .
Hacen algo como esto:
$ ssh user@host 'cd /path/to/source && tar cf - .' | cd /path/to/destination && tar xvf -
Si de repente no está claro, explicaré cómo funciona. La primera parte del comando tar cf - .
crea un archivo de los contenidos de la carpeta actual y escribe en la salida estándar. Además, esta salida se transmite a través de una tubería a través de un canal ssh seguro a la entrada de un tar xvf -
similar tar xvf -
que realiza el procedimiento contrario, es decir, lee entradas estándar y descomprime en la carpeta actual. De hecho, el archivo se transfiere, ¡pero sin crear un archivo intermedio!
El inconveniente de este enfoque también es obvio. Necesitamos acceso ssh de una máquina a otra, y esto casi nunca se hace en el caso general.
¿Es posible lograr todo lo anterior, pero sin estos problemas descritos?
Entonces, es hora de formalizar lo que construiremos:
- Programa fácil de instalar (binario estático)
- Lo que le permitirá transferir tanto un archivo como una carpeta con todo el contenido
- Con compresión opcional
- Lo que permite que el host descargue los archivos utilizando solo herramientas estándar * nix (wget / curl / tar)
- El programa inmediatamente después del lanzamiento emitirá los comandos exactos para descargar
Solución
En la conferencia JEEConf , a la que asistí no hace mucho, el tema de Graal se planteó repetidamente. El tema está lejos de ser nuevo, pero para mí fue un detonante para finalmente sentir esta bestia con mi propia mano.
Para aquellos que aún no están en el tema (¿existe realmente tal cosa? OO) permítanme recordarles que GraalVM es una JVM tan bombeada de Oracle con características adicionales, las más notables de las cuales son:
- Polyglot JVM: la capacidad de ejecutar sin problemas Java, Javascript, Python, Ruby, R, etc. codigo
- Soporte para compilación AOT: compilación de Java directamente en el binario nativo
- Una característica menos notable, pero muy interesante: el compilador C2 se reescribió de C ++ a Java para facilitar su desarrollo posterior. Esto ya ha producido resultados notables. Este compilador hace muchas más optimizaciones en la etapa de convertir el bytecode de Java a código nativo. Por ejemplo, puede eliminar de manera más efectiva las asignaciones. Twitter pudo reducir el consumo de CPU en un 11% con solo activar esta configuración, que en su escala dio un notable ahorro de recursos (y dinero).
Puede actualizar la idea de Graal, por ejemplo, en este artículo de habr .
Escribiremos en Java, por lo que para nosotros la característica más relevante será la compilación AOT.
En realidad, el resultado del desarrollo se presenta en este repositorio de Github .
Ejemplo de uso para transferir un solo archivo:
$ serv '/path/to/report.pdf' To download the file please use one of the commands below: curl http://192.168.0.179:17777/dl > 'report.pdf' wget -O- http://192.168.0.179:17777/dl > 'report.pdf' curl http://192.168.0.179:17777/dl?z --compressed > 'report.pdf' wget -O- http://192.168.0.179:17777/dl?z | gunzip > 'report.pdf'
Un ejemplo de uso al transferir el contenido de una carpeta (¡todos los archivos, incluidos los archivos adjuntos!):
$ serv '/path/to/folder' To download the files please use one of the commands below. NB! All files will be placed into current folder! curl http://192.168.0.179:17777/dl | tar -xvf - wget -O- http://192.168.0.179:17777/dl | tar -xvf - curl http://192.168.0.179:17777/dl?z | tar -xzvf - wget -O- http://192.168.0.179:17777/dl?z | tar -xzvf -
Si, tan simple!
Tenga en cuenta que el programa mismo determina la dirección IP correcta en la que los archivos estarán disponibles para descargar.
Observaciones / Pensamientos
Está claro que uno de los objetivos al crear el programa era su compacidad. Y aquí está el resultado que se logró:
$ du -hs `which serv` 2.4M /usr/local/bin/serv
Increíblemente, ¡toda la JVM, junto con el código de la aplicación, cabe en unos miserables pocos megabytes! Por supuesto, todo está algo mal, pero más sobre eso más adelante.
De hecho, el compilador Graal produce un binario que es un poco más grande que 7 megabytes de tamaño. Decidí comprimirlo aún más con el UPX .
Esto resultó ser una buena idea, ya que el tiempo de lanzamiento aumentó mientras era muy insignificante:
Opción sin comprimir:
$ time ./build/com.cmlteam.serv.serv -v 0.1 real 0m0.001s user 0m0.001s sys 0m0.000s
Comprimido
$ time ./build/serv -v 0.1 real 0m0.021s user 0m0.021s sys 0m0.000s
A modo de comparación, el tiempo de lanzamiento en la "forma tradicional":
$ time java -cp "/home/xonix/proj/serv/target/classes:/home/xonix/.m2/repository/commons-cli/commons-cli/1.4/commons-cli-1.4.jar:/home/xonix/.m2/repository/org/apache/commons/commons-compress/1.18/commons-compress-1.18.jar" com.cmlteam.serv.Serv -v 0.1 real 0m0.040s user 0m0.030s sys 0m0.019s
Como puede ver, el doble de lento que la versión UPX.
En general, un tiempo de inicio corto es uno de los puntos fuertes de GraalVM. Esto, así como el bajo consumo de memoria, genera un entusiasmo significativo en torno al uso de esta tecnología para microservicios y sin servidor.
Traté de hacer que la lógica del programa sea lo más mínima posible y usar un mínimo de bibliotecas. En principio, este enfoque generalmente está justificado y, en este caso, me preocupaba que agregar dependencias de expertos de terceros “ponderara” significativamente el archivo de programa resultante.
Por ejemplo, por eso no utilicé una dependencia de terceros para un servidor web Java (y hay muchos para todos los gustos y colores), pero utilicé la implementación JDK de un servidor web de com.sun.net.httpserver.*
Paquete. En realidad, el uso del paquete com.sun.*
Se considera de com.sun.*
, pero en este caso lo considero permisible, ya que estoy compilando en código nativo y, por lo tanto, no hay ninguna cuestión de compatibilidad entre la JVM.
Sin embargo, mis miedos eran completamente en vano. En el programa, utilicé dos dependencias por conveniencia
commons-cli
: para analizar argumentos de línea de comandoscommons-compress
- para generar una carpeta tarball y compresión gzip opcional
Al mismo tiempo, el tamaño del archivo aumentó muy ligeramente. Me aventuraré a sugerir que el compilador de Graal es muy inteligente para no poner todos los nicks de complementos en el archivo ejecutable, sino solo el código de ellos que realmente usa el código de la aplicación.
La compilación en código Graal nativo se realiza mediante la utilidad de imagen nativa . Vale la pena mencionar que este proceso requiere muchos recursos. Digamos, en mi configuración no tan lenta con una CPU Intel 7700K a bordo, este proceso lleva 19 segundos. Por lo tanto, recomiendo que al desarrollar, ejecute el programa como de costumbre (a través de Java), y recopile el binario en la etapa final.
Conclusiones
El experimento, me parece, fue muy exitoso. Al desarrollar usando el kit de herramientas Graal, no encontré ningún problema insuperable o incluso significativo. Todo funcionó de manera predecible y estable. Aunque, casi con certeza, todo no será tan fácil si intenta construir algo más complejo de esta manera, por ejemplo, una aplicación en Spring Boot . Sin embargo, ya se han presentado varias plataformas en las que se declara el soporte nativo para Graal. Entre ellos están Micronaut , Microprofile , Quarkus .
En cuanto al desarrollo posterior del proyecto, una lista de mejoras planificadas para la versión 0.2 ya está lista. Además, por el momento, el ensamblaje del binario final se implementa solo para Linux x64. Espero que esta omisión se solucione en el futuro, especialmente porque el compilador de native-image
de Graal es compatible con MacOS y Windows. Desafortunadamente, aún no admite la compilación cruzada, lo que podría facilitar las cosas.
Espero que la utilidad presentada sea útil al menos para alguien de la comunidad habr acreditada. Estaré doblemente contento si hay quienes desean contribuir al proyecto .