Jouer à l'échelle ! à des milliers de demandes simultanées

Publié: 2022-03-11

Les développeurs Web de Scala omettent souvent de prendre en compte les conséquences de milliers d'utilisateurs accédant à nos applications en même temps. C'est peut-être parce que nous aimons prototyper rapidement ; c'est peut-être parce que tester de tels scénarios est tout simplement difficile .

Quoi qu'il en soit, je vais affirmer qu'ignorer l'évolutivité n'est pas aussi grave qu'il n'y paraît, si vous utilisez le bon ensemble d'outils et suivez les bonnes pratiques de développement.

Ignorer l'évolutivité n'est pas aussi grave qu'il n'y paraît, si vous utilisez les outils appropriés.

Lojinha et le jeu ! Cadre

Il y a quelque temps, j'ai lancé un projet appelé Lojinha (qui se traduit par "petit magasin" en portugais), ma tentative de construire un site d'enchères. (Au fait, ce projet est open source). Mes motivations étaient les suivantes :

  • Je voulais vraiment revendre des vieux trucs que je n'utilise plus.
  • Je n'aime pas les sites d'enchères traditionnels, surtout ceux que nous avons ici au Brésil.
  • Je voulais « jouer » avec le Play ! Cadre 2 (jeu de mots).

Alors évidemment, comme mentionné ci-dessus, j'ai décidé d'utiliser le Play! Cadre. Je n'ai pas un décompte exact du temps qu'il a fallu pour construire, mais il ne fallut certainement pas longtemps avant que mon site soit opérationnel avec le système simple déployé sur http://lojinha.jcranky.com. En fait, j'ai passé au moins la moitié du temps de développement sur le design, qui utilise Twitter Bootstrap (rappelez-vous : je ne suis pas designer…).

Le paragraphe ci-dessus devrait clarifier au moins une chose : je ne me suis pas trop préoccupé des performances, voire pas du tout, lors de la création de Lojinha.

Et c'est exactement ce que je veux dire : il y a du pouvoir à utiliser les bons outils - des outils qui vous maintiennent sur la bonne voie, des outils qui vous encouragent à suivre les meilleures pratiques de développement par leur construction même.

Dans ce cas, ces outils sont le Play! Framework et le langage Scala, avec Akka faisant quelques "apparitions en tant qu'invités".

Laissez-moi vous montrer ce que je veux dire.

Immuabilité et mise en cache

Il est généralement admis que la minimisation de la mutabilité est une bonne pratique. En bref, la mutabilité rend plus difficile le raisonnement sur votre code, en particulier lorsque vous essayez d'introduire un parallélisme ou une concurrence.

Le jeu! Le framework Scala vous oblige à utiliser l'immuabilité une bonne partie du temps, tout comme le langage Scala lui-même. Par exemple, le résultat généré par un contrôleur est immuable. Parfois, vous pourriez considérer cette immuabilité comme «gênante» ou «ennuyeuse», mais ces «bonnes pratiques» sont «bonnes» pour une raison.

Dans ce cas, l'immuabilité du contrôleur était absolument cruciale lorsque j'ai finalement décidé de lancer des tests de performances : j'ai découvert un goulot d'étranglement et, pour le réparer, j'ai simplement mis en cache cette réponse immuable.

Par mise en cache , je veux dire enregistrer l'objet de réponse et servir une instance identique, telle quelle, à tous les nouveaux clients. Cela évite au serveur d'avoir à recalculer le résultat une fois de plus. Il ne serait pas possible de servir la même réponse à plusieurs clients si ce résultat était modifiable.

Inconvénient : pendant une brève période (délai d'expiration du cache), les clients peuvent recevoir des informations obsolètes. Ce n'est un problème que dans les scénarios où vous avez absolument besoin que le client accède aux données les plus récentes, sans tolérance de retard.

Pour référence, voici le code Scala pour charger la page de démarrage avec une liste de produits, sans mise en cache :

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

Maintenant, en ajoutant le cache :

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

Tout simple, n'est-ce pas ? Ici, "index" est la clé à utiliser dans le système de cache et 5 est le délai d'expiration, en secondes.

Après la mise en cache, le débit est passé à 800 requêtes par seconde. C'est une amélioration de plus de 4x pour moins de deux lignes de code.

Pour tester l'effet de ce changement, j'ai exécuté des tests JMeter (inclus dans le référentiel GitHub) localement. Avant d'ajouter le cache, j'ai atteint un débit d'environ 180 requêtes par seconde. Après la mise en cache, le débit est passé à 800 requêtes par seconde. C'est une amélioration de plus de 4x pour moins de deux lignes de code.

C'est ainsi que j'ai utilisé Play ! cache pour améliorer les performances sur mon site d'enchères Scala.

Consommation de mémoire

Un autre domaine où les bons outils Scala peuvent faire une grande différence est la consommation de mémoire. Là encore, Jouez ! vous pousse dans la bonne direction (évolutive). Dans le monde Java, pour une application Web "normale" écrite avec l'API de servlet (c'est-à-dire presque n'importe quel framework Java ou Scala), il est très tentant de mettre beaucoup de bric-à-brac dans la session utilisateur car l'API offre des fonctionnalités faciles à utiliser. appelez les méthodes qui vous permettent de le faire :

 session.setAttribute("attrName", attrValue);

Parce qu'il est si facile d'ajouter des informations à la session utilisateur, il est souvent abusé. Par conséquent, le risque d'utiliser trop de mémoire sans raison valable est tout aussi élevé.

Avec le Play! framework, ce n'est pas une option - le framework n'a tout simplement pas d'espace de session côté serveur. Le jeu! La session utilisateur du framework est conservée dans un cookie de navigateur, et vous devez vivre avec. Cela signifie que l'espace de session est limité en taille et en type : vous ne pouvez stocker que des chaînes. Si vous avez besoin de stocker des objets, vous devrez utiliser le mécanisme de mise en cache dont nous avons parlé précédemment. Par exemple, vous souhaiterez peut-être stocker l'adresse e-mail ou le nom d'utilisateur de l'utilisateur actuel dans la session, mais vous devrez utiliser le cache si vous devez stocker un objet utilisateur entier à partir de votre modèle de domaine.

Jouer! vous maintient sur la bonne voie, vous obligeant à examiner attentivement votre utilisation de la mémoire, ce qui produit un code de premier passage pratiquement prêt pour le cluster.

Encore une fois, cela peut sembler pénible au début, mais en vérité, jouez ! vous maintient sur la bonne voie, vous obligeant à examiner attentivement votre utilisation de la mémoire, ce qui produit un code de premier passage pratiquement prêt pour le cluster, d'autant plus qu'il n'y a pas de session côté serveur qui devrait être propagée dans tout votre cluster, rendant la vie infiniment plus facile.

Prise en charge asynchrone

Suivant dans ce jeu ! examen du cadre, nous examinerons comment Play! brille également dans le support async (hronous). Et au-delà de ses fonctionnalités natives, Play ! vous permet d'embarquer Akka, un puissant outil de traitement asynchrone.

Bien que Lojinha ne profite pas encore pleinement d'Akka, sa simple intégration avec Play! a rendu très facile :

  1. Planifiez un service de messagerie asynchrone.
  2. Traiter les offres pour divers produits simultanément.

En bref, Akka est une implémentation du modèle d'acteur rendu célèbre par Erlang. Si vous n'êtes pas familier avec le modèle d'acteur Akka, imaginez-le simplement comme une petite unité qui ne communique que par messages.

Pour envoyer un e-mail de manière asynchrone, je crée d'abord le message et l'acteur appropriés. Ensuite, tout ce que j'ai à faire, c'est quelque chose comme:

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

La logique d'envoi d'e-mails est implémentée à l'intérieur de l'acteur, et le message indique à l'acteur quel e-mail nous souhaitons envoyer. Cela se fait dans un schéma fire-and-forget, ce qui signifie que la ligne ci-dessus envoie la requête et continue ensuite à exécuter tout ce que nous avons après cela (c'est-à-dire qu'elle ne bloque pas).

Pour plus d'informations sur l'Async natif de Play !, consultez la documentation officielle.

Conclusion

En résumé : J'ai rapidement développé une petite application, Lojinha, capable de très bien évoluer. Lorsque j'ai rencontré des problèmes ou découvert des goulots d'étranglement, les correctifs ont été rapides et faciles, grâce aux outils que j'ai utilisés (Play!, Scala, Akka, etc.), ce qui m'a poussé à suivre les meilleures pratiques en termes d'efficacité et évolutivité. Sans me soucier des performances, j'ai pu m'adapter à des milliers de requêtes simultanées.

Lors du développement de votre prochaine application, réfléchissez bien à vos outils.

En relation : Réduire le code standard avec les macros Scala et les quasi-quotes