El objetivo de este proyecto es diseñar e implementar un sistema FaaS (Function as a Service), una solución que permita a los usuarios registrar y ejecutar funciones bajo demanda a través de una API REST.
Este sistema FaaS básico permite:
- Registro de usuarios: El proyecto permite registrar usuarios a través de basic auth de apisix.
- Registro de funciones: Los usuarios registran sus propias funciones (referencias a imágenes de Docker) de manera privada.
- Eliminación de funciones: Los usuarios pueden eliminar las funciones que previamente han registrado
- Ejecución de funciones: Las funciones se activan mediante llamadas API y deben aceptar un único parámetro de tipo cadena. Estas funciones corren dentro de contenedores que se eliminan automáticamente tras su ejecución.
El sistema no cuenta con un sistema de inicio de sesión tradicional. En su lugar, la autenticación se realiza mediante Basic Auth, gestionada por APISIX.
- Para realizar operaciones como registrar, eliminar o ejecutar una función, los usuarios deben incluir sus credenciales en la cabecera de cada petición.
- Este enfoque garantiza que solo los usuarios autenticados puedan interactuar con el sistema y realizar acciones.
Esto se ha gestionado mediante dos tipos diferentes de rutas en APISIX, que se detallarán más adelante en su correspondiente apartado.
- Docker
- Tener instalado Postman (Se pueden hacer con curl pero es más fácil y visual postman)
git clone https://github.com/zarlorcode/Proyecto-FaaS.git cd Proyecto-FasSSe lanzan a ejecución el proxy inverso Apisix, Nats, la API que ofrece el servicio y tantos workers como queramos tener, por defecto 1. En este ejemplo se lanzan 3 workers especificando con --scale.
docker compose up --build --scale worker=3Si quisieramos lanzar más workers en tiempo de ejecución podríamos ejecutar este comando en terminales distintas
docker run --rm --name worker -v /var/run/docker.sock:/var/run/docker.sock --network apisix worker-imageUna vez con el servicio lanzado:
- Abrimos postman
Hay que hacer una petición POST a la siguiente url:
http://localhost:9080/registerÚnicamente hay que navegar a la sección Auth, en tipo hay que seleccionar "Basic Auth" e introducir los credenciales ahí. Esto podría ir en el header también pero es más directo y visual si usamos el apartado Auth.
No hay que especificar nada en el body. A continuación, se pulsa Send.
{
"message": "Usuario registrado exitosamente",
"status": "success"
}{
"message": "El usuario ya existe",
"status": "error"
}Hay que hacer una petición POST a la siguiente url:
http://localhost:9080/functions/registerEn el apartado Auth nos tenemos que autenticar. Seleccionamos el tipo "Basic Auth" y usamos los credenciales que hemos registrado antes.
En el body ponemos lo siguiente:
{
"functionName": "reverse-string",
"dockerImage": "lzarpor/reverse-string-function"
}A continuación se pulsa Send.
Esta función se ha creado para realizar las pruebas del proyecto y lo que hace es darle la vuelta a los caracteres de una string. Es decir devuelve la string al revés.
{
"message": "Función registrada exitosamente",
"status": "success"
}{
"message": "la función ya está registrada",
"status": "error"
}Hay que hacer una petición POST a la siguiente url:
http://localhost:9080/functions/deregisterEn el apartado Auth nos tenemos que autenticar. Seleccionamos el tipo "Basic Auth" y usamos los credenciales que hemos registrado antes.
En el body ponemos lo siguiente:
{
"functionName": "reverse-string"
}A continuación se pulsa Send.
{
"message": "Función eliminada exitosamente",
"status": "success"
}{
"message": "función no encontrada",
"status": "error"
}Hay que hacer una petición POST a la siguiente url:
http://localhost:9080/functions/activateEn el apartado Auth nos tenemos que autenticar. Seleccionamos el tipo "Basic Auth" y usamos los credenciales que hemos registrado antes.
En el body ponemos lo siguiente:
{
"functionName": "reverse-string",
"parameter": "hello"
}{
"result": "olleh\n",
"status": "success"
}{
"message": "función no encontrada",
"status": "error"
}El sistema FaaS está compuesto por varios elementos clave que trabajan en conjunto para garantizar su funcionalidad y escalabilidad:
Apache APISIX es un proxy inverso que actúa como puerta de enlace para la aplicación. Este componente gestiona todas las solicitudes API y ofrece funciones esenciales para la operación segura y eficiente del sistema.
- Gestión de solicitudes API: Redirige las solicitudes entrantes al componente correspondiente dentro de la arquitectura.
- Autenticación y autorización: Configurable para verificar la identidad de los usuarios antes de conceder acceso a funciones específicas.
Para integrar APISIX, se ha utilizado su Dashboard como herramienta de configuración. La persistencia de las rutas se asegura almacenándolas en un volumen basado en etcd.
Se han definido dos rutas principales, cada una con un propósito específico:
Ruta abierta (RutaRegister):
- Descripción: Esta ruta es de acceso público, no requiere credenciales para ser utilizada.
- Propósito: Permitir el registro de nuevos usuarios en el sistema sin necesidad de autenticarse previamente.
- Prioridad: Tiene una prioridad más alta para asegurar que las solicitudes de registro siempre sean atendidas primero.
Ruta cerrada (Ruta1):
- Descripción: Esta ruta está protegida mediante autenticación básica (Basic Auth), requiriendo un nombre de usuario y una contraseña válidos.
- Propósito: Gestionar el resto de las operaciones del FaaS, tales como:
- Registro de funciones.
- Eliminación de funciones.
- Activación de funciones.
- Seguridad: Garantiza que solo los usuarios autenticados puedan realizar acciones críticas en el sistema.
Con esta configuración, APISIX asegura un control robusto sobre las solicitudes, diferenciando claramente entre las operaciones públicas y las protegidas.
NATS es un sistema de mensajería ligera que actúa como la columna vertebral de la comunicación entre los microservicios. Este componente desempeña un papel crucial en la arquitectura del sistema FaaS, facilitando la coordinación y el flujo de datos entre los distintos componentes.
- Cola de mensajes: Permite la comunicación asincrónica entre el servidor de la API y los workers.
- Almacenamiento clave-valor: Se utiliza como base de datos para almacenar información esencial del sistema. Por ejemplo:
- Bucket Key-Value:
- Key: Una tupla que combina el nombre del usuario y el nombre de la función.
- Value: La referencia a la imagen de Docker asociada a la función.
- Bucket Key-Value:
- Escalabilidad: Soporta múltiples conexiones simultáneas, manteniendo una comunicación eficiente incluso en entornos de alta carga.
- Propósito:
Este stream almacena las solicitudes de activación de funciones enviadas desde el servidor de la API. - Funcionamiento:
- Cada solicitud contiene:
- Nombre del usuario que activa la función.
- Nombre de la función registrada.
- Parámetro de entrada como una cadena de texto.
- Se genera un ID único (
requestID) para cada solicitud y se publica como un mensaje en el streamactivations.<requestID>. - Los workers actúan como consumidores de este stream, procesando las solicitudes en paralelo según la disponibilidad.
- Cada solicitud contiene:
- Propósito:
Este stream permite que los workers envíen los resultados de la ejecución de funciones de vuelta al servidor de la API. - Funcionamiento:
- Una vez que un worker completa la ejecución de una función, publica el resultado como un mensaje en el stream
results.<requestID>. - El servidor de la API escucha este stream y consume el resultado, devolviéndolo al usuario que activó la función.
- Una vez que un worker completa la ejecución de una función, publica el resultado como un mensaje en el stream
Consumidor del stream activations:
- Los workers están configurados como consumidores duraderos de este stream.
- Cada worker procesa mensajes, ejecuta la función especificada utilizando Docker y publica los resultados en el stream
results. - Para garantizar robustez, se configuran reintentos en caso de errores (por ejemplo, 5 intentos con tiempos de espera crecientes).
Procesamiento de Mensajes:
- El mensaje de activación se descompone en sus componentes (
username,functionName,parameter). - El worker ejecuta la función usando un contenedor Docker, asegurando que el resultado sea enviado a través de
stdout. - En caso de error, el worker publica un mensaje de error en el stream
results.
Consumidor del stream results:
- La API se suscribe a mensajes en
results.<requestID>. - Espera de forma síncrona hasta recibir el resultado, que es devuelto al usuario.
- Si no se recibe un mensaje dentro de un tiempo límite predefinido (por ejemplo, 40 segundos), se notifica un error de tiempo de espera.
Activación de la Función:
- La API publica un mensaje en
activations.<requestID>con los detalles de la solicitud.
Procesamiento por el Worker:
- Un worker disponible consume el mensaje, extrae los datos y ejecuta la función especificada mediante Docker.
- El resultado de la ejecución (o un error, si ocurre) se publica en
results.<requestID>.
Recepción del Resultado:
- La API escucha el mensaje de resultado en
results.<requestID>y lo procesa para devolverlo al usuario final.
-
Paralelismo y Escalabilidad:
- Los workers pueden escalar horizontalmente para manejar mayor carga.
- El uso de consumidores asegura que los mensajes sean procesados de manera eficiente sin interferencias.
-
Manejo de Errores:
- Los reintentos y el manejo de errores en los workers aseguran la fiabilidad del sistema incluso ante fallos puntuales.
El servidor de la API es el núcleo lógico de la aplicación. Sus funciones principales son:
- Gestión de usuarios:
- Registro de nuevos usuarios.
- Autenticación y autorización según el método implementado.
- Registro y gestión de funciones:
- Permite a los usuarios registrar, listar y eliminar funciones (referencias a imágenes de Docker).
- Verifica que solo los usuarios propietarios puedan gestionar sus funciones.
- Coordinación: Recibe solicitudes API, valida los parámetros y comunica las tareas a los trabajadores a través de NATS.
Los Workers son los encargados de procesar las funciones solicitadas por los usuarios, ejecutándolas en contenedores Docker y gestionando la comunicación con el sistema mediante NATS y JetStream.
Ejecución de Contenedores:
- Utilizan la imagen de Docker especificada por el usuario para crear y ejecutar un contenedor.
- Configuran el contenedor para recibir un único parámetro de tipo cadena, que es proporcionado en la solicitud de activación.
- Ejecutan la función y capturan tanto la salida (
stdout) como cualquier error (stderr).
Gestión de Mensajes:
- Los Workers están suscritos al stream
activationsde JetStream, donde reciben solicitudes para activar funciones. - Procesan las solicitudes, ejecutan las funciones, y publican los resultados en el stream
results.
Gestión de Recursos:
- Cada contenedor se elimina inmediatamente después de su ejecución para optimizar el uso de recursos.
- Los Workers pueden escalar horizontalmente, lo que permite manejar múltiples solicitudes simultáneamente en entornos de alta carga.
Conexión a NATS:
- Los Workers se conectan al servidor NATS, implementando lógica de reintento para garantizar la conexión incluso si el servidor no está disponible inicialmente.
Suscripción al Stream activations:
- Configuran un consumidor duradero para el stream
activations, lo que asegura que los mensajes no procesados persistan en caso de reinicios o fallos. - Cada mensaje contiene los datos necesarios para la activación:
- Usuario (
username) - Nombre de la función (
functionName) - Parámetro (
parameter)
- Usuario (
Procesamiento de Mensajes:
- Los Workers extraen los datos del mensaje y ejecutan la función especificada utilizando Docker.
- Los resultados (o errores) se publican en el stream
results.<requestID>.
Publicación de Resultados:
- La respuesta, que puede ser la salida de la función o un mensaje de error, se envía al servidor de la API mediante el stream
results.
-
Durabilidad:
Los consumidores son configurados como duraderos para asegurar la persistencia de los mensajes. -
Manejo de Errores:
- Cada mensaje tiene un máximo de 5 intentos de entrega en caso de fallos en su procesamiento.
- Se implementan intervalos de backoff entre intentos (por ejemplo, 5 y 10 segundos).
Recepción de Solicitud:
- Un mensaje es recibido desde
activations.<requestID>con los datos del usuario, función y parámetro.
Ejecución en Docker:
- El Worker ejecuta el siguiente comando:
docker run --rm <functionName> <parameter>
- Captura la salida estándar y cualquier error producido durante la ejecución.
Publicación del Resultado:
- Si la ejecución es exitosa, el resultado es publicado en
results.<requestID>. - En caso de error, un mensaje de error detallado es enviado al mismo stream.
-
Escalabilidad:
- El sistema permite añadir más Workers según sea necesario, distribuyendo la carga y mejorando el rendimiento.
-
Resiliencia:
- El manejo de errores y los consumidores duraderos aseguran que las solicitudes no se pierdan incluso en condiciones adversas.
-
Eficiencia en la Gestión de Recursos:
- Los contenedores son eliminados inmediatamente después de la ejecución, minimizando el uso de recursos.
El siguiente fragmento de código representa la implementación de un Worker:
// Fragmento destacado del Worker
func processFunction(workerMsgsId, functionName, parameter string) (string, error) {
log.Printf("[%s] PROCESANDO la función %s", workerMsgsId, functionName)
cmd := exec.Command("docker", "run", "--rm", functionName, parameter)
var out, stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return "", fmt.Errorf("error ejecutando la función: %s", stderr.String())
}
return out.String(), nil
}El sistema se organiza en una arquitectura de microservicios compuesta por los siguientes componentes:
- APISIX: Un proxy inverso que gestiona la entrada de solicitudes API y autentica usuarios.
- NATS: Un sistema de mensajería que conecta los componentes de manera asincrónica.
- API Server: El núcleo lógico que coordina las operaciones del sistema.
- Workers: Procesan las funciones registradas ejecutándolas en contenedores Docker.
- Los Workers consumen mensajes del stream
activationsen NATS, que contienen los detalles de las solicitudes. - Ejecutan funciones en contenedores Docker, procesando un único parámetro de entrada y capturando el resultado de la ejecución.
- Publican los resultados en el stream
resultspara que el servidor de la API pueda enviarlos al usuario.
- Los Workers pueden escalarse horizontalmente añadiendo más instancias. Esto se puede hacer al iniciar el sistema con
--scale worker=Nen Docker Compose o lanzándolos manualmente con un comando de Docker. - El uso de NATS como sistema de mensajería asegura que los nuevos Workers se integren automáticamente y comiencen a procesar tareas pendientes.
- La base de datos se implementa utilizando el almacenamiento de clave-valor de NATS (JetStream). Esto permite que los datos se distribuyan y repliquen en entornos de alta disponibilidad.
- NATS garantiza que las claves estén disponibles de manera eficiente incluso bajo alta carga.
- Workers: Se pueden añadir o quitar dinámicamente para manejar incrementos o disminuciones en la carga de trabajo.
- Streams en NATS: JetStream maneja colas y persistencia de mensajes, lo que permite absorber picos de solicitudes.
- APISIX: Configurable para manejar el equilibrio de carga y proporcionar autenticación segura.
- Cada función registrada hace referencia a una imagen de Docker personalizada que se ejecuta en un contenedor aislado.
- Los Workers son responsables de configurar y ejecutar estas imágenes de forma segura, sin exponer información sensible ni requerir acceso directo a los servicios externos desde el sistema central.
- Escalabilidad: Los componentes del sistema, como los Workers y NATS, son altamente escalables.
- Modularidad: La arquitectura basada en microservicios permite un mantenimiento y desarrollo más simples.
- Eficiencia: La eliminación de contenedores tras la ejecución asegura un uso óptimo de los recursos.
- Latencia: La comunicación asincrónica y la creación de contenedores puede introducir retrasos en comparación con soluciones sin contenedores.
- Complejidad Operativa: Requiere experiencia en orquestación de contenedores y configuración de sistemas distribuidos.
- Timestamp de creación de la solicitud: Para facilitar el monitoreo y diagnóstico de problemas.
- Identificador de región o servidor: Para implementar estrategias de despliegue multi-región.
- Metadatos de la función: Por ejemplo, la versión de la función o parámetros de configuración adicionales.
- Seguridad: El uso de contenedores garantiza un entorno aislado para ejecutar las funciones, minimizando riesgos de seguridad.
- Monitoreo: El sistema puede mejorarse añadiendo herramientas de monitoreo como Prometheus para supervisar el rendimiento de los componentes.
- Timeouts configurables: Los tiempos de espera son críticos para evitar que las solicitudes queden pendientes indefinidamente.
| Característica | Este Sistema | OpenFaaS | Knative |
|---|---|---|---|
| Escalabilidad | Manual/Docker Compose | Automática mediante K8s | Automática mediante K8s |
| Almacenamiento de Estado | NATS (JetStream) | Configurable (DB/Filesystem) | Configurable (DB/Filesystem) |
| Complejidad de Configuración | Moderada | Alta | Muy Alta |
| Recursos por ejecución | Docker (por función) | Docker (por función) | Kubernetes Pod |