Background Image
TECNOLOGÍA

Java compilado de forma nativa en Google App Engine

Brendon Anderson

Director de Consultoría

June 14, 2022 | 6 Minuto(s) de lectura

Google App Engine es un producto de plataforma como servicio que se comercializa como una forma de llevar sus aplicaciones a la nube sin tener que conocer necesariamente todas las piezas de infraestructura para hacerlo. Google App Engine existe desde 2008 y fue uno de los primeros servicios en la nube de Google. Inicialmente soportaba Python, pero pronto se amplió a Java y ahora soporta muchos lenguajes como Go, .Net, Node.js y otros. El entorno estándar de App Engine gestiona la ejecución de instancias por ti escalando desde cero instancias (en el caso de que no haya tráfico) hasta escalar rápidamente para satisfacer cualquier nivel de demanda. Una aplicación con poco tráfico se puede ejecutar de forma gratuita, lo que es ideal para la creación de prototipos o aplicaciones de prueba de concepto.

Recientemente, mientras leía sobre Java compilado de forma nativa, me encontré con un chisme que decía que Java compilado como un binario nativo se puede implementar en el entorno estándar de App Engine. Así que pensé en investigar más a fondo, ya que sólo era consciente de la implementación de un artefacto de tipo war o jar en App Engine.

Introducir Micronaut

Si tu aplicación desplegada en App Engine no está sirviendo ninguna petición, se reduce a cero instancias automáticamente. Cuando llegue una solicitud, App Engine iniciará una instancia automáticamente. Usted quiere que su aplicación se inicie rápidamente para que la primera solicitud no tenga que esperar demasiado tiempo para ser servida. Incluso una aplicación Spring de tamaño moderado puede tardar varios segundos en iniciarse. Otro framework que promociona su capacidad para arrancar rápidamente es Micronaut. La rápida velocidad de arranque de Micronaut se atribuye a cómo realiza todas sus operaciones de inyección de dependencias y de tipo AOP en tiempo de compilación. Spring hace todo esto en tiempo de ejecución. Aún mejor, Micronaut tiene mucho soporte para construir binarios compilados nativamente usando GraalVM, lo que hace que el tiempo de arranque y el consumo de memoria sean aún menores. Otro beneficio de Micronaut es que se parece mucho a Spring cuando se desarrolla una aplicación, por lo que es una transición bastante fácil para aquellos desarrolladores de Spring por ahí.

Nota

Antes de ir más lejos en este post, debo señalar que hice todo el desarrollo en Linux. Los binarios compilados en MacOS o Windows probablemente no funcionarán en Google App Engine. En un entorno real, existirá una tubería CI/CD donde el binario se construye utilizando Linux en un contenedor Docker.

Requisitos previos

Varios requisitos previos son necesarios para empezar, algunos de los cuales ya pueden existir para usted. No voy a entrar en detalles sobre cómo se instalan, pero tendrá enlaces disponibles para empezar.

  • SDKMan - ¡Una gran herramienta para tener instalada de todos modos!

  • GraalVM - Instalar con SDKMan (la versión puede ser diferente)

  • sdk install java 22.1.0.r17-grl 

  • La herramienta native-image de GraalVM

  • gu install native-image 

  • Gradle - Instalar con SDKMan

  • sdk install gradle 

  • Micronaut CLI - Instalar con SDKMan

  • sdk install micronaut 

  • zlib y musl

  • Cree una cuenta GCP y un proyecto App Engine. Si te dan a elegir, elige el entorno "standard" (en lugar de "flex"). Hay algunas instrucciones básicas aquí.

  • gcloud SDK CLI

Código

El código fuente completo de mi aplicación de ejemplo se puede encontrar en GitHub, pero voy a caminar a través de él y señalar algunas gotchas.

Utilice Micronaut para crear su aplicación:

mn create-app com.improving.native-on-app-engine --build=gradle --lang=java --features=graalvm 

En este punto, usted tendrá una cáscara de una aplicación con la mayor parte de lo que usted necesitará.

El primer paso es crear un controlador simple. En mi código, he inyectado una propiedad para indicar de qué perfil está leyendo. Los perfiles de Micronaut funcionan de forma similar a Spring nombrando los archivos application.yml apropiadamente.

@Controller("/hola") public class HolaController { @Inject // necesario para las variables privadas cuando se compila de forma nativa @Property(name = "app.saludo") private String saludo; private final String baseText; public HelloController(@Property(name = "app.basetext") String txt) { this.baseText = txt; } @Get @Produce(MediaType.TEXT_PLAIN) public String index() { return baseText + " " + saludo; } }

Todo esto debería ser normal excepto por la anotación @Inject. Cuando se inyectan variables privadas, la anotación @Inject es necesaria para la compilación nativa (no es necesaria para artefactos tipo jar/war). Otras formas de evitar esto son tener un setter público o usar inyección de constructor, como he hecho aquí. Establecer la variable como package-private es otra opción. Creo que en la mayoría de los casos, la inyección del constructor es la mejor ruta.

El archivo build.gradle también necesita algunas modificaciones. Necesitará este plugin:

id("com.google.cloud.tools.appengine") version '2.4.2'

Estas bibliotecas adicionales:

annotationProcessor("io.micronaut:micronaut-graal")

nativeImageCompileOnly("com.google.cloud:native-image-support:0.14.1")

annotationProcessor("io.micronaut:micronaut-inject-java")

También hay que añadir esta configuración adicional

appengine { stage.artifact = "${buildDir}/native/nativeCompile/native" desplegar { projectId = "YOUR_PROJECT_ID_HERE" version = "1" } }

graalvmNative { binarios{ main { imageName.set('native') buildArgs.addAll(["--verbose", "--static", "--libc=musl"]) } } }

Los argumentos de compilación en el archivo graalvmNative config le dicen que compile estáticamente el binario usando la librería musl para libc. Esto asegura que se ejecutará en cualquier lugar sin importar la versión de libc/glibc instalada en la máquina anfitriona (siempre y cuando sea Linux). Me encontré con problemas al no compilar estáticamente el código. La versión de glibc en App Engine era diferente de la versión en mi portátil Linux por lo que no se iniciaría en App Engine. Compilar estáticamente el binario de esta manera pone todo lo que se necesita en el binario. Crea un binario más grande, pero también aumenta la compatibilidad por lo que se ejecutará en más lugares.

Ver settings.gradle para algunas configuraciones adicionales relacionadas con plugins.

Crea un directorio src/main/appengine/app.yml con el siguiente contenido:

tiempo de ejecución: java11 punto de entrada: ./native

En este punto comprueba que todo funciona sin la compilación nativa. La aplicación debería iniciarse ejecutando ./gradlew ejecutar. Usando curl o Postman para acceder a la URL http://localhost:8080/hello debería devolver el mensaje "Hola Mundo basea menos que haya cambiado el perfil para utilizar uno de los otros archivos de propiedades (estableciendo la variable de entorno MICRONAUT_ENVIRONMENTS=local). Si todo esto funciona como se espera, pasemos a la compilación nativa.

Compilación nativa

Usando el comando ./gradlew nativeCompile debería construir un binario. Es una compilación mucho más larga comparada con la compilación de un archivo jar. El nuevo binario debe ubicarse en /build/native/nativeCompile/native 

Para probar el binario, ejecute el comando ./build/native/nativeCompile/native

Debería notar que los tiempos de arranque caen de 500-600ms a menos de 100ms.

Despliegue en la nube

Cuando creó su proyecto GCP, se le asignó un ID de proyecto. Este ID debe ir en el directorio appengine en el archivo build.gradle de appengine. Si todo está configurado, puedes desplegar el binario usando ./gradlew appengineDeploy que tardará unos minutos.

Cuando el despliegue se complete, te dará una URL base para tu aplicación en la terminal. Si tomas esa URL y añades /hello al final deberías ver el mensaje "Hola Cloud GCP". Se recogió el perfil correcto de forma automática sobre la base de su entorno (application-gcp.yml¡)! La primera vez que usted golpea su punto final, podría tomar 1500ms tiempo real, pero las solicitudes posteriores debe tomar ~ 50ms. Creo que la mayor parte de esos 1500ms es sobrecarga con App Engine haciendo lo que tiene que hacer para poner su aplicación en marcha. Si usted mira los registros reales en GCP verá que su aplicación se inició en 100-200ms.

 

Tecnología
Application Modernization

Reflexiones más recientes

Explore las entradas de nuestro blog e inspírese con los líderes de opinión de todas nuestras empresas.
Blog Image - Unveiling the Future of AI at Google Cloud Next 24 -1
IA/ML

Unveiling the Future of AI at Google Cloud Next ‘24

Get firsthand insights from Improving into the innovation brewing around artificial intelligence and cloud computing at Google Cloud Next '24.