Un momento divertido (a las 38:50) sucedió durante la sesión de Tim Bray ( SRV306 ) en re:invent 2017, cuando preguntó a la audiencia si deberíamos tener muchas funciones simples con un solo propósito o menos funciones monolíticas, y la sala estaba bastante mucho partido por la mitad.
Habiendo sido educado en los principios SOLID, y especialmente en el principio de responsabilidad única (SRP), este fue un momento que desafió mi creencia de que seguir el SRP en el mundo sin servidor es una obviedad.
Eso motivó este examen más detenido de los argumentos de ambos lados.
Divulgación completa, estoy sesgado en este debate. Si encuentra fallas en mi pensamiento, o simplemente no está de acuerdo con mis puntos de vista, indíquelo en los comentarios.
actualización 28/01/18: como señaló Quentin Ventura en los comentarios, no es inmediatamente obvio para el lector cómo organizo mi base de código, por lo que no está claro el contexto desde el que estoy discutiendo estos dos enfoques, así que los estoy presentando aquí.
Utilizo el marco Serverless
y todas las funciones de un servicio se organizarían en el mismo repositorio, por lo que compartir código dentro del repositorio se realiza a través de módulos compartidos.
Y para cada repositorio, hay un serverless.yml
que configura todas las funciones con fuentes de eventos relevantes, etc. y todas las funciones se implementan juntas cuando ejecutamos sls deploy
.
Cada repositorio también tendría un script build.sh
que encapsula los diferentes pasos de compilación para simplificar CI/CD y hacer que los pasos de compilación sean ejecutables localmente e independientemente de la herramienta de CI; las diferentes herramientas de CI tienen su propio requisito para un archivo yml, pero ser un simple de una sola línea para invocar nuestro propio script build.sh
.
Entonces, un repositorio típico se vería así:
Por "funciones monolíticas", me refiero a funciones que tienen una lógica de bifurcación interna basada en el evento de invocación y pueden hacer una de varias cosas.
Por ejemplo, puede hacer que una función maneje varios puntos finales y métodos HTTP y realice acciones diferentes según la path
y el method
.
module.exports.handler = (evento, contexto, cb) => {const path = event.path;const method = event.httpMethod;if (path === '/user' && method === 'GET') { .. // obtener usuario} else if (ruta === '/usuario' && método === 'ELIMINAR') {.. // eliminar usuario} else if (ruta === '/usuario' && método == = 'POST') {.. // crear usuario} else if ... // otros puntos finales y métodos}
No se puede razonar racionalmente y comparar soluciones sin comprender primero el problema y qué cualidades se desean más en una solución.
Y cuando escucho quejas como:
tener tantas funciones es difícil de manejar
Inmediatamente me pregunto qué implica administrar . ¿Es para encontrar funciones específicas que estás buscando? ¿Es para descubrir qué funciones tiene? ¿Esto se convierte en un problema cuando tienes 10 funciones o 100 funciones? ¿O se convierte en un problema solo cuando tienes más desarrolladores trabajando en ellos de los que puedes seguir?
A partir de mis propias experiencias, el problema con el que nos enfrentamos tiene menos que ver con las funciones que tenemos, sino con las características y capacidades que poseemos a través de estas funciones.
Después de todo, una función Lambda, como un contenedor Docker o un servidor EC2, es solo un conducto para brindar alguna función o capacidad comercial que necesita.
No preguntarías "¿Tenemos una _get-user-by-facebook-id_
?" ya que necesitará saber cómo se llama la función sin siquiera saber si la capacidad existe y si es capturada por una función Lambda. En su lugar, probablemente preguntaría "¿Tenemos una función Lambda que pueda encontrar un usuario en función de su ID de Facebook?" .
Entonces, el problema real es que, dado que tenemos un sistema complejo que consta de muchas características y capacidades, que es mantenido por muchos equipos de desarrolladores, ¿cómo organizamos estas características y capacidades en funciones Lambda para que esté optimizado para ...
Estas son las cualidades que son más importantes para mí. Con este conocimiento, puedo comparar los 2 enfoques y ver cuál es el más adecuado para mí .
Es posible que le interesen diferentes cualidades, por ejemplo, es posible que no le importe escalar el equipo, pero realmente le preocupa el costo de ejecutar su arquitectura sin servidor. Sea lo que sea, creo que siempre es útil hacer que esos objetivos de diseño sean explícitos y asegurarse de que su equipo los comparta y los entienda (¡tal vez incluso los acepte!).
Según Simon Wardley, la capacidad de descubrimiento no es un problema nuevo, es bastante común tanto en el gobierno como en el sector privado, y la mayoría de las organizaciones carecen de una forma sistemática para que los equipos compartan y descubran el trabajo de los demás.
cortesía de las publicaciones de Simon Wardley en Twitter
Como se mencionó anteriormente, lo importante aquí es la capacidad de averiguar qué capacidades están disponibles a través de sus funciones, en lugar de qué funciones hay allí.
No preguntes qué funciones tienes, qué pueden hacer tus funciones.
Un argumento que escucho a menudo para las funciones monolíticas es que reduce el no. de funciones, lo que las hace más fáciles de manejar.
En la superficie, esto parece tener sentido. Pero cuanto más lo pienso más me llama la atención que el no. de función solo sería un impedimento para nuestra capacidad de administrar nuestras funciones Lambda SI tratamos de administrarlas a mano en lugar de usar las herramientas disponibles para nosotros.
Después de todo, si somos capaces de ubicar libros por su contenido ( "encuéntrame libros sobre el tema de X" ) en un espacio físico enorme con decenas de miles de libros, ¿cómo podemos luchar para encontrar funciones Lambda cuando hay tantos? herramientas disponibles para nosotros?
¡Bibliotecas, sí, todavía existen!
Con una convención de nomenclatura simple, como la que impone el marco Serverless
, podemos encontrar rápidamente funciones relacionadas por prefijo.
Por ejemplo, si quiero encontrar todas las funciones que forman parte de nuestra API de usuario, puedo hacerlo buscando user-api
.
Con el etiquetado, también podemos catalogar funciones en múltiples dimensiones, como el entorno, el nombre de la función, el tipo de origen del evento, el nombre del autor, etc.
De forma predeterminada, el marco Serverless agrega la etiqueta STAGE a todas sus funciones. También puede agregar sus propias etiquetas, consulte la documentación sobre cómo agregar etiquetas.
La consola de administración de Lambda también le brinda una práctica lista desplegable de los valores disponibles cuando intenta buscar por etiqueta.
Si tiene una idea aproximada de lo que está buscando, entonces el no. de funciones no va a ser un impedimento para su capacidad de descubrir lo que hay.
Por otro lado, las capacidades de la API de usuario son inmediatamente obvias con funciones de propósito único, donde puedo ver en las funciones relevantes que tengo las capacidades CRUD básicas porque hay funciones correspondientes para cada una.
Puedo ver qué capacidades tengo como parte del conjunto de funciones que componen la función de API de usuario.
Sin embargo, con una función monolítica, no es inmediatamente obvio, y tendré que mirar el código yo mismo o consultar con el autor de la función, lo que para mí hace que la capacidad de descubrimiento sea bastante pobre.
Debido a esto, marcaré el enfoque monolítico en la capacidad de descubrimiento.
Sin embargo, tener más funciones significa que hay más páginas por las que puede desplazarse si solo desea explorar qué funciones hay en lugar de buscar algo específico.
Aunque, en mi experiencia, con todas las funciones bien agrupadas por prefijo de nombre gracias a la convención de nomenclatura que impone el marco Serverless, en realidad es bastante bueno ver lo que puede hacer cada grupo de funciones en lugar de tener que adivinar qué sucede dentro de un monolítico. función.
Pero supongo que puede ser una molestia desplazarse por todo cuando tienes miles de funciones. Por lo tanto, voy a marcar ligeramente las funciones de un solo propósito para eso. Creo que a ese nivel de complejidad, incluso si reduce el no. de funciones al incluir más capacidades en cada función, aún sufrirá más por no poder conocer las verdaderas capacidades de esas funciones monolíticas de un vistazo.
En términos de depuración, la pregunta relevante aquí es si tener menos funciones hace que sea más fácil identificar y ubicar rápidamente el código que necesita mirar para depurar un problema.
Según mi experiencia, el rastro de migas de pan que lo lleva desde, por ejemplo, un error HTTP o un rastro de pila de errores en los registros, hasta la función relevante y luego el repositorio es el mismo independientemente de si la función hace una cosa o muchas cosas diferentes. .
Lo que será diferente es cómo encontraría el código relevante dentro del repositorio para los problemas que está investigando.
Una función monolítica que tiene más ramificaciones y, en general, hace más cosas, comprensiblemente requeriría más esfuerzo cognitivo para comprender y seguir el código que es relevante para el problema en cuestión.
Para eso, también rebajaré ligeramente las funciones monolíticas.
Uno de los primeros argumentos que surgieron a favor de los microservicios es que facilita el escalado, pero ese no es el caso: si sabe cómo escalar un sistema, puede escalar un monolito con la misma facilidad con la que puede escalar un microservicio.
Lo digo como alguien que ha construido sistemas back-end monolíticos para juegos que tenían un millón de usuarios activos diarios. Supercell, la empresa matriz de mi empleador actual , y creadora de los juegos más taquilleros como Clash of Clans y Clash Royale , tiene más de 100 millones de usuarios activos diarios en sus juegos y sus sistemas back-end para estos juegos también son monolitos.
En cambio, lo que hemos aprendido de los gigantes tecnológicos como Amazon, Netflix y Google de este mundo es que un estilo de arquitectura orientado a los servicios hace que sea más fácil escalar en una dimensión diferente: nuestro equipo de ingeniería.
Este estilo de arquitectura nos permite crear límites dentro de nuestro sistema, en torno a características y capacidades. Al hacerlo, también permite a nuestros equipos de ingeniería escalar la complejidad de lo que construyen, ya que pueden construir más fácilmente sobre el trabajo que otros han creado antes que ellos.
Tome el almacén de datos en la nube de Google, por ejemplo, los ingenieros que trabajan en ese servicio pudieron producir un servicio altamente sofisticado al construir sobre muchas capas de servicios, cada una de las cuales proporciona una capa poderosa de abstracciones.
Estos límites de servicio son lo que nos brinda una mayor división del trabajo, lo que permite que más ingenieros trabajen en el sistema al brindarles áreas donde pueden trabajar en relativo aislamiento. De esta manera, no tropiezan constantemente con conflictos de fusión, problemas de integración, etc.
Michael Nygard también escribió recientemente un buen artículo que explica este beneficio de los límites y el aislamiento en términos de cómo ayuda a reducir la sobrecarga de compartir modelos mentales.
“si tiene una penalización de coherencia alta y demasiadas personas, entonces el equipo en su conjunto se mueve más lento… Se trata de reducir la sobrecarga de compartir modelos mentales ”. —Michael Nygard
Tener muchas funciones con un solo propósito es quizás el pináculo de esa división de tareas, y algo que se pierde un poco cuando se cambia a funciones monolíticas. Aunque en la práctica, probablemente no termines teniendo tantos desarrolladores trabajando en el mismo proyecto que sientas dolor, ¡a menos que realmente los empaques con esas funciones monolíticas!
Además, restringir una función para que haga una sola cosa también ayuda a limitar la complejidad de una función. Para hacer algo más complejo, en su lugar, compondría estas funciones simples juntas a través de otros medios, como con AWS Step Functions .
Una vez más, voy a rebajar las funciones monolíticas por perder algo de esa división del trabajo y elevar el techo de complejidad de una función.
actualización 02/09/2018: como varias personas han preguntado sobre los arranques en frío en el contexto de funciones monolíticas frente a funciones de un solo propósito, aquí están mis pensamientos.
Como preguntó Kostas Bariotis :
Las funciones monolíticas también tienen los beneficios de usarse con más frecuencia, por lo que es menos probable que estén en estado frío, mientras que las funciones de un solo propósito que no se usan con frecuencia siempre pueden estar en estado frío, ¿no cree?
Eso parece una suposición justa, pero el comportamiento real de los arranques en frío es una discusión más matizada y puede tener resultados drásticamente diferentes dependiendo de la velocidad con la que lleguen las solicitudes. Consulte mi otra publicación que trata este comportamiento con más detalle.
Para simplificar las cosas, consideremos “la cantidad de arranques en frío que habrá experimentado a medida que aumenta a X req/s” . Asumiendo que:
A pequeña escala, digamos, 1 req/s por punto final y un total de 10 puntos finales (que es 1 función monolítica frente a 10 funciones de un solo propósito ) tendremos un total de 10 req/s. Dado el tiempo de ejecución de 100 ms, está dentro de lo que puede manejar una función concurrente.
Para alcanzar 1 req/s por endpoint, habrá experimentado:
A medida que aumenta la carga, a 100 req/s por terminal, lo que equivale a un total de 1000 req/s. Para manejar esta carga, necesitará al menos 100 ejecuciones simultáneas de la función monolítica (100 ms por solicitud, por lo que el rendimiento por ejecución simultánea es de 10 solicitudes/s, por lo tanto, ejecuciones simultáneas = 1000/10 = 100). Para alcanzar este nivel de concurrencia, habrá experimentado:
En este punto, 100 req/s por punto final = 10 ejecuciones simultáneas para cada una de las funciones de propósito único. Para alcanzar ese nivel de concurrencia, también habrá experimentado:
Entonces, las funciones monolíticas no te ayudan con el no. de arranques en frío experimentará incluso con una carga moderada .
Además, cuando la carga es baja, hay cosas simples que puede hacer para mitigar los arranques en frío al precalentar sus funciones (como se discutió en la otra publicación ). Incluso puede usar el calentamiento del complemento sin servidor para que lo haga por usted, e incluso viene con la opción de realizar una ejecución previa al calentamiento después de una implementación.
Sin embargo, esta práctica deja de ser efectiva cuando tiene incluso una cantidad moderada de concurrencia. En ese momento, las funciones monolíticas incurrirían en tantos arranques en frío como las funciones de un solo propósito.
Al empaquetar más "acciones" en una función, también aumentamos el número. de módulos que deben inicializarse durante el inicio en frío de esa función y, por lo tanto, es probable que experimenten inicios en frío más largos como resultado (básicamente, todo lo que esté fuera de la función del controlador exportado se inicializa durante la fase de tiempo de Bootstrap runtime
(ver más abajo) del inicio fresco.
de la charla de Ajay Nair en re:invent 2017 — https://www.youtube.com/watch?v=oQFORsso2go
Imagínese en la versión monolítica de la user-api
ficticia que utilicé para ilustrar el punto en esta publicación, nuestro módulo controlador require
todas las dependencias utilizadas por todos los puntos finales.
const depA = require('lodash');const depB = require('facebook-node-sdk');const depC = require('aws-sdk');...
Mientras que en la versión de propósito único de user-api
, solo la función de controlador del punto final get-user-by-facebook-id
tendría que incurrir en la sobrecarga adicional de inicializar la dependencia facebook-node-sdk
durante el arranque en frío.
También debe tener en cuenta cualquier otro módulo en el mismo proyecto, y sus dependencias, y cualquier código que se ejecutará durante la inicialización de esos módulos, y así sucesivamente.
Entonces, contrariamente a la intuición, las funciones monolíticas no ofrecen ningún beneficio para los arranques en frío fuera de lo que ya puede lograr el precalentamiento básico, y es muy probable que extiendan la duración de los arranques en frío.
Dado que el inicio en frío lo afecta de manera muy diferente según el idioma, la memoria y la cantidad de inicialización que está haciendo en su código . Argumentaré que, si los arranques en frío son una preocupación para usted, entonces es mucho mejor que cambie a otro idioma (es decir, Go, Node.js o Python) e invierta esfuerzo en optimizar su código para que sufra arranques en frío más cortos. .
Además, tenga en cuenta que esto es algo en lo que AWS y otros proveedores están trabajando activamente y sospecho que la plataforma mejorará enormemente la situación en el futuro.
En general, creo que cambiar las unidades de implementación (una gran función frente a muchas funciones pequeñas) no es la forma correcta de abordar los arranques en frío.
Como puede ver, según los criterios que son importantes para mí , tener muchas funciones con un solo propósito es claramente la mejor manera de hacerlo.
Como todos los demás, vengo precargado con un conjunto de predisposiciones y sesgos formados a partir de mis experiencias, que muy probablemente no reflejen las suyas. No le pido que esté de acuerdo conmigo, sino que simplemente aprecie el proceso de resolver las cosas que son importantes para usted y su organización, y cómo encontrar el enfoque adecuado para usted.
Sin embargo, si no está de acuerdo con mi línea de pensamiento y los argumentos que presento para mis criterios de selección (descubrimiento, depuración y escalado del equipo y complejidad del sistema), hágamelo saber a través de comentarios.
Hola, mi nombre es Yan Cui . Soy un héroe sin servidor de AWS y el autor de Production-Ready Serverless . He ejecutado cargas de trabajo de producción a escala en AWS durante casi 10 años y he sido arquitecto o ingeniero principal en una variedad de industrias que van desde la banca, el comercio electrónico, la transmisión de deportes hasta los juegos móviles. Actualmente trabajo como consultor independiente enfocado en AWS y serverless.
Puede ponerse en contacto conmigo a través de correo electrónico , Twitter y LinkedIn .
Consulte mi nuevo curso,Guía completa de AWS Step Functions .
En este curso, cubriremos todo lo que necesita saber para usar el servicio de AWS Step Functions de manera efectiva. Incluye conceptos básicos, disparadores de eventos y HTTP, actividades, patrones de diseño y mejores prácticas.
Consigue tu copia aquí .
Venga a conocer las MEJORES PRÁCTICAS operativas para AWS Lambda: CI/CD, funciones de prueba y depuración localmente, registro, monitoreo, seguimiento distribuido, implementaciones canary, administración de configuración, autenticación y autorización, VPC, seguridad, manejo de errores y más.
También puedes obtener un 40% de descuento sobre el precio nominal con el código ytcui .
Consigue tu copia aquí .