Blog de Amazon Web Services (AWS)

Funciones de AWS Lambda con tecnología AWS Graviton2 ejecute sus funciones en ARM y obtenga una relación preciorendimiento hasta un 34% mejor

Por Danilo Poccia, Evangelista Principal en AWS

 

Muchos de nuestros clientes (como Fórmula Uno, Honeycomb, Intuit, SmugMug y Snap Inc.) utilizan el procesador AWS Graviton2 basado en la arquitectura ARM para sus tareas y disfrutan de una mejor relación precio/rendimiento. A partir del septiembre pasado podrá obtener los mismos beneficios para sus funciones AWS Lambda. Ahora usted podrá configurar funciones nuevas y existentes para que se ejecuten en procesadores x86 o ARM/Graviton2.

Con esta opción, podra ahorrar dinero de dos maneras:

  • En primer lugar, sus funciones se ejecutan de forma más eficiente gracias a la arquitectura Graviton2.
  • Segundo, paga menos por el tiempo de ejecución. De hecho, las funciones de Lambda con tecnología Graviton2 están diseñadas para ofrecer un rendimiento hasta un 19% mejor a un coste un 20% menor.

Con Lambda, se le cobrará según el número de solicitudes a cada función y la duración (el tiempo que tarda en ejecutarse el código) de cada una, con una granularidad de milisegundos. Para las funciones que utilizan la arquitectura ARM/Graviton2, los cargos por duración son un 20% inferiores a los precios actuales de x86. La misma reducción del 20% también se aplica a los cargos por duración de las funciones que utilizan Provisioned Concurrency.

Además de la reducción de precio, las funciones que utilizan la arquitectura ARM se benefician del rendimiento y la seguridad incorporadas en el procesador Graviton2. Las cargas de trabajo que utilizan subprocesos múltiples y multiprocesamiento, o que realizan muchas operaciones de E/S, pueden reducir el tiempo de ejecución y, en consecuencia, reducir aún más los costes. Esto resulta especialmente útil ahora que puedes usar funciones Lambda con hasta 10 GB de memoria y 6 vCPU. Por ejemplo, puede obtener un mejor rendimiento para backends web y móviles, microservicios y sistemas de procesamiento de datos.

Si sus funciones no utilizan binarios específicos a la arquitectura, incluso en sus librerías, puedes cambiar de una arquitectura a otra. Este suele ser el caso de muchas funciones que utilizan lenguajes interpretados como Node.js y Python o funciones compiladas en bytecode Java.

Todos los Lambda-runtimes creados sobre Amazon Linux 2, incluidos los custom-runtimes, son compatibles con ARM, con la excepción de Node.js 10 que ha llegado al final del soporte. Si utiliza binarios en sus funciones, será necesario recompilar el código para la arquitectura ARM. Las funciones empaquetadas como imágenes de contenedores deben crearse para la arquitectura (x86 o ARM) que van a utilizar.

Para medir la diferencia entre arquitecturas, puede crear dos versiones de una función, una para x86 y otra para ARM.

A continuación, puede enviar tráfico a la función mediante un alias utilizando diferentes pesos para distribuir el tráfico entre las dos versiones. En Amazon CloudWatch, las métricas de rendimiento se recopilan por versiones de función y puede consultar indicadores clave (como la duración) mediante estadísticas. A continuación, puede comparar, por ejemplo, la duración media y p99 entre las dos arquitecturas.

También puede utilizar versiones de función y alias ponderados para controlar el despliegue en producción. Por ejemplo, puede implementar la nueva versión en una pequeña cantidad de invocaciones (como el 1 por ciento) y, a continuación, aumentar hasta el 100 % para una implementación completa. Durante el despliegue, puede reducir el peso o ponerlo a cero si las métricas muestran algo sospechoso (como un aumento de errores).

Veamos cómo funciona esta nueva capacidad en la práctica con algunos ejemplos.

 

Cambio de arquitectura para funciones sin dependencias binarias

Cuando no hay dependencias en el código, cambiar la arquitectura de una función Lambda es como cambiar un interruptor. Por ejemplo, hace algún tiempo, creé una aplicación de cuestionarios con una función Lambda. Con esta aplicación, puedes hacer y responder preguntas mediante una API web. Utilizo una API HTTP de Amazon API Gateway para activar la función. Este es el código Node.js que incluye algunas preguntas de ejemplo al principio:

const questions = [
  {
    question:
      "Are there more synapses (nerve connections) in your brain or stars in our galaxy?",
    answers: [
      "More stars in our galaxy.",
      "More synapses (nerve connections) in your brain.",
      "They are about the same.",
    ],
    correctAnswer: 1,
  },
  {
    question:
      "Did Cleopatra live closer in time to the launch of the iPhone or to the building of the Giza pyramids?",
    answers: [
      "To the launch of the iPhone.",
      "To the building of the Giza pyramids.",
      "Cleopatra lived right in between those events.",
    ],
    correctAnswer: 0,
  },
  {
    question:
      "Did mammoths still roam the earth while the pyramids were being built?",
    answers: [
      "No, they were all exctint long before.",
      "Mammooths exctinction is estimated right about that time.",
      "Yes, some still survived at the time.",
    ],
    correctAnswer: 2,
  },
];
 
exports.handler = async (event) => {
  console.log(event);
 
  const method = event.requestContext.http.method;
  const path = event.requestContext.http.path;
  const splitPath = path.replace(/^\/+|\/+$/g, "").split("/");
 
  console.log(method, path, splitPath);
 
  var response = {
    statusCode: 200,
    body: "",
  };
 
  if (splitPath[0] == "questions") {
    if (splitPath.length == 1) {
      console.log(Object.keys(questions));
      response.body = JSON.stringify(Object.keys(questions));
    } else {
      const questionId = splitPath[1];
      const question = questions[questionId];
      if (question === undefined) {
        response = {
          statusCode: 404,
          body: JSON.stringify({ message: "Question not found" }),
        };
      } else {
        if (splitPath.length == 2) {
          const publicQuestion = {
            question: question.question,
            answers: question.answers.slice(),
          };
          response.body = JSON.stringify(publicQuestion);
        } else {
          const answerId = splitPath[2];
          if (answerId == question.correctAnswer) {
            response.body = JSON.stringify({ correct: true });
          } else {
            response.body = JSON.stringify({ correct: false });
          }
        }
      }
    }
  }
 
  return response;
};

Para iniciar el cuestionario, pido la lista de ID de preguntas. Para hacerlo, utilizo curl con un HTTP GET en el extremo /questions:

$ curl https://<api-id>.execute-api.us-east-1.amazonaws.com/questions
[
  "0",
  "1",
  "2"
]

A continuación, pido más información sobre una pregunta añadiendo el ID al punto final:

$ curl https://<api-id>.execute-api.us-east-1.amazonaws.com/questions/1
{
  "question": "Did Cleopatra live closer in time to the launch of the iPhone or to the building of the Giza pyramids?",
  "answers": [
    "To the launch of the iPhone.",
    "To the building of the Giza pyramids.",
    "Cleopatra lived right in between those events."
  ]
}

Tengo la intención de utilizar esta función en producción. Espero muchas invocaciones y busco opciones para optimizar mis costos. En la consola Lambda, veo que esta función utiliza la arquitectura x86_64.

 

 

Debido a que esta función no utiliza binarios, cambio la arquitectura a arm64 y me beneficio de los precios más bajos.

 

 

El cambio de arquitectura no cambia la forma en que se invoca la función ni comunica su respuesta. Esto significa que la integración con API Gateway, así como las integraciones con otras aplicaciones o herramientas, no se ven afectadas por este cambio y siguen funcionando como antes.

Continúo con mi cuestionario sin indicios de que la arquitectura utilizada para ejecutar el código ha cambiado en el backend. Respondo a la pregunta anterior añadiendo el número de la respuesta (empezando por cero) al punto final de la pregunta:

$ curl https://<api-id>.execute-api.us-east-1.amazonaws.com/questions/1/0
{
  "correct": true
}

¡Correcto! Cleopatra vivió más cerca del lanzamiento del iPhone que de la construcción de las pirámides de Giza. Mientras estoy asimilando esta información, me doy cuenta de que he completado la migración de la función a ARM y he optimizado mis costos.

 

Cambio de arquitectura para funciones empaquetadas mediante imágenes de contenedor

Cuando AWS introdujo la capacidad de empaquetar e implementar funciones de Lambda mediante imágenes de contenedores, hice una demostración con una función Node.js generando un archivo PDF con el módulo PDFKit. Veamos cómo migrar esta función a Arm.

Cada vez que se invoca, la función crea un nuevo correo PDF que contiene datos aleatorios generados por el módulo faker.js. El resultado de la función utiliza la sintaxis de Amazon API Gateway para devolver el archivo PDF mediante la codificación Base64. Para mayor comodidad, anexo el código (app.js) de la función a continuación:

const PDFDocument = require('pdfkit');
const faker = require('faker');
const getStream = require('get-stream');

exports.lambdaHandler = async (event) => {

    const doc = new PDFDocument()

    const randomName = faker.name.findName();

    doc.text(randomName, { align: 'right' });
    doc.text(faker.address.streetAddress(), { align: 'right' });
    doc.text(faker.address.secondaryAddress(), { align: 'right' });
    doc.text(faker.address.zipCode() + ' ' + faker.address.city(), { align: 'right' });
    doc.moveDown();
    doc.text('Dear ' + randomName + ',');
    doc.moveDown();
    for(let i = 0; i < 3; i++) {
        doc.text(faker.lorem.paragraph());
        doc.moveDown();
    }
    doc.text(faker.name.findName(), { align: 'right' });
    doc.end();

    pdfBuffer = await getStream.buffer(doc);
    pdfBase64 = pdfBuffer.toString('base64');

    const response = {
        statusCode: 200,
        headers: {
            'Content-Length': Buffer.byteLength(pdfBase64),
            'Content-Type': 'application/pdf',
            'Content-disposition': 'attachment;filename=test.pdf'
        },
        isBase64Encoded: true,
        body: pdfBase64
    };
    return response;
}

Para ejecutar este código, necesito los módulos pdfkit, faker y get-stream npm. Estos paquetes y sus versiones se describen en los archivos package.json y package-lock.json.

Actualizo la línea FROM en Dockerfile para usar una imagen base de AWS para Lambda para la arquitectura ARM. Dada la oportunidad, también actualizo la imagen para usar Node.js 14 (estaba usando Node.js 12 en ese momento). Este es el único cambio que necesito para cambiar de arquitectura.

FROM public.ecr.aws/lambda/nodejs:14-arm64
COPY app.js package*.json ./
RUN npm install
CMD [ "app.lambdaHandler" ]

Para los próximos pasos, sigo el post que mencioné anteriormente. Esta vez utilizo aleatorio-letter-arm para el nombre de la imagen del contenedor y para el nombre de la función Lambda. Primero, construyo la imagen:

$ docker build -t random-letter-arm .

A continuación, inspecciono la imagen para comprobar que está utilizando la arquitectura correcta:

$ docker inspect random-letter-arm | grep Architecture
"Architecture": "arm64",

Para asegurarme de que la función funciona con la nueva arquitectura, ejecuto el contenedor localmente.

$ docker run -p 9000:8080 random-letter-arm:latest

Debido a que la imagen del contenedor incluye el emulador de interfaz de tiempo de ejecución de Lambda, puedo probar la función localmente:

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'

¡Funciona! La respuesta es un documento JSON que contiene una respuesta codificada en base64 para API Gateway:

{
    "statusCode": 200,
    "headers": {
        "Content-Length": 2580,
        "Content-Type": "application/pdf",
        "Content-disposition": "attachment;filename=test.pdf"
    },
    "isBase64Encoded": true,
    "body": "..."
}

Confiado en que mi función Lambda funciona con la arquitectura arm64, creo un nuevo repositorio de Amazon Elastic Container Registry mediante la interfaz de línea de comandos (CLI) de AWS:

$ aws ecr create-repository --repository-name random-letter-arm --image-scanning-configuration scanOnPush=true

Etiqueto la imagen y la envío al repositorio:

$ docker tag random-letter-arm:latest 123412341234.dkr.ecr.us-east-1.amazonaws.com/random-letter-arm:latest
$ aws ecr get-login-password | docker login --username AWS --password-stdin 123412341234.dkr.ecr.us-east-1.amazonaws.com
$ docker push 123412341234.dkr.ecr.us-east-1.amazonaws.com/random-letter-arm:latest

En la consola de Lambda, creo la función de random-letter-arm y selecciono la opción para crear la función a partir de una imagen de contenedor.

 

 

Ingreso el nombre de la función, busco en mis repositorios ECR para seleccionar la imagen del contenedor aleatorio-letter-arm y elijo la arquitectura arm64.

 

 

Completo la creación de la función. A continuación, agrego API Gateway como desencadenador. Para simplificar, dejo abierta la autenticación de la API.

 

 

Ahora, hago clic en el punto final de la API varias veces y descargo algunos correos PDF generados con datos aleatorios:

 

 

La migración de esta función de Lambda a Arm ha finalizado. El proceso será diferente si tiene dependencias específicas que no admiten la arquitectura de destino. La capacidad de probar tu imagen de contenedor localmente te ayuda a encontrar y solucionar problemas en las primeras etapas del proceso.

 

Comparación de diferentes arquitecturas con versiones de funciones y alias

Para tener una función que haga un uso significativo de la CPU, utilizo el siguiente código Python. Calcula todos los números primos hasta un límite pasado como parámetro. No estoy usando el mejor algoritmo posible, ese sería el tamiz de Eratóstenes. Para tener más visibilidad, añado la arquitectura utilizada por la función a la respuesta de la función.

import json
import math
import platform
import timeit

def primes_up_to(n):
    primes = []
    for i in range(2, n+1):
        is_prime = True
        sqrt_i = math.isqrt(i)
        for p in primes:
            if p > sqrt_i:
                break
            if i % p == 0:
                is_prime = False
                break
        if is_prime:
            primes.append(i)
    return primes

def lambda_handler(event, context):
    start_time = timeit.default_timer()
    N = int(event['queryStringParameters']['max'])
    primes = primes_up_to(N)
    stop_time = timeit.default_timer()
    elapsed_time = stop_time - start_time

    response = {
        'machine': platform.machine(),
        'elapsed': elapsed_time,
        'message': 'There are {} prime numbers <= {}'.format(len(primes), N)
    }

    return {
        'statusCode': 200,
        'body': json.dumps(response)
}

Creo dos versiones de funciones utilizando arquitecturas diferentes.

 

 

Utilizo un alias ponderado con un peso del 50% en la versión x86 y un peso del 50% en la versión ARM para distribuir las invocaciones de manera uniforme. Al invocar la función a través de este alias, las dos versiones que se ejecutan en las dos arquitecturas diferentes se ejecutan con la misma probabilidad.

 

 

Creo un activador de API Gateway para el alias de la función y luego genero algo de carga usando algunos terminales en mi computadora portátil. Cada invocación calcula números primos hasta un millón. Puede ver en el resultado cómo se utilizan dos arquitecturas diferentes para ejecutar la función.

$ while True
  do
    curl https://<api-id>.execute-api.us-east-1.amazonaws.com/default/prime-numbers\?max\=1000000
  done

{"machine": "aarch64", "elapsed": 1.2595275060011772, "message": "There are 78498 prime numbers <= 1000000"}
{"machine": "aarch64", "elapsed": 1.2591725109996332, "message": "There are 78498 prime numbers <= 1000000"}
{"machine": "x86_64", "elapsed": 1.7200910530000328, "message": "There are 78498 prime numbers <= 1000000"}
{"machine": "x86_64", "elapsed": 1.6874686619994463, "message": "There are 78498 prime numbers <= 1000000"}
{"machine": "x86_64", "elapsed": 1.6865161940004327, "message": "There are 78498 prime numbers <= 1000000"}
{"machine": "aarch64", "elapsed": 1.2583248640003148, "message": "There are 78498 prime numbers <= 1000000"}
...

Durante estas ejecuciones, Lambda envía métricas a CloudWatch y la versión de la función (ExecutedVersion) se almacena como una de las dimensiones.

Para entender mejor lo que está sucediendo, creo un panel de control de CloudWatch para supervisar la duración de p99 para las dos arquitecturas. De esta manera, puedo comparar el rendimiento de los dos entornos para esta función y tomar una decisión informada sobre qué arquitectura utilizar en la producción.

 

 

Para esta tarea en particular, las funciones se ejecutan mucho más rápido en el procesador Graviton2, lo que proporciona una mejor experiencia de usuario y costes mucho más bajos.

 

Comparación de diferentes arquitecturas con Power Tuning de Lambda

El proyecto de código abierto AWS Lambda Power Tuning, creado por mi amigo Alex Casalboni, ejecuta sus funciones con diferentes configuraciones y sugiere una configuración para minimizar los costes y/o maximizar rendimiento. El proyecto se ha actualizado recientemente para permitirte comparar dos resultados en el mismo gráfico. Esto resulta útil para comparar dos versiones de la misma función, una con x86 y la otra ARM.

Por ejemplo, este gráfico compara los resultados x86 y ARM/Graviton2 para la función de calcular los números primos que usé anteriormente en la publicación:

 

 

La función utiliza un solo hilo. De hecho, la duración más baja para ambas arquitecturas se indica cuando la memoria está configurada con 1,8 GB. Por encima de eso, las funciones de Lambda tienen acceso a más de 1 vCPU, pero en este caso, la función no puede usar la potencia adicional. Por la misma razón, los costes son estables con una memoria de hasta 1,8 GB. Con más memoria, los costos aumentan porque no hay ventajas de rendimiento adicionales para esta carga de trabajo.

Miro el gráfico y configuro la función para usar 1,8 GB de memoria y la arquitectura Arm. El procesador Graviton2 ofrece claramente un mejor rendimiento y unos costes más bajos para esta función de cómputo intensivo.

 

Disponibilidad y precios

Puede utilizar las funciones Lambda con tecnología del procesador Graviton2 en la actualidad en EE.UU. Este (Norte de Virginia), EE.UU. Este (Ohio), EE.UU. Oeste (Oregón), Europa (Fráncfort), Europa (Irlanda), UE (Londres), Asia Pacífico (Bombay), Asia Pacífico (Singapur), Asia Pacífico (Sídney) y Asia Pacífico (Tokio).

ARM admite los siguientes tiempos de ejecución que se ejecutan sobre Amazon Linux 2:

  • js 12 y 14
  • Python 3.8 y 3.9
  • Java 8 (java8.al2) y 11
  • .NET Core 3.1
  • Rubí 2.7
  • Tiempo de ejecución personalizado (provided.al2)

Puede administrar las funciones de Lambda con la tecnología del procesador Graviton2 mediante el  modelo de aplicación serverless (SAM) de  AWS y el kit de desarrollo de la nube de AWS (AWS CDK). El soporte también está disponible a través de muchos socios de AWS Lambda, como AntStack, Check Point, Cloudwiry , Contino, Coralogix, Datadog, Lumigo, Pulumi, Slalom, Sumo Logic, Thundra y Xerris.

Las funciones Lambda que utilizan la arquitectura ARM/Graviton2 proporcionan una mejora de la relación precio/rendimiento de hasta un 34 por ciento. La reducción del 20 por ciento en los costes de duración también se aplica cuando se utiliza la Provisioned Concurrency. Puede reducir aún más sus costos hasta un 17 por ciento con los planes de ahorro. Las funciones de Lambda con tecnología Graviton2 se incluyen en la capa gratuita de AWS hasta los límites existentes. Para obtener más información, consulte la página de precios de AWS Lambda.

Puede encontrar ayuda para optimizar las cargas de trabajo del procesador AWS Graviton2 en el repositorio Introducción a AWS Graviton.

Empieza a ejecutar tus funciones Lambda en Arm hoy mismo.

Este artículo fue traducido del  Blog de AWS en Inglés .

 


Sobre el autor

Danilo Poccia trabaja con startups y empresas de cualquier tamaño para apoyar su innovación. En su función de evangelista principal (EMEA) en Amazon Web Services, aprovecha su experiencia para ayudar a las personas a dar vida a sus ideas, centrándose en las arquitecturas sin servidor y la programación basada en eventos, así como en el impacto técnico y empresarial del aprendizaje automático y la computación perimetral. Es autor de AWS Lambda in Action de Manning.

 

 

 

 

Traductores

Jorge González

Armando Barrales