Introducción
Algunos de los principales inconvenientes a la hora de construir servicios Java para integrarlos a las ya comúnmente conocidas arquitecturas Cloud Serverless son la cantidad de memoria y el tiempo que emplea la Java Virtual Machine (JVM) para iniciar y así poder tener listo nuestra aplicación para su uso.
Estos factores claramente dejan a Java en desventajas frente a otros lenguajes de programación como Node.js o Go; a la hora de construir soluciones Severless basados en un modelo FaaS (Function as a Service), ya que estos últimos lenguajes mencionados otorgan mejores números en cuanto a utilización de memoria (memory footpring), tamaño de la aplicación y tiempo de inicio de nuestra aplicación antes de poder atender peticiones (cold start), que son factores críticos a la hora de evaluar el performance y sacar el mejor provecho a nuestras herramientas y servicios Cloud.
Aquí entra en escena Quarkus, un Framework para desarrollar aplicaciones Java que promete dar solución a estos problemas de rendimiento en las que aplicaciones Java y sus frameworks de desarrollo mayormente utilizados como Spring, los cuales no están del todo optimizados para este tipo de arquitecturas. Quarkus se caracteriza principalmente por otorgarle a nuestras aplicaciones, sobre todo en modo bytecode nativo, propiedades como:
- Aligerar los tiempos de arranque
- Un menor consumo de memoria
- Tamaño de aplicación más reducido
Ejecutables Quarkus
Código binario JVM optimizado
Modo en el cual el framework compila nuestro código (bytecode), crea un artefacto y es ejecutado (interpretado) a través de la JVM, pero con algunos ajustes por parte de Quarkus. En tiempo de construcción del artefacto se ajustan tareas como la construcción de entidades de base de datos, el procesado de anotaciones, el análisis y carga de configuraciones; y en tiempo de ejecución el framework se reduce la información dinámica que necesita la aplicación, consiguiendo que tareas que normalmente realizadas en fase de ejecución (runtime) de la aplicación sean ejecutadas durante la construcción (build) del artefacto, así se consigue reducir los tiempos de arranque de la aplicación, factor importante para entornos contenerizados o Serverless (short-lived).
Código binario nativo
El otro modo de Quarkus es native image, donde se da un paso más en la optimización de las aplicaciones y el resultado de la construcción del artefacto es un ejecutable (standalone executable) que no necesita una JVM para su ejecución; ya que este genera código máquina que es perfectamente legible e interpretado por el hardware en el que se ejecuta, y contiene toda las clases y dependencias (incluidas las de la JDK) que precisa la aplicación para funcionar. El soporte de Quarkus para generar este tipo de artefactos impacta notablemente en la reducción de los consumos de memoria y tiempos de arranque de aplicaciones Java en este caso.
Creación Quarkus Lambda Function [Caso Práctico]
Prerrequisitos
- JDK 11+ (JAVA_HOME Configurado)
- Apache Maven 3.3.9+
- IDE (IntelliJ IDEA, utilizado en las pruebas)
- Quarkus CLI, para la generación de proyectos a partir de arquetipos (Opcional).
- Cuenta Amazon AWS
- AWS CLI instalado y configurado.
- AWS SAM CLI, local serverless testing.
- Docker para la construcción de imágenes nativas. (Utilizado en las pruebas)
Opcionalmente se puede utilizar Mandrel o GraalVM para la construcción de imágenes nativas.
Generación del proyecto base
Para la generación de proyecto Quarkus ofrece 2 opciones al desarrollador, uno a través de Maven (utilizando el arquetipo Maven de Quarkus) y la otra mediante su página web de Quarkus Code.
Generación Maven
Se inicia la construcción del arquetipo:
mvn archetype:generate \
-DarchetypeGroupId=io.quarkus \
-DarchetypeArtifactId=quarkus-amazon-lambda-archetype \
-DarchetypeVersion=2.8.0.Final
Se introducen los atributos básicos para el proyecto Maven:
[INFO] Generating project in Interactive mode
[INFO] Archetype repository not defined. Using the one from [io.quarkus:quarkus-amazon-lambda-archetype:0.28.1] found in catalog remote
Define value for property 'groupId': com.at
Define value for property 'artifactId': lambda-quarkus
Define value for property 'version' 1.0-SNAPSHOT: : 1.0.0-SNAPSHOT
Define value for property 'package' com.at: : com.at.lambda
Confirm properties configuration:
groupId: com.at
artifactId: lambda-quarkus
version: 1.0.0-SNAPSHOT
package: com.at.lambda
Generación Quarkus Code
Quarkus expone una herramienta a través de su página web, para ayudar al desarrollador con una interfaz gráfica más amigable la creación de los proyectos Maven con las dependencias necesarias y las propiedades del artefacto a construir:
Con la herramienta podemos filtrar por plataforma “lambda” y seleccionar la extensión “AWS Lambda”.
Estructura
Las anteriores acciones generan el proyecto Maven con los parámetros seleccionados y preparado para ser importado en cualquier IDE que soporte este tipo de proyecto.
Desarrollo De La Lambda
Para la aplicación es importante definir el punto de entrada a la lambda, y esto se logra implementando la interfaz RequestStreamHandler o RequestHandler de manera obligatoria dentro del código, de otra forma la extensión quarkus-amazon-lambda generará un error en tiempo de compilación.
@Named("personLambda")
public class PersonLambda
implements RequestHandler<InputObject, OutputObject> {
…
@Named("personLambda")
public class PersonLambda implements RequestStreamHandler {
…
El proyecto creado por el arquetipo vendrá ya con una implementación básica de la interfaz RequestHandler que podemos modificar de acuerdo con los requisitos particulares de la Lambda.
Para más información sobre el desarrollo con Quarkus pulse aquí.
En caso de que el código implemente más de una la interfaz RequestStreamHandler o RequestHandler la extensión permite distinguir cual de todas las implementaciones debe lanzar AWS Lambda a través de la propiedad quarkus.lambda.handler de la aplicación:
quarkus.lambda.handler= personLambda
Obsérvese que el valor de la propiedad quarkus.lambda.handler (/src/main/resources/application.properties) tiene que coincidir con el valor asignado en la anotación “named” de la clase PersonLambda que implementa la interfaz RequestHandler:
@Named("personLambda")
public class PersonLambda implements RequestHandler<Person, String> {
@Inject
PersonService personService;
@ConfigProperty(name = "greeting.prefix")
String prefixGreeting;
@Override
public String handleRequest(Person input, Context context) {
return prefixGreeting + " " + personService.getName(input);
}
}
La implementación de la interfaz RequestHandler también define los objetos de entrada y salida de la Lambda, en esta implementación se recibe un Objeto tipo Person y retorna un String como objetivo de salida.
El método handleRequest que implementa la clase principal además de definir los objetos de entrada y salida, también recibe como parámetro el contexto runtime en el que se ejecuta la Lambda tales como Aws Request Id, Function Name, Function Version, Invoked Function Arn, Memory LimitIn MB, etc.
Empaquetado y Despliegue
Creación Artefacto Optimizado JVM
Para la creación solo es utilizar Maven o Quarkus CLI para construir el empaquete del artefacto, lanzando la instrucción correspondiente sobre la ruta raíz del proyecto:
Maven
Para el uso del AWS Java Runtime:
./mvnw clean package
Quarkus
quarkus build
Creación Artefacto Optimizado JVM
Maven
Para el uso de binarios AWS Linux (Ejecutable Nativo):
./mvnw clean package -Dnative
Es importante resaltar que la construcción del artefacto en modo nativo es costosa en términos de tiempo y recursos, puede emplear varios minutos en ello, por lo que la recomendación para desarrollos es dejar el proceso de creación y despliegue de este tipo de artefactos para entornos productivos y manejar la versión JVM para desarrollos y pruebas para entornos previos incluyendo local.
En caso de tener un entorno de desarrollo diferente a Linux, la recomendación es utilizar Docker como herramienta para que la extensión de Quarkus construya el artefacto nativo:
./mvnw package -Dnative -Dquarkus.native.container-build=true
Quarkus CLI
Para el uso de binarios AWS Linux (Ejecutable Nativo):
quarkus build --native
En caso de tener un entorno de desarrollo diferente a Linux, la recomendación es utilizar Docker como herramienta para que la extensión de Quarkus construya el artefacto nativo:
quarkus build --native -Dquarkus.native.container-build=true
Resultado Compilación
Esta acción genera una serie de artefactos o archivos dentro de la carpeta target del proyecto:
De los cuales destacamos los siguientes:
<<artefactId>>
+<<version>>
+".jar"
: Corresponde al empaquetado final del artefacto Quarkus construido.<<artefactId>>
+<<version>>
+"-runner.jar"
: Corresponde al empaquetado final del artefacto Quarkus versión nativa construido.function.zip
: Archivo de despliegue lambdamanage.sh
: Script que encapsula las llamadas a AWS CLIsam.jvm.yaml
: Archivo que define propiedades para local testing usando SAM CLIsam.native.yaml
: Archivo que define propiedades de la versión nativa para local testing usando SAM CLI
Estructura Empaquetado Lambda
El empaquetado function.zip
de la carpeta target contiene el código Lambda finalmente a desplegar en AWS y su estructura interna varía dependiendo el tipo de compilado generado:
- JVM Optimizado
- Binario Nativo
Creación de Role IAM
Antes de subir y desplegar nuestro código Quarkus Java Lambda, debemos antes crear o asignar un rol de ejecución a nuestro recurso en AWS para poder ejecutar este tipo de servicios. Para efectos de nuestras pruebas se otorgarán permisos de invocación de cualquier Lambda en nuestra cuenta:
Una vez se ha creado el rol y otorgado los permisos necesarios, se edita el archivo target/manage.sh para agregar esta propiedad al script antes de proceder a desplegar:
FUNCTIONNAME=LambdaQuarkus
HANDLER=io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
RUNTIME=java11
.
.
.
LAMBDAROLE_ARN="arn:aws:iam::0000000000:role/lambda-role"
Despliegue Artefacto Optimizado JVM
Para crear la función Lambda utilizando JVM, se lanza el siguiente comando estando en la raíz del proyecto:
sh target/manage.sh create
El archivo target/manage.sh es construido cada vez que se lanza el proceso de empaquetado, por consiguiente, la información del rol del paso anterior se tiene que agregar manualmente después de este proceso para poder realizar el proceso de creación de la función nuevamente; o la otra alternativa es enviar como parámetro este rol en el proceso de creación de la función:
LAMBDA_ROLE_ARN="arn:aws:iam::0000000000:role/lambda-role" sh target/manage.sh create
Despliegue Artefacto Nativo
Para la compilación del código java a un ejecutable nativo, se lanza el siguiente comando estando en la raíz del proyecto:
sh target/manage.sh native create
Una vez creada la función lambda, verificar que la variable de entorno DISABLE_SIGNAL_HANDLERS ha sido creada con la ejecución del script, de lo contrario crearla manualmente:
Ejecución Función Lambda
Ejecución Remota
Para la ejecución de la función sobre el entorno Cloud, en este caso AWS, se lanza el siguiente comando:
sh target/manage.sh invoke
El script de ejecución (manage.sh --payload file://payload.json) toma como payload de entrada el archivo payload.json localizado en la raíz del proyecto, este se puede modificar para customizar la entrada del servicio al objeto esperado.
aws lambda invoke response.txt \
${inputFormat} \
--function-name ${FUNCTION_NAME} \
--payload file://payload.json \
--log-type Tail \
--query 'LogResult' \
--output text
Output ejecución:
lambda-quarkus$ ./target/manage.sh invoke
Invoking function
++ aws lambda invoke response.txt --function-name LambdaQuarkusNative --payload file://payload.json --log-type Tail --query LogResult --output text
++ base64 --decode
START RequestId: de9f030c-4f56-43a8-84d3-e8edecc0eeee Version: $LATEST
2022-04-11 15:00:47,454 INFO [io.quarkus] (main) lambda-quarkus 1.0.0-SNAPSHOT native (powered by Quarkus 2.7.5.Final) started in 0.160s.
2022-04-11 15:00:47,472 INFO [io.quarkus] (main) Profile prod activated.
2022-04-11 15:00:47,472 INFO [io.quarkus] (main) Installed features: [amazon-lambda, cdi]
END RequestId: de9f030c-4f56-43a8-84d3-e8edecc0eeee
REPORT RequestId: de9f030c-4f56-43a8-84d3-e8edecc0eeee Duration: 5.66 ms Billed Duration: 374 ms Memory Size: 256 MB Max Memory Used: 53 MB Init Duration: 367.37 ms
"Hello Bill Roshi"
Igualmente, al crearse un log group para la Lambda en AWS con el despliegue de la misma mediante SAM, los logs de la aplicación pueden verse a través de CloudWatch:
Ejecución Local
Para la ejecución local de la función Lambda utilizamos la herramienta AWS SAM, la cual nos permite a través de líneas de comando la creación y manejo de aplicaciones Serverless de forma local:
sam local invoke --template target/sam.jvm.yaml --event payload.json
Output ejecución:
lambda-quarkus$ sam local invoke --template target/sam.jvm.yaml
Invoking io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest (java11)
Decompressing /home/onoriel/aws/sam/lambda-quarkus/target/function.zip
Skip pulling image and use local one: public.ecr.aws/sam/emulation-java11:rapid-1.43.0-x86_64.
Mounting /tmp/tmp15osbzmz as /var/task:ro,delegated inside runtime container
START RequestId: 56247499-d992-4639-926e-50ef462be871 Version: $LATEST
2022-04-07 13:02:17,771 INFO [io.quarkus] (main) lambda-quarkus 1.0.0-SNAPSHOT on JVM (powered by Quarkus 2.7.5.Final) started in 0.671s.
2022-04-07 13:02:17,773 INFO [io.quarkus] (main) Profile prod activated.
2022-04-07 13:02:17,773 INFO [io.quarkus] (main) Installed features: [amazon-lambda, cdi]
"Hello Vegeta"
END RequestId: 56247499-d992-4639-926e-50ef462be871
REPORT RequestId: 56247499-d992-4639-926e-50ef462be871 Init Duration: 0.15 ms Duration: 833.79 ms Billed Duration: 834 ms Memory Size: 256 MB Max Memory Used: 256 MB
Si la versión a ejecutar es nativa se utiliza el siguiente comando:
sam local invoke --template target/sam.native.yaml --event payload.json
Actualización Función Lambda
Una vez hechas las modificaciones en el código, se empaqueta la aplicación y se redespliega el artefacto sobre AWS cambiando el parámetro de entrada en el script de despliegue a update:
sh target/manage.sh update
Si existen problema para la actualización por versiones previas de la misma sobre los recursos de AWS una solución alternativa puede ser eliminando y creando nuevamente la función lambda del entorno:
sh target/manage.sh delete create
Conclusiones
En este artículo hemos visto como a partir del framework Quarkus podemos optimizar el ciclo de vida de nuestras aplicaciones Java con el objetivo de poder sacar el máximo rendimiento de las soluciones Serverless en la nube, como puede ser AWS y sus Lambda Functions. Con Quarkus se logra cerrar un poco más la brecha de desempeño que existe frente a otros lenguajes de programación otorgándole mejores tiempos de arranque, ejecutables más ligeros y consumo de recursos reducidos.
En próximas ediciones trataremos de recrear el trabajo de este capítulo, pero en diferentes plataformas Serverless para sacar más conclusiones y obtener más datos con qué comparar.
¡Síguenos en Twitter para estar al día de próximas entregas!