O blog da AWS

Otimização de custos usando Amazon Elastic Kubernetes Services (EKS) e AWS Graviton

Por Damián Rivas, Arquiteto de Soluções para o setor Corporativo na Argentina e Uruguai

 

Nas últimas décadas a arquitetura x86 assumiu a liderança em data centers, servindo como uma plataforma para a execução da grande maioria dos aplicações. Hoje, é cada vez mais comum encontrar aplicações executando em várias arquiteturas, onde ARM é apresentado como uma alternativa moderna em situações em que historicamente x86 era a única arquitetura viável. ARM conseguiu atrair interesse especial porque seus processadores oferecem desempenho comparável ou superior a seus pares x86, mas com gerenciamento de energia mais eficiente e custos reduzidos.

É importante ressaltar que nem todas as aplicações podem ser executadas em processadores baseados em ARM, a princípio isso cria uma limitação ao projetarmos aplicações e arquiteturas. No entanto, hoje o conceito de microsserviços e aplicações multicamadas é popular na modernização de aplicações monolíticas tradicionais. Esta abordagem nos permite dividir e criar componentes específicos da aplicação na forma de pequenos serviços, criando assim novas oportunidades quando se trata de construir seu design e arquitetura.

Se já temos um serviço rodando sobre a arquitetura x86 e sabendo que é compatível com ARM, seria viável migrar o serviço de forma controlada entre as diferentes arquiteturas de CPU sem afetar sua disponibilidade? A resposta é sim.

A tecnologia de contêineres se consolidou como a escolha ideal para a implementação de aplicações projetadas sob padrões de microsserviços. Ao propor uma alternativa ágil e portável,  também adicionam vários desafios quando se trata de fornecer disponibilidade, escalabilidade e elasticidade. É aí que os orquestradores entram em jogo para adicionar a camada de gerenciamento necessária para ambientes produtivos, sendo o Kubernetes o mais popular e mais adotado na comunidade.

O Amazon Elastic Kubernetes Service (Amazon EKS) oferece a flexibilidade de executar aplicações  Kubernetes na nuvem AWS. O Amazon EKS oferece clusters seguros e altamente disponíveis e automatiza tarefas importantes, como aplicação de patches, provisionamento de nós e upgrade de software.

O EKS oferece suporte à nós com processadores x86 (Intel e AMD), bem como nós com processadores ARM, chamados de AWS Graviton.

Os processadores Graviton foram criados pela AWS usando núcleos ARM Neoverse de 64 bits, oferecendo a melhor relação preço/desempenho para cargas de trabalho em nuvem executadas em instâncias EC2. É usado em instâncias T4g, M6g, C6g e R6g do Amazon EC2 e suas variantes com armazenamento SSD local baseado em NVMe. As instâncias oferecem relação preço/desempenho até 40% maior comparadas às atuais instâncias baseadas em x86 para uma ampla variedade de cargas de trabalho, incluindo servidores de aplicações, microsserviços, computação de alto desempenho, automação de design eletrônico, jogos, bancos de dados de código abertos e caches em memória.

Trabalhando com um cluster Amazon EKS e nós AWS Graviton

Neste artigo, exploraremos com um estudo de caso, como podemos aproveitar a integração entre o Amazon EKS usando nós do AWS Graviton para implantar serviços que suportam a arquitetura ARM com o objetivo de otimizar os custos. Finalmente, realizaremos uma comparação de desempenho de uma aplicação de exemplo em execução em ambas as arquiteturas.

Detalhes da aplicação

Para este exemplo, vamos trabalhar com uma aplicação web simples que consome informações de um banco de dados e executa uma série de operações matemáticas com números aleatórios. Ela foi projetada usando um modelo de três camadas com um serviço de frontend que usa Nginx, um serviço de back-end desenvolvido em NodeJS e um banco de dados NoSQL utilizando Amazon DynamoDB.

Os serviços de frontend e back-end operam em pods separados (unidade atômica do Kubernetes) em um cluster do EKS usando um conjunto de nós com processadores x86 (instâncias M5). Cada serviço tem 3 réplicas cada para distribuir a carga.

 

Arquitetura da aplicação implantada na AWS

Em seguida, vamos implantar a aplicação em nós Graviton.

Implantando aplicação em nós Graviton

Nesta aplicação de exemplo, tanto o frontend quanto o back-end podem ser migrados para nós com base na arquitetura do processador ARM. Neste caso específico, vamos replicar as implantações para garantir um teste controlado, evitando impactos no serviço em produção e sendo capaz de comparar o desempenho corretamente.

Para realizar a implantação que pretendemos, precisamos do seguinte:

  1. Ter uma versão de imagem dos contêineres compatíveis com ARM.
  2. Ter instâncias Graviton (ARM) em um pool de nós.
  3. Modificar a implantação dos serviços para que eles possam ser atribuídos aos nós suportados. Este último ponto é necessário para garantir que os pods sejam gerados e executados nos nós compatíveis com a arquitetura para a qual foram definidos, já que teremos os dois grupos de nós (x86 e Graviton) coexistindo enquanto fazemos os testes.

Criar versões compatíveis com ARM de imagens de contêiner de frontend e back-end

Para o primeiro ponto o que precisamos fazer é criar a imagem do contêiner que suporta a arquitetura do processador ARM. Para isso, temos várias alternativas, neste caso particular iremos provisionar uma instância Graviton do tipo t4g.small com o Amazon Linux, que usaremos para criar a imagem compatível. Nela, garantiremos que você tenha o Docker e a AWS CLI instalados.

Depois que a instância é iniciada, baixamos as fontes do aplicativo junto com os arquivos Dockerfiles. Nós editamos cada Dockerfile para usar as imagens base ARM para cada aplicativo. Para isso, você pode adicionar arm64v8/ na frente do nome da imagem base: arm64v8/nginx e arm64v8/node.  Por fim, geramos as novas imagens e as colocamos nos repositórios correspondentes para cada uma em ECR com a tag “arm”, a fim de diferenciar essa imagem da que originalmente tínhamos (criada para x86).
Comandos de exemplo para carregar imagens compatíveis com ARM no ECR:

aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <AccountID>.dkr.ecr.us-east-1.amazonaws.com
docker tag blog-app-front:latest <AccountID>.dkr.ecr.us-east-1.amazonaws.com/blog-app-front:arm
docker push <AccountID>.dkr.ecr.us-east-1.amazonaws.com/blog-app-front:arm docker tag blog-app-front:latest <AccountID>.dkr.ecr.us-east-1.amazonaws.com/blog-app-back:arm
docker push <AccountID>.dkr.ecr.us-east-1.amazonaws.com/blog-app-back:arm

Substitua <AccountID> pelo número de conta da AWS aplicável.

 
       

Repositórios ECR com imagens que suportam a arquitetura ARM

 

Como mencionei anteriormente, existem outros métodos alternativos para criar uma imagem de contêiner compatível com ARM, como a funcionalidade “buildx” do Docker com a qual as imagens podem ser geradas para várias arquiteturas ao mesmo tempo sem a necessidade de criar uma instância de processador específico compatível.

Criar novo Graviton Nodegroup e aplicar rótulos

Para o segundo ponto, criaremos um novo pool de nós com instâncias m6g. Quando os novos nós estiverem prontos (Status deve estar listado como “Ready”), vamos proceder e colocar rótulos (rótulos  ) para os nós de cada grupo, usando a tag arch = x86 para os nós originais e arch = arm  para os novos nós ARM.

Para este exemplo, criamos o pool de nós usando a linha de comando:

eksctl create nodegroup --cluster eks-demo --region us-east-1 --name
 graviton-nodes --node-type m6g.large --nodes 3 --nodes-min 1 --nodes-
max 3 --ssh-access --ssh-public-key sample-key --managed

Lista de nós exibidos a partir do console da Web

 

Para aplicar o rótulo, usamos os seguintes comandos:

kubectl label nodes <nodo> arch=arm (para nodos Graviton)
kubectl label nodes <nodo> arch=x86 (para los nodos originales)

 

Para verificar se os rótulos foram adicionados com êxito:

kubectl get nodes –show-labels 

Ou, eles podem ser consultados a partir do console web:

 

Marcas de exemplo para um nó de cada nodegroup visualizado a partir do console do EKS

 

Editar arquivos de configuração para implantações e serviços

Finalmente, para o terceiro e último ponto, modificaremos as configurações de nossas implantações e serviços, de tal forma que:

A) Dependendo do tipo de arquitetura do processador suportada, eles serão implantados nos nós correspondentes. Isso impede que o Kubernetes tente implantar pods em nós que não são compatíveis com a arquitetura do processador correspondente à imagem do contêiner. Para isso, usaremos o recurso NodeSelector dentro da seção de spec.

B) Os nomes de implantações e serviços são diferentes dos atuais para evitar conflitos com a implantação em produção atual usando x86.

A) Primeiro, criamos uma cópia dos arquivos de implantação e configuração de serviço. Em seguida, modificamos as implantações nessas cópias alterando os nomes (para não afetar a implantação em produção), adicionando os detalhes do NodeSelector e modificando a imagem do contêiner a ser usada nas especificações do pod para cada caso:

No caso do frontend:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-arm
…spec:  nodeSelector:   arch: arm  containers:
   - name: front
     image: “<repoECR>/blog-app-front:arm”
…

 

No caso do backend:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-arm
…spec:  nodeSelector:   arch: arm  containers:
   - name: back
     image: “<repoECR>/blog-app-back:arm”
…

 

Dessa forma, quando executamos as novas implantações, vamos nos certificar de fazê-lo em nós que são compatíveis com a tag marcada e que levam a imagem apropriada.

B) Agora modificaremos os nomes dos serviços para garantir o acesso a novas implantações no Graviton.

Para o back-end:

apiVersion: v1
kind: Servicemetadata:   name: flightsproc-arm
…

Para o frontend:

apiVersion: v1
kind: Servicemetadata:   name: frontend-arm
…

No caso específico desta aplicação, para respeitar a ideia de ter as implantações simultaneamente para fazer as comparações de desempenho, tivemos que modificar a configuração do Nginx para poder referenciar o novo nome do serviço e garantir que o frontend se comunique com o back-end correspondente à sua mesma arquitetura de processador.

Implantando o aplicativo em nós Graviton

Com todos os itens acima, estamos prontos para implantar o aplicativo usando as novas definições:

$ kubectl apply -f back-deployment-arm.yaml
$ kubectl apply -f back-service-arm.yaml
$ kubectl apply -f front-deployment-arm.yaml
$ kubectl apply -f front-service-arm.yaml

Consultamos as implantações para validar que está tudo bem:

$ kubectl get deployment
NAME           READY   UP-TO-DATE   AVAILABLE   AGE

backend        3/3     3            3           5d2h

backend-arm    3/3     3            3           1m

frontend       3/3     3            3           6d1h

frontend-arm   3/3     3            3           1m

Agora analisamos as informações dos serviços para que possamos testar o aplicativo em execução nos nós Graviton:

 

 

Tentamos acessar o aplicativo usando o valor indicado em EXTERNAL-IP para verificar se ele é acessado corretamente (pode levar alguns minutos para se tornar disponível):

 

 

Excelente! O aplicativo já está funcionando corretamente. Vamos rever como ficou a arquitetura:

 

 

Como podemos ver, comparando o diagrama inicial adicionamos um novo pool de nós com instâncias Graviton, implantamos as versões das aplicações e criamos uma instância T4G  como auxiliar para poder criar imagens de contêiner compatíveis com ARM e carregá-las para o repositório ECR com a tag arm. Para simplificar o diagrama, apenas duas setas são marcadas de dois pods de back-end para o DynamoDB, mas todas têm comunicação direta com a base de dados.

Em seguida, faremos uma análise e compararação de desempenho entre as aplicações de cada grupo.

Teste de desempenho

Os seguintes mecanismos e critérios são usados para medir o desempenho da implementação:

  1. A ferramenta de benchmarking da linha de comando Siege com a qual 10 conexões simultâneas serão simuladas com 20 repetições cada (200 transações no total) a partir de um único nó gerador de carga. O tempo medido em segundos para finalizar a simulação (Tempo decorrido) será tomado como um indicador.
  2. A solução Distributed Load Testing on AWS com a qual geraremos um teste de carga distribuído a partir de 10 nós diferentes, aumentando o número de conexões durante o primeiro minuto e mantendo essa carga de pico constante por mais 2 minutos. O tempo médio de resposta será o indicador principal.

Desempenho em x86:

Os testes mostram os seguintes valores:

Teste de carga com Siege:

A simulação levou 8,52 segundos para executar 200 transações

Teste de carga com Distributed Load Testing:

540694 transações foram executadas no total (18 falharam) em um tempo médio de 0.13882 segundos para cada

Desempenho em ARM:

Utilizaremos as mesmas ferramentas e critérios que foram utilizados para mensurar o desempenho da aplicação anteriormente em x86 a fim de ter uma comparação consistente.

Teste de carga com Siege:

A simulação levou 7,97 segundos para executar 200 transações

Testes de carga com Distributed Load Testing:

656238 transações foram executadas no total (12 falharam) em um tempo médio de 0.11260 segundos para cada

Comparando os resultados

Vamos comparar com os resultados obtidos nas medições iniciais:

Ferramenta Indicador x86 Indicador Graviton Variação
Siege 8.52 7.97 6%
Distributed Load Testing 0.13882 0.11260 19%

Com base nesta comparação pode ser observado:

  • Melhorias em tempo de execução de 6% usando um teste de carga de baixo estresse e de um único nó.
  • Melhorias da ordem de 19% usando um teste de carga distribuída (gerando maior estresse) em comparação com a execução em nós x86. É interessante ver que, neste segundo caso, graças ao menor atraso de tempo por transação, foi possível executar um número maior e com uma taxa de falha menor.

Custo

Agora vamos analisar rapidamente os custos envolvidos em cada pool de nós. Para simplificar, nos concentraremos apenas no custo dos nós de computação no modo sob demanda.

x86 Nós:

Tipo de instância
Número de nós
Custo mensal (USD) *
m5.large 3 210.24

Nós Graviton:

Tipo de instância
Número de nós
Custo mensal (USD) *
m6g.large 3 168.63

* Os custos são aqueles obtidos através da Calculadora Pública da AWS

Comparação:

Custo x86 Custo Graviton Variação
210.24 168.63 20%

Nesta comparação, podemos ver que há uma economia de 20% nos custos de computação ao usar nós Graviton.

Fechamento da migração

Já vimos a integração entre o Amazon EKS e o AWS Graviton em ação e como a plataforma nos permitiu, com pouco esforço, implantar a aplicação usando uma nova arquitetura do processador em paralelo para validar o seu bom funcionamento. Tendo comprovado a disponibilidade do serviço e atingido o objetivo de otimizar custos sem perda de performance, podemos proceder à eliminação dos pods e nós x86. Exemplo de comandos:

# Eliminar Pods desplegados y servicios para accederlos en x86
$ kubectl delete service frontend
$ kubectl delete deployment frontend
$ kubectl delete service backend
$ kubectl delete deployment backend

# Eliminar nodos (también puede hacerse desde la consola web de EKS)
$ eksctl delete nodegroup --cluster eks-demo --region us-east-1 --name ng-2ccb48d4 –approve

 

Depois de remover os nós originais, a arquitetura ficaria assim:

 

 

Ficaríamos com nosso cluster EKS com um grupo de nós Graviton e preservamos a instância auxiliar para trabalhar na atualização de imagem quando necessário. Fora disso, a Builder Instance pode permanecer desativada e ser ativada somente quando necessário.

E se os nós x86 ainda fossem úteis? Poderíamos eventualmente ter cargas de trabalho com arquiteturas de processador híbrido? Claro! Por que não?

Um fato que fomos capazes de comprovar ao longo do caminho é que o EKS pode manter em perfeita coexistência grupos de nós de diferentes arquiteturas de processadores sem qualquer inconveniente. Podemos, por exemplo, pensar nos seguintes cenários de implantação de aplicativos em um cluster com arquiteturas de processador híbrido:

  • Aplicações ou componentes de aplicações que não podem ser migrados para a arquitetura do processador ARM devido à falta de compatibilidade. Isso permite migrar aplicações ou componentes compatíveis com nós do AWS Graviton para otimizar custos, enquanto podemos manter componentes ou aplicações não compatíveis com ARM em execução em nós x86.
  • Um serviço comum balanceado entre pods executados em diferentes arquiteturas de processador. Muito comum em cenários onde você deseja experimentar o comportamento da aplicação de forma gradual.

Opinião

Percorrendo o caminho da migração e depois de fazer medições e calcular os custos dos nós de computação e compará-los com medições e cálculos de custos após migrar a aplicação para os nós Graviton, podemos ver que: Não só a aplicação migrou com sucesso sem afetar sua disponibilidade, mas também conseguiu otimizar os custos de computação em 20% combinado com uma melhoria de desempenho de até 19%.

Os custos podem continuar a ser otimizados, eventualmente, usando opções como Savings Plans para fazer reservas de computação e alcançar uma economia adicional de 40% dependendo das condições da reserva. Outra alternativa em prol da otimização de custos, se a aplicação permitir, são as instâncias spot com as quais você pode obter melhorias de custos de até 90% em relação aos custos sob demanda.

 

Este artigo foi traduzido do Blog da AWS em Espanhol.

 


Sobre o autor

Damián Rivas é arquiteto de soluções da Amazon Web Services para o setor corporativo na Argentina e Uruguai. Ele é responsável por orientar e ajudar as organizações em sua adoção de tecnologia em nuvem. Ele tem mais de 17 anos de experiência em tecnologia e é um guitarrista apaixonado por música.