O blog da AWS

Funções do AWS Lambda viabilizadas pelo AWS Graviton2 execute suas funções no ARM e obtenha uma relação preçodesempenho até 34% melhor

Por Danilo Poccia, Evangelista Principal na AWS

 

Muitos de nossos clientes (como Formula One, Honeycomb, Intuit, SmugMug e Snap Inc.) usam o processador AWS Graviton2 baseado em arquitetura ARM para suas tarefas e desfrutam de uma melhor relação preço/desempenho. A partir de Setembro de 2021, você poderá contar com os mesmos benefícios para suas funções AWS Lambda. Agora você pode configurar recursos novos e existentes para serem executados em processadores x86 ou ARM/Graviton2.

Com essa opção, você pode otimizar custos de duas maneiras:

  • Primeiro, suas funções são executadas de forma mais eficiente graças à arquitetura Graviton2.
  • Segundo, você paga menos pelo tempo de execução. Na verdade, as funções Lambda com a tecnologia Graviton2 são projetadas para oferecer um desempenho até 19% melhor, com um custo 20% menor

Com o AWS Lambda, você será cobrado com base no número de solicitações para cada função e na duração (o tempo que leva para o código ser executado) para cada função, com granularidade de milissegundos. Para recursos que usam a arquitetura ARM/Graviton2, as cobranças de duração de execução são 20% menores do que os preços x86 atuais. A mesma redução de 20% também se aplica a cobranças de duração de execução para recursos que usam concorrência provisionada.

Além da redução de preço, os recursos que usam a arquitetura ARM se beneficiam do desempenho e segurança incorporados ao processador Graviton2. As cargas de trabalho que usam multi-threading e multiprocessamento, ou que executam muitas operações de I/O, podem reduzir o tempo de execução e, consequentemente, reduzir ainda mais os custos. Isso é especialmente útil agora que você pode usar funções do Lambda com até 10 GB de memória e 6 vCPUs. Por exemplo, você pode obter melhor desempenho para back-ends web e móveis, microsserviços e sistemas de processamento de dados.

Se suas funções não usam binários que requerem uma arquitetura específica, incluindo suas dependências, você pode alternar de uma arquitetura para outra. Geralmente, esse é o caso de muitas funções que usam linguagens interpretadas, como Node.js e Python, ou funções compiladas em bytecode Java.

Todos os runtimes Lambda criados no Amazon Linux 2, incluindo runtimes personalizados, são compatíveis com o ARM, com exceção do Node.js 10 que já está no final do seu ciclo de suporte. Se houverem binários em suas funções, será necessário recompilar o código os mesmos para a arquitetura ARM. Funções empacotadas como imagens de contêiner devem ser criadas para a arquitetura (x86 ou ARM) escolhida.

Para medir a diferença entre arquiteturas, você pode criar duas versões de um recurso, uma para x86 e outra para ARM.
Em seguida, você pode enviar tráfego para o recurso usando um alias com pesos diferentes para distribuir o tráfego entre as duas versões. No Amazon CloudWatch, as métricas de desempenho são coletadas por versão de recurso, e você pode visualizar as principais métricas (como duração) usando estatísticas. Você pode então comparar, por exemplo, a duração média e p99 entre as duas arquiteturas.

Você também pode usar versões de função e balanceamento por peso para controlar a implementação em produção. Por exemplo, você pode utilizar a nova versão em um pequeno número de invocações (como 1%) e, em seguida, aumentar para 100% para uma utilização completa. Durante a implementação, você pode reduzir o peso ou redefini-lo para zero se as métricas mostrarem algo suspeito (como aumento de erros).

Vamos ver como esse novo recurso funciona na prática com alguns exemplos.

Mudança de arquitetura para funções sem dependências binárias

Quando não há dependências no código, alterar a arquitetura de uma função do Lambda é como mudar uma chave. Por exemplo, há algum tempo, criei um aplicativo de teste com uma função Lambda. Com este aplicativo, você pode fazer e responder perguntas usando uma API. Eu uso o API HTTP Amazon API Gateway para disponibilizar publicamente este recurso. Este é o código Node.js que inclui alguns exemplos de perguntas no início:

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 o questionário, peço a lista de IDs de perguntas. Para fazer isso, eu uso curl com um HTTP GET no final /questions:

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


Em seguida, peço mais informações sobre uma pergunta adicionando o ID ao ponto 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."
  ]
}

Como a ideia é utilizar esse recurso em produção. Espero receber muitas chamadas e procuro opções para otimizar meus custos. No console do Lambda, vejo que essa função usa a arquitetura x86_64.

 

 

Como essa função não usa binários, mudo a arquitetura para arm64 e me beneficio automaticamente dos preços mais baixos.

 

 

A mudança de arquitetura não muda a forma como a função é chamada ou mesmo sua resposta. Isso significa que a integração com o API Gateway, bem como as integrações com outros aplicativos ou ferramentas, não são afetadas por essa alteração e continuam funcionando como antes.
Eu continuo com meu questionário sem nenhuma indicação de que a arquitetura usada para executar o código mudou no back-end. Eu respondo à pergunta anterior adicionando o número da resposta (começando com zero) ao endpoint:

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


Está certo! Cleópatra viveu mais perto do lançamento do iPhone do que da construção das pirâmides de Gizé. Enquanto assimilo essas interessantes informações, percebo que concluí a migração do recurso para o ARM e ainda otimizei meus custos.

Mudança de arquitetura para funções empacotadas usando imagens de contêiner

Quando a AWS introduziu a capacidade de empacotar e implementar funções Lambda usando imagens de contêiner, fiz uma demonstração com uma função Node.js gerando um arquivo PDF com o módulo PDFKit. . Vamos ver como migrar esse recurso para o Arm.

Cada vez que é chamada, a função cria um novo e-mail PDF contendo dados aleatórios gerados pelo módulo faker.js. O resultado da função usa a sintaxe do Amazon API Gateway para retornar o arquivo PDF usando codificação Base64. Por conveniência, eu anexo o código (app.js) da função abaixo:

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 executar esse código, preciso dos módulos pdfkit, faker e get-stream npm. Esses pacotes e suas versões são descritos nos arquivos package.json e package-lock.json.

Eu atualizo a linha FROM no Dockerfile para usar uma imagem base da AWS para Lambda em arquitetura ARM. Dada a oportunidade, eu também atualizo a imagem para usar o Node.js 14 (eu estava usando o Node.js 12 na época). Essa é a única mudança que preciso para mudar a arquitetura.

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

Para os próximos passos, sigo o post que mencionei anteriormente. Desta vez, uso o aleatorio-letter-arm para o nome da imagem do contêiner e para o nome da função Lambda. Primeiro, eu construo a imagem:

$ docker build -t random-letter-arm .

Em seguida, inspeciono a imagem para verificar se ela está usando a arquitetura correta:

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

Para garantir que a função funcione com a nova arquitetura, executo o contêiner localmente.

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

Como a imagem do contêiner inclui o emulador de interface de runtime do Lambda, posso testar a função localmente:

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

Funciona! A resposta é um documento JSON que contém uma resposta codificada em base64 para o API Gateway:

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

Confiante de que minha função do Lambda funciona com a arquitetura arm64, crio um novo repositório do Amazon Elastic Container Registry usando a interface de linha de comando (CLI) da AWS:

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

Eu marquei a imagem e a envio para o repositório:

$ 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

No console do Lambda, crio a função random-letter-arm e seleciono a opção para criar a função a partir de uma imagem de contêiner

 

 

Eu insiro o nome da função, procuro meus repositórios ECR para selecionar a imagem do contêiner random-letter-arm e escolho a arquitetura arm64.

 

 

Concluída a criação da função. Em seguida, adiciono o API Gateway como um gatilho. Para simplificar, deixo a autenticação de API aberta.

 

 

Agora, clico no endpoint da API várias vezes e faço o download de alguns e-mails em PDF gerados com dados aleatórios:

 

 

A migração dessa função do Lambda para o Arm está concluída. O processo será diferente se houverem dependências específicas que não são suportadas pela arquitetura de destino. A capacidade de testar a imagem do contêiner localmente ajuda a encontrar e corrigir problemas já no início do processo.

Comparação de diferentes arquiteturas com versões de recursos e aliases

Para ter uma função que faz uso significativo da CPU, eu uso o seguinte código Python. Ele calcula todos os números primos até um limite passado como parâmetro. Não estou usando o melhor algoritmo possível, isso seria a peneira de Eratóstenes. Para maior visibilidade, adiciono a arquitetura usada pela função à resposta da função.

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)
}

Eu crio duas versões de funções usando arquiteturas diferentes.

 

 

Eu uso um alias com um peso de 50% na versão x86 e um peso de 50% na versão ARM para distribuir as invocações uniformemente. Ao invocar a função por meio desse alias, as duas versões executadas nas duas arquiteturas diferentes são executadas com a mesma probabilidade.

 

Eu crio um gatilho do API Gateway para o alias da função e, em seguida, gero carga usando alguns terminais no meu laptop. Cada requisição calcula números primos de até um milhão. Você pode ver na saída como duas arquiteturas diferentes são usadas para executar a função.

$ 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 essas execuções, o Lambda envia métricas para o CloudWatch e a versão da função (ExecutedVersion) é armazenada como uma das dimensões.

 

 

Para entender melhor o que está acontecendo, crio um painel do CloudWatch para monitorar a duração de p99 para as duas arquiteturas. Dessa forma, posso comparar o desempenho dos dois ambientes para esse recurso e tomar uma decisão informada sobre qual arquitetura usar na produção.

Para essa tarefa específica, as funções são executadas muito mais rapidamente no processador Graviton2, o que proporciona uma melhor experiência ao usuário final e custos muito mais baixos.

 

Comparação de diferentes arquiteturas com o Lambda Power Tuning

O projeto de código aberto AWS Lambda Power Tuning, criado pelo meu amigo Alex Casalboni, executa suas funções com diferentes configurações e sugere configurações para minimizar custos e/ou maximizar desempenho.
O projeto foi atualizado recentemente para permitir que você compare dois resultados no mesmo gráfico. Isso é útil para comparar duas versões do mesmo recurso, uma com x86 e outra ARM.
Por exemplo, este gráfico compara os resultados x86 e ARM/Graviton2 para a função de cálculo de números primos que usei anteriormente no post:

 

 

A função usa uma única thread. Na verdade, a duração mais baixa para ambas as arquiteturas é indicada quando a memória é configurada com 1,8 GB. Além disso, as funções do Lambda têm acesso a mais de 1 vCPU, mas, nesse caso, a função não pode usar a energia extra. Pelo mesmo motivo, os custos são estáveis com uma memória de até 1,8 GB. Com mais memória, os custos aumentam porque não há benefícios adicionais de desempenho para essa carga de trabalho.

Eu olho para o gráfico e configuro o recurso para usar 1,8 GB de memória e a arquitetura Arm. O processador Graviton2 oferece claramente melhor desempenho e custos mais baixos para essa função de computação intensiva.

 

Disponibilidade e preços

Você pode usar as funções Lambda com processador Graviton2 hoje nas regiões de EUA. Eastern (Norte da Virgínia), EUA East (Ohio), EUA Oeste (Oregon), Europa (Frankfurt), Europa (Irlanda), UE (Londres), Ásia-Pacífico (Bombaim), Ásia-Pacífico (Cingapura), Ásia-Pacífico (Sydney) e Ásia-Pacífico (Tóquio).

O ARM é compatível com os seguintes runtimes executados no Amazon Linux 2:

Você pode gerenciar funções do Lambda baseadas no processador Graviton2 usando o AWS Serverless Application Model (SAM) e o AWS Cloud Development Kit (AWS CDK). O suporte também está disponível por meio de muitos parceiros do AWS Lambda, como AntStack, Check Point, Cloudwiry , Continuum, Coralogix, Datadog, Lumigo, Pulumi, Slalom, Lógica de sumô, Thundra e Xerris.

As funções do Lambda usando a arquitetura ARM/Graviton2 oferecem uma melhoria de preço/desempenho de até 34%. A redução de 20% nos custos detempo de execução também se aplica ao usar a concorrência provisionada. Você pode reduzir ainda mais seus custos em até 17% com Savings Plans. As funções do Lambda usando Graviton2 estão incluídas no nível gratuito da AWS nos limites já existentes. Para obter mais informações, consulte a página de definição de preço do AWS Lambda.

O suporte a otimização de workloads em AWS Graviton2 pode ser visto no repositório Getting Started with AWS Graviton.

Comece a executar suas funções do Lambda no Arm hoje mesmo. 

 

Este artigo foi traduzido do Blog da AWS em Inglês.

 


Sobre o autor

Danilo Poccia trabalha com startups e empresas de diversos tamanhos para apoiar em sua inovação. Como evangelista sênior (EMEA) na Amazon Web Services, ele aproveita sua experiência para ajudar as pessoas a dar vida às suas ideias, concentrando-se em arquiteturas serverless e programação baseada em eventos, bem como o impacto técnico e comercial de Machine Learning e Edge Computing. Ele é o autor do AWS Lambda in Action da Manning.

 

 

 

 

 

Tradutores

Thiago Tietze

Jorge González