Bajando de Serverless a Kubernetes y ahorrando +$43K en AWS

Cambiando de una arquitectura Serverless a una arquitectura basada en Kubernetes y ahorrando +$43K dólares en AWS.

Serverless & Contenedores
Serverless & Contenedores

Antes de empezar, un poco de contexto. La infraestructura está alojada en AWS y la arquitectura estaba basada en servicios Serverless:

  • API Gateway
  • Lambdas (Corrían un servicio de Node bastante simple que recibía una serie de parámetros, los serializaba e insertaba en una base de datos)

Para base de datos se está utilizando RDS con MySQL. La arquitectura a grosso modo se veía tal que así:

Arquitectura Serverless
Arquitectura Serverless

Esta arquitectura tenía tres problemas grandes:

  1. Debido al volumen de peticiones constantes que estábamos recibiendo, el Nº de invocaciones en simultáneo de las Lambdas se nos quedaba corto. Incluso con el incremento del límite a 10K tocábamos techo (por defecto son 1K).
  2. Por la confección de la Lambda en Node, en el RDS se abrían un Nº muy alto de conexiones, lo que obligaba a subir en recursos el RDS ya que utilizábamos una cantidad de RAM enorme (llegamos a tener una instancia db.r6i.4xlarge).
  3. El coste de mantener esto era enorme. En solo una semana se llegó a tener un coste en Lambdas de +$6.5K y AWS daba una previsión de gasto de +$36K. Y todo esto sin contar el dineral de gasto que estaba teniendo el RDS.
Coste de Lambda en AWS
Coste de Lambda en AWS

Cambiando la arquitectura

Una vez detectados los problemas y cuellos de botella de la arquitectura actual teníamos que pensar como reducir el coste drásticamente y además ser capaces de atender correctamente todo este tráfico:

Uno de los últimos picos de llamadas al API GW

Barajamos la posibilidad de añadir algún sistema de colas de forma intermedia que nos permitiese invocar Lambdas solo en momentos puntuales pero nuestro volumen no solo fluctúa en picos sino que es constante, 24/7. Y justo uno de los problemas del Serverless es que es muy caro a la hora de atender peticiones de forma recurrente y con un volumen tan alto.

Dado que además la función Lambda era un servicio en Node bastante simple y su refactor para bajarlo a una API no nos llevaría tiempo, decidimos cambiar la arquitectura completa. Para ello:

  • Como punto de entrada utilizamos ALB (Application Load Balancer)
  • Aprovechamos nuestros clústers de Kubernetes (EKS + Autoscaling Groups) que ya corrían otros servicios.
  • En cuanto al servicio de Node, decidimos dividirlo en 2 servicios:
    • Una API reescrita en Python con FastAPI que recibía las peticiones y las añadía en un servicio de streaming.
    • Un Worker que leía del servicio de streaming e insertaba por lotes los eventos en la base de datos para no saturarla.
  • Para el servicio de Streaming vimos varias alternativas pero finalmente optamos por Redis Streams, el cual podíamos, mas adelante, utilizar como caché para otros servicios. Este lo alojamos en Elasticache.
  • Finalmente para la base de datos seguimos en RDS con MySQL ya que esta contiene varios TB de datos.
Arquitectura con Kubernetes

Algunas cosas a destacar e interesantes en la configuración actual de Kubernetes:

  • Se han desplegado 2 deployments, uno para la API y otro para el Worker. Tienen un HPA (Horizontal Pod Autoscaling) de:
    • API: mínimo 10 Pods y máximo 20 Pods.
    • Worker: mínimo 5 Pods y máximo 1o Pods.
  • El número de nodos está gestionado por Autoscaler, aunque los límites están entre 1 - 10 nodos dependiendo de la demanda. Por el momento lo habitual parece ser un promedio de ~5-6 nodos.

Con esta nueva arquitectura ya éramos capaces de soportar el tráfico que teníamos. Además hemos solventado dos grandes problemas:

  • Los límites de invocaciones recurrentes de las Lambdas. Con Kubernetes tenemos más capacidad de autoescalado.
  • La escritura en base de datos al ser ahora por lotes en lugar de directamente, hizo que cayera en picado el Nº de conexiones recurrentes, que a su vez se tradujo en una demanda mucho menor de recursos:
Conexiones simultáneas al RDS

Show me the money!

Tras poco mas de una semana con todo funcionando con la nueva arquitectura, ya podemos empezar a sacar varias conclusiones y sobre todo a hacer números de cuánto estamos ahorrando.

Infraestructura Serverless + RDS:

  • API Gateway: +$1.2K con una previsión de gasto de +$7.2K
  • Lambdas: +$6.5K con una previsión de gasto en el mes de +$36K.
  • RDS: con una instancia tipo db.r6i.4xlarge la previsión de coste mensual era de +$1.5K

🔥 Total previsto al mes: +$45K

Infraestructura en K8s + Elasticache + RDS:

  • Application Load Balancer: +$160, previsión al mes de ~$650
  • Computo:
    • EC2: +$130 con una previsión pensando que el cluster siempre tiene el nº máximo de nodos: +$800
    • EKS: +$103 con una previsión de +$300
  • Redis en Elasticache: Cluster con t3.medium con una previsión de gasto de +$103.
  • RDS con MySQL: De momento hemos reducido a una db.r6g.xlarge que tiene una previsión de gasto mensual de +$350

🤑 Total previsto al mes: ~$2K

El ahorro previsto por este cambio de arquitectura es de ~$43K al mes. De hecho posiblemente incluso podamos ahorrar aún más porque aún seguimos evaluando si podemos bajar en recursos alguno de los nodos, tenemos que verificar si podemos ajustar los tipos de instancias y comprobar finalmente Saving Plans, reservas de instancias, etc.

Pero esto, posiblemente, sea ya para otro post 👋