Blog de Amazon Web Services (AWS)
Optimización de costos utilizando Amazon Elastic Kubernetes Services (EKS) y AWS Graviton
Por Damián Rivas, Arquitecto de Soluciones para el sector Corporativo en Argentina y Uruguay
Durante las últimas décadas, la arquitectura de procesador x86 tomó el liderazgo en los centros de datos, sirviendo de plataforma para la ejecución de la gran mayoría de aplicaciones. En la actualidad, es cada vez más frecuente encontrarnos con aplicaciones ejecutándose en diversas arquitecturas, donde ARM se presenta como una alternativa moderna en situaciones donde históricamente x86 era la única arquitectura viable. ARM ha logrado atraer especial interés debido a que sus procesadores ofrecen un rendimiento comparable o superior a sus pares x86 pero con un manejo de energía más eficiente y costos reducidos.
Es importante destacar que no todas las aplicaciones pueden ejecutarse en procesadores basados en ARM, esto genera una limitante, en principio, a la hora de diseñar nuestras aplicaciones y arquitecturas. Sin embargo, hoy en día está popularizado el concepto de microservicios y aplicaciones multi-capa para modernizar las aplicaciones monolíticas tradicionales. Este acercamiento nos permite dividir y crear componentes específicos de la aplicación en forma de pequeños servicios, creando así nuevas oportunidades a la hora de construir el diseño y arquitectura de la misma.
Si ya disponemos de un servicio funcionando en una tecnología compatible con arquitectura x86 y sabiendo que la misma es compatible con ARM ¿Será factible realizar una migración del servicio en forma ordenada entre las diferentes arquitecturas de CPU, sin afectar su disponibilidad? La respuesta es sí.
Los contenedores se han posicionado como una tecnología ideal para el despliegue de aplicaciones diseñadas bajo patrones de microservicios. Si bien proponen una alternativa ágil y portable, también suponen varios desafíos a la hora de poder proveer disponibilidad, escalabilidad y elasticidad. Allí es donde entran en juego los orquestadores para agregar esa capa de gestión tan necesaria para ambientes productivos, siendo Kubernetes el más popular y de mayor adopción en la comunidad.
Amazon Elastic Kubernetes Service (Amazon EKS) le brinda la flexibilidad de iniciar, ejecutar y escalar aplicaciones de Kubernetes en la nube de AWS. Amazon EKS le proporciona clústeres seguros y de alta disponibilidad y automatiza tareas clave como los parches, el aprovisionamiento de nodos y las actualizaciones.
EKS provee soporte tanto para nodos con procesadores x86 (Intel y AMD), como así también para nodos con procesadores ARM, llamados AWS Graviton.
Los procesadores Graviton son creados por AWS utilizando núcleos ARM Neoverse de 64 bits, ofreciendo la mejor relación entre precio y rendimiento para cargas de trabajo en la nube que se ejecutan en Amazon EC2. Potencian las instancias T4g, M6g, C6g y R6g de Amazon EC2 y sus variantes con almacenamiento SSD local basado en NVMe. Las instancias ofrecen una relación entre precio y rendimiento de hasta un 40 % superior a la de instancias comparables actuales basadas en x86 para una amplia variedad de cargas de trabajo, que incluyen servidores de aplicaciones, microservicios, informática de alto rendimiento, automatización de diseño electrónico, juegos, bases de datos de código abierto y cachés en memoria.
Trabajando con un cluster Amazon EKS y nodos AWS Graviton
En este artículo vamos a explorar, con un caso práctico, cómo podemos aprovechar la integración entre Amazon EKS utilizando nodos AWS Graviton para poder desplegar servicios compatibles con arquitectura ARM y, de esta manera optimizar costos. Por último, realizaremos una comparación de rendimiento de la una aplicación de ejemplo ejecutándose en ambas arquitecturas.
Detalles de la aplicación
Para este ejemplo, vamos a trabajar con una aplicación web simple que consume información de una base de datos y realiza una serie de operaciones matemáticas con números aleatorios. Está diseñada utilizando un modelo de tres capas con un servicio de frontend que utiliza Nginx, un servicio de backend desarrollado en NodeJS y una base de datos NoSQL operando en Amazon DynamoDB.
Los servicios de frontend y backend operan en pods (unidad atómica de Kubernetes) separados dentro de un cluster EKS en un conjunto de nodos con procesadores x86 (instancias M5). Cada servicio cuenta con 3 réplicas cada uno para distribuir la carga.
A continuación, vamos a desplegar la aplicación en nodos Graviton.
Desplegando aplicación en nodos Graviton
En esta aplicación de ejemplo, tanto el frontend como el backend pueden ser migrados a nodos basados en arquitectura de procesador ARM. En este caso particular, vamos a replicar los despliegues a fin de garantizar una prueba ordenada, evitar impactos en el servicio de la aplicación productiva y poder hacer la comparativa de rendimiento correctamente.
Para llevar el despliegue que pretendemos a cabo necesitaremos lo siguiente:
- Tener una versión de la imagen de los contenedores compatibles con ARM.
- Disponer de instancias Graviton (ARM) en un grupo de nodos.
- Modificar el despliegue de los servicios para que puedan asignarse en nodos compatibles. Este último punto es para asegurarnos de que los pods se generen y ejecuten en los nodos compatibles con la arquitectura para la que fueron definidos, puesto que vamos a tener conviviendo los dos grupos de nodos (x86 y Graviton) mientras hacemos las pruebas.
Crear las versiones compatibles con ARM de las imágenes de los contenedores de frontend y backend
Para el primer punto, lo que debemos hacer es crear la imagen del contenedor compatible con arquitectura de procesador ARM. Para ello disponemos de varias alternativas, en este caso particular, vamos a aprovisionar una instancia Graviton del tipo t4g.small con Amazon Linux, la cual utilizaremos para crear la imagen compatible. En la misma nos aseguraremos de tener instalado Docker y AWS CLI.
Una vez desplegada la instancia, descargamos las fuentes de la aplicación junto con los Dockerfiles. Editamos cada archivo Dockerfile para que utilicen las imágenes de base ARM para cada aplicativo. Para esto, alcanza con agregar arm64v8/ delante del nombre de la imagen base: arm64v8/nginx y arm64v8/node. Finalmente, generamos las imágenes nuevas y las colocamos en los repositorios correspondientes a cada uno en ECR con el tag “arm” para poder diferenciar esta imagen de la que teníamos originalmente (armada para x86).
Ejemplo de comandos para subir las imágenes compatibles con ARM a 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 Reemplazar <AccountID> por el número de cuenta AWS que corresponda
Reemplazar por el número de cuenta AWS que corresponda
Como comentaba anteriormente, existen otros métodos alternativos para crear una imagen de contenedor compatible con ARM, como por ejemplo la funcionalidad “buildx” de Docker con la cual pueden generarse imágenes para múltiples arquitecturas a la vez sin la necesidad de crear una instancia específica con procesador compatible
Crear nuevo grupo de nodos Graviton y aplicar etiquetas
Para el segundo punto, vamos a crear un nuevo grupo de nodos con instancias m6g. Una vez listos los nuevos nodos (el Status debe figurar en “Ready”), procederemos a colocar etiquetas ( ) para los nodos de cada grupo, usando la etiqueta arch = x86 para los nodos originales y arch = arm para los nodos nuevos del tipo ARM.
Para este ejemplo, voy a crear el grupo de nodos utilizando la línea de comandos:
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
Para aplicar la etiqueta usamos los siguientes comandos:
kubectl label nodes <nodo> arch=arm (para nodos Graviton) kubectl label nodes <nodo> arch=x86 (para los nodos originales)
Para verificar que las etiquetas se agregaron correctamente:
kubectl get nodes –show-labels
O bien, pueden consultarse desde la consola web:
Editar los archivos de configuración para los despliegues y servicios
Finalmente, para el tercer y último punto vamos a modificar las configuraciones de nuestros despliegues y servicios, de manera tal que:
a. Dependiendo el tipo de arquitectura de procesador soportada, sean desplegados en los nodos correspondientes. De esta manera, evitamos que Kubernetes intente desplegar Pods en nodos que no sean compatibles con la arquitectura de procesador correspondiente a la imagen del contenedor. Para esto vamos a usar la característica nodeSelector dentro de la sección de spec.
b. Los nombres de despliegues y servicios sean diferentes a los actuales para evitar conflictos con el actual despliegue productivo en x86.
A) Primero, creamos una copia de los archivos de configuración de despliegues y servicios. Luego, modificamos en dichas copias los deployments cambiando los nombres (para no pisar el despliegue productivo), agregando el detalle de nodeSelector y modificando la imagen de contenedor a utilizar dentro de las especificaciones del pod para cada caso:
En el caso del frontend:
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-arm
…spec: nodeSelector: arch: arm containers:
- name: front
image: “<repoECR>/blog-app-front:arm”
…
En el caso del backend:
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-arm
…spec: nodeSelector: arch: arm containers:
- name: back
image: “<repoECR>/blog-app-back:arm”
…
De esta manera, cuando ejecutemos los nuevos despliegues, se va a asegurar de hacerlo en los nodos que sean compatibles con la etiqueta marcada y que tomen la imagen adecuada.
B) Ahora vamos a modificar los nombres de los servicios para asegurar el acceso a los nuevos despliegues en Graviton.
Para el backend:
apiVersion: v1 kind: Servicemetadata: name: flightsproc-arm …
Para el frontend:
apiVersion: v1 kind: Servicemetadata: name: frontend-arm …
En este caso particular de la aplicación utilizada y, para respetar la idea de tener los despliegues en simultáneo a fin de hacer las comparaciones de rendimiento correspondientes, se tuvo que modificar adicionalmente la configuración de Nginx para poder referenciar al nombre de servicio nuevo y asegurarnos que el frontend se comunique con el backend correspondiente a su misma arquitectura de procesador.
Despliegue de la aplicación en nodos Graviton
Con todo lo anterior, ya estamos listos para desplegar la aplicación utilizando las nuevas definiciones:
$ 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 los despliegues para validar que todo esté bien:
$ 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
Ahora revisamos la información de los servicios para poder probar la aplicación funcionando en los nodos Graviton:
Probamos acceder a la aplicación usando el valor indicado en EXTERNAL-IP para verificar que se acceda correctamente (puede demorarse unos minutos en estar disponible):
¡Excelente! La aplicación ya se encuentra funcionando correctamente. Repasemos cómo quedó la arquitectura:
Como podemos observar, comparando con el diagrama inicial, hemos sumado un nuevo grupo de nodos con instancias Graviton, desplegamos las versiones de la aplicación compatibles allí y creamos una instancia T4g como auxiliar para poder crear imágenes de contenedores compatibles con ARM y subirlas al mismo repositorio de ECR con la etiqueta arm. Para simplificar el diagrama, se marcan solamente dos flechas saliendo de dos Pods de Backend hacia DynamoDB pero todos ellos tienen comunicación directa con la base.
A continuación, vamos a hacer un análisis y comparativa de rendimiento de la aplicación en cada grupo.
Pruebas de rendimiento
Para medir el rendimiento de la aplicación se emplearan los siguientes mecanismos y criterios:
- La herramienta de benchmarking por línea de comando Siege con la cual se simularán 10 conexiones concurrentes con 20 repeticiones cada una (200 transacciones en total) desde un único nodo generador de carga. Se tomará como indicador el tiempo medido en segundos para terminar la simulación (Elapsed Time).
- La solución Distributed Load Testing on AWS con la cual generaremos una prueba de carga distribuida desde 10 nodos diferentes incrementando la cantidad de conexiones durante el primer minuto y manteniendo esta carga pico constante durante 2 minutos adicionales. Se tomará como indicador principal el tiempo promedio de respuesta.
Rendimiento en x86:
Las pruebas muestran los siguientes valores:
Prueba de carga con Siege:
Prueba de carga con Distributed Load Testing:
Rendimiento en ARM:
Vamos a utilizar las mismas herramientas y criterios que se usaron para medir la aplicación anteriormente en x86, a modo de disponer de una comparativa coherente.
Pruebas de carga con Siege:
Pruebas de carga con Distributed Load Testing:
Comparando los resultados
Comparemos con los resultados obtenidos en las mediciones iniciales:
Herramienta | Indicador x86 | Indicador Graviton | Variación |
Siege | 8.52 | 7.97 | 6% |
Distributed Load Testing | 0.13882 | 0.11260 | 19% |
En base a esta comparación se pueden observar:
- Mejoras de tiempo de ejecución en un 6% utilizando una prueba de carga con poco estrés y desde un único nodo
- Mejoras del orden del 19% utilizando una prueba de carga distribuida (generando mayor estrés) respecto a la ejecución en nodos x86. Es interesante ver que, en este segundo caso, gracias a demorar menos tiempo por transacción permitió ejecutar un mayor número de las mismas y con un índice de fallas menor.
Costo
Ahora vamos a analizar rápidamente los costos involucrados en cada grupo de nodos. Para simplificar vamos solamente a centrarnos en el costo de los nodos de cómputo en modalidad bajo demanda.
Nodos x86:
Tipo de instancia | Cantidad de nodos | Costo mensual (USD)* |
m5.large | 3 | 210.24 |
Nodos Graviton:
Tipo de instancia | Cantidad de nodos | Costo mensual (USD)* |
m6g.large | 3 | 168.63 |
* Los costos son los obtenidos a través de la calculadora pública de AWS
Comparativa:
Costo x86 | Costo Graviton | Variación |
210.24 | 168.63 | 20% |
En esta comparativa podemos observar que hay un ahorro del 20% en costos de cómputo al utilizar nodos Graviton.
Cierre de migración
Ya hemos visto en acción la integración entre Amazon EKS y AWS Graviton y cómo la plataforma nos permitió realizar un despliegue en paralelo, con poco esfuerzo, para validar el correcto funcionamiento de la aplicación en la nueva arquitectura de procesador. Habiendo corroborado la disponibilidad del servicio y alcanzado el objetivo de optimizar costos sin pérdida de performance, podemos proceder a eliminar los pods y los nodos x86. Ejemplo 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
Luego de remover los nodos originales, la arquitectura quedaría de esta manera:
Quedaríamos con nuestro cluster EKS con un grupo de nodos Graviton y preservaríamos la instancia auxiliar para trabajar en la actualización de imágenes cuando fuera necesario. Fuera de esto, la Builder Instance puede permanecer apagada y ser encendida sólo cuando se requiera.
¿Y si los nodos de x86 todavía tenían utilidad? ¿Podríamos tener eventualmente cargas de trabajo con arquitecturas de procesador híbridas? ¡Claro! ¿Por qué no?
Un dato no menor que hemos podido corroborar en todo este camino es que EKS puede mantener en perfecta convivencia grupos de nodos de arquitecturas de procesador diferentes sin ningún inconveniente. Podemos, por ejemplo, pensar los siguientes escenarios de desplegar aplicaciones en un cluster con arquitecturas de procesador híbridas:
- Aplicaciones o componentes de aplicación que no pueden migrarse a arquitectura de procesador ARM por falta de compatibilidad. De esta forma, pueden migrarse aplicaciones o componentes de aplicación compatibles a nodos AWS Graviton para optimizar costos, mientras que podemos mantener los componentes o aplicaciones no compatibles con ARM ejecutándose en nodos x86.
- Un servicio común que balancee entre Pods ejecutándose en diferentes arquitecturas de procesador. Muy común en escenarios donde se desea experimentar el comportamiento de la aplicación en una forma gradual.
Conclusiones
Recorriendo el camino de la migración y, luego de tomar las mediciones y calcular los costos de los nodos de cómputo iniciales y compararlas con las mediciones y cálculos de costos posteriores a migrar la aplicación a nodos Graviton, podemos observar que: se logró no solamente migrar la aplicación satisfactoriamente y sin afectar su disponibilidad sino que también se ha logrado optimizar costos de cómputo en un 20% en conjunto con una mejora de rendimiento de hasta el 19%.
Los costos pueden seguir optimizándose eventualmente utilizando opciones como Saving Plans para efectuar reservas de cómputo y alcanzar alrededor de hasta un 40% adicional de ahorro dependiendo las condiciones de reserva. Otra alternativa de optimización de costos, si la aplicación lo permite, son las instancias Spot con las que pueden obtenerse mejoras de costos de hasta 90% respecto de los costos bajo demanda.
Sobre el Autor
Damián Rivas es Arquitecto de Soluciones en Amazon Web Services para el sector Corporativo en Argentina y Uruguay. Es responsable de guiar y proveer ayuda a las organizaciones en su adopción tecnológica de la nube. Cuenta con más de 17 años de experiencia en tecnología y es un guitarrista apasionado por la música.