¡Juego de escala! a miles de solicitudes simultáneas

Publicado: 2022-03-11

Los desarrolladores web de Scala a menudo no consideran las consecuencias de que miles de usuarios accedan a nuestras aplicaciones al mismo tiempo. Tal vez sea porque nos encanta hacer prototipos rápidamente; tal vez sea porque probar tales escenarios es simplemente difícil .

Independientemente, voy a argumentar que ignorar la escalabilidad no es tan malo como parece, si usa el conjunto adecuado de herramientas y sigue buenas prácticas de desarrollo.

Ignorar la escalabilidad no es tan malo como parece, si usa las herramientas adecuadas.

Lojinha y el juego! Estructura

Hace algún tiempo, inicié un proyecto llamado Lojinha (que se traduce como “pequeña tienda” en portugués), mi intento de construir un sitio de subastas. (Por cierto, este proyecto es de código abierto). Mis motivaciones fueron las siguientes:

  • Tenía muchas ganas de vender algunas cosas viejas que ya no uso.
  • No me gustan los sitios de subastas tradicionales, especialmente los que tenemos aquí en Brasil.
  • ¡Quería “jugar” con Play! Marco 2 (juego de palabras).

Entonces, obviamente, como se mencionó anteriormente, ¡decidí usar Play! Estructura. No tengo una cuenta exacta de cuánto tiempo tomó construirlo, pero ciertamente no pasó mucho tiempo antes de que tuviera mi sitio en funcionamiento con el sistema simple implementado en http://lojinha.jcranky.com. De hecho, dediqué al menos la mitad del tiempo de desarrollo al diseño, que usa Twitter Bootstrap (recuerde: no soy diseñador…).

El párrafo anterior debería dejar en claro al menos una cosa: no me preocupé demasiado por el rendimiento, si es que me preocupé al crear Lojinha.

Y ese es exactamente mi punto: hay poder en el uso de las herramientas correctas, herramientas que lo mantienen en el camino correcto, herramientas que lo alientan a seguir las mejores prácticas de desarrollo desde su misma construcción.

En este caso, esas herramientas son Play! Framework y el lenguaje Scala, con Akka haciendo algunas "apariciones especiales".

Déjame mostrarte lo que quiero decir.

Inmutabilidad y almacenamiento en caché

En general, se acepta que minimizar la mutabilidad es una buena práctica. Brevemente, la mutabilidad hace que sea más difícil razonar sobre su código, especialmente cuando intenta introducir algún paralelismo o concurrencia.

¡El juego! Scala framework te hace usar la inmutabilidad una buena parte del tiempo, al igual que el propio lenguaje Scala. Por ejemplo, el resultado generado por un controlador es inmutable. A veces, puede considerar que esta inmutabilidad es "molesta" o "molesta", pero estas "buenas prácticas" son "buenas" por una razón.

En este caso, la inmutabilidad del controlador fue absolutamente crucial cuando finalmente decidí ejecutar algunas pruebas de rendimiento: descubrí un cuello de botella y, para solucionarlo, simplemente almacené en caché esta respuesta inmutable.

Al almacenar en caché , me refiero a guardar el objeto de respuesta y servir una instancia idéntica, tal como está, a cualquier cliente nuevo. Esto libera al servidor de tener que recalcular el resultado nuevamente. No sería posible brindar la misma respuesta a varios clientes si este resultado fuera mutable.

La desventaja: durante un breve período (el tiempo de caducidad de la memoria caché), los clientes pueden recibir información desactualizada. Esto es solo un problema en escenarios en los que es absolutamente necesario que el cliente acceda a los datos más recientes, sin tolerancia a la demora.

Como referencia, aquí está el código de Scala para cargar la página de inicio con una lista de productos, sin almacenamiento en caché:

 def index = Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) }

Ahora, agregando el caché:

 def index = Cached("index", 5) { Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) } }

Bastante simple, ¿no? Aquí, "índice" es la clave que se utilizará en el sistema de caché y 5 es el tiempo de caducidad, en segundos.

Después del almacenamiento en caché, el rendimiento aumentó a 800 solicitudes por segundo. Esa es una mejora de más de 4x por menos de dos líneas de código.

Para probar el efecto de este cambio, ejecuté algunas pruebas de JMeter (incluidas en el repositorio de GitHub) localmente. Antes de agregar el caché, logré un rendimiento de aproximadamente 180 solicitudes por segundo. Después del almacenamiento en caché, el rendimiento aumentó a 800 solicitudes por segundo. Esa es una mejora de más de 4x por menos de dos líneas de código.

Así es como usé Play! caché para mejorar el rendimiento en mi sitio de subastas Scala.

Consumo de memoria

Otra área en la que las herramientas de Scala adecuadas pueden marcar una gran diferencia es en el consumo de memoria. Aquí, de nuevo, ¡Juega! lo empuja en la dirección correcta (escalable). En el mundo de Java, para una aplicación web "normal" escrita con la API de servlet (es decir, casi cualquier marco de Java o Scala), es muy tentador poner mucha basura en la sesión del usuario porque la API ofrece fácil de usar. métodos de llamada que le permiten hacerlo:

 session.setAttribute("attrName", attrValue);

Debido a que es tan fácil agregar información a la sesión del usuario, a menudo se abusa de ella. Como consecuencia, el riesgo de usar demasiada memoria posiblemente sin una buena razón es igualmente alto.

Con el juego! framework, esta no es una opción: el marco simplemente no tiene un espacio de sesión del lado del servidor. ¡El juego! La sesión de usuario del marco se mantiene en una cookie del navegador y usted tiene que vivir con ella. Esto significa que el espacio de la sesión está limitado en tamaño y tipo: solo puede almacenar cadenas. Si necesita almacenar objetos, tendrá que usar el mecanismo de almacenamiento en caché que discutimos antes. Por ejemplo, es posible que desee almacenar la dirección de correo electrónico o el nombre de usuario del usuario actual en la sesión, pero tendrá que usar la memoria caché si necesita almacenar un objeto de usuario completo de su modelo de dominio.

¡Jugar! lo mantiene en el camino correcto, obligándolo a considerar cuidadosamente el uso de su memoria, lo que produce un código de primer paso que está prácticamente listo para el clúster.

Una vez más, esto puede parecer molesto al principio, pero en realidad, ¡Juega! lo mantiene en el camino correcto, obligándolo a considerar cuidadosamente su uso de memoria, lo que produce un código de primer paso que está prácticamente listo para el clúster, especialmente dado que no hay una sesión del lado del servidor que deba propagarse a través de su clúster, lo que hace que la vida infinitamente más fácil.

Soporte asíncrono

Siguiente en este Play! revisión del marco, examinaremos cómo Play! también brilla en el soporte asíncrono (hronous). Y más allá de sus características nativas, Play! le permite incorporar Akka, una poderosa herramienta para el procesamiento asíncrono.

Aunque Lojinha aún no aprovecha al máximo Akka, su sencilla integración con Play! hizo que fuera muy fácil:

  1. Programe un servicio de correo electrónico asíncrono.
  2. Procesar ofertas para varios productos al mismo tiempo.

Brevemente, Akka es una implementación del Actor Model que Erlang hizo famoso. Si no está familiarizado con el modelo de actor Akka, imagínelo como una pequeña unidad que solo se comunica a través de mensajes.

Para enviar un correo electrónico de forma asincrónica, primero creo el mensaje y el actor adecuados. Entonces, todo lo que necesito hacer es algo como:

 EMail.actor ! BidToppedMessage(item.name, itemUrl, bidderEmail)

La lógica de envío de correo electrónico se implementa dentro del actor, y el mensaje le dice al actor qué correo electrónico nos gustaría enviar. Esto se hace en un esquema de disparar y olvidar, lo que significa que la línea anterior envía la solicitud y luego continúa ejecutando lo que tengamos después de eso (es decir, no se bloquea).

Para obtener más información sobre Async nativo de Play!, consulte la documentación oficial.

Conclusión

En resumen: rápidamente desarrollé una pequeña aplicación, Lojinha, capaz de escalar hacia arriba y hacia afuera muy bien. Cuando me encontré con problemas o descubrí cuellos de botella, las soluciones fueron rápidas y fáciles, con mucho crédito debido a las herramientas que usé (Play!, Scala, Akka, etc.), lo que me impulsó a seguir las mejores prácticas en términos de eficiencia y escalabilidad Con poca preocupación por el rendimiento, pude escalar a miles de solicitudes simultáneas.

Cuando desarrolle su próxima aplicación, considere cuidadosamente sus herramientas.

Relacionado: Reduzca el código repetitivo con Scala Macros y Quasiquotes