Fermer

octobre 10, 2018

Comment nous avons optimisé un projet MEAN Stack pour de meilleures performances


MEAN Stack – Optimisations de performance

Ces dernières années, MEAN stack est passé de ‘ à un autre produit JS brillant’ à une alternative viable pour la création d’applications Web hautes performances. Nous avons quelques applications de moyenne à grande qui fonctionnent, ce sont MEAN.

Un de nos récents projets MEAN a enregistré une croissance rapide du nombre d'utilisateurs (des centaines de milliers d'utilisateurs au cours des premières semaines), ce qui a permis au système d'atteindre les charges maximales initialement conçues beaucoup plus tôt que prévu. Nous avons donc dû examiner l'architecture et les paramètres de performance pour identifier et résoudre les goulots d'étranglement.

Ce blog est un bref récapitulatif des que s et comment s dans notre réglage initial des performances. . Comme le système était hébergé sur AWS, nous avons eu recours aux services Amazon chaque fois que possible.

Nous avons implémenté ce qui suit pour améliorer les performances du projet MEAN Stack.

  • Introduction de l'équilibrage de charge

  • Mise en cluster de NodeJS

  • Amazon S3 en tant que CDN [19659013] Réplication mongo

  • Mise en oeuvre du microservice

Equilibrage de charge

Lorsqu'il s'agit d'accroître les performances des sites Web et des services Web, plusieurs options sont possibles:

  • Améliorer l'efficacité du code.
  • Jetez plus de matériel dessus

La deuxième option (ajouter plus de matériel) est la solution la plus rapide et la plus simple pour atténuer les problèmes de performances.

Discutons de la manière dont nous pouvons redimensionner l’infrastructure de serveur Node.js à l’aide de ELB, qui signifie Elastic Load Balancing . ELB répartit automatiquement le trafic applicatif entrant sur un groupe de serveurs principaux. L'équilibreur de charge achemine les demandes entrantes vers le deuxième serveur lorsque celui-ci est occupé à traiter d'autres demandes, de sorte que l'utilisateur final ne subisse aucun retard dans l'obtention de la réponse.

Il existe trois types d'équilibreurs de charge:

  1. Equilibreur de charge d'application

  2. Equilibreur de charge réseau

  3. Equilibreur de charge classique

L'équilibreur de charge d'application convient mieux à l'équilibrage de charge des trafics HTTP et HTTPS et fournit une requête avancée. routage destiné à la fourniture d'architectures d'applications modernes, y compris de microservices et de conteneurs Fonctionnant au niveau de la demande individuelle (couche 7), Application Load Balancer achemine le trafic vers des cibles au sein d'Amazon Virtual Private Cloud (Amazon VPC) en fonction du contenu de la demande. Le transfert de socket est pris en charge uniquement dans l'équilibreur de charge de l'application. Nous avons donc utilisé l'équilibreur de charge d'application Amazon AWS pour mettre à l'échelle l'architecture de serveur.

La décision sur le nombre de serveurs à utiliser dans l'équilibreur de charge dépend du trafic attendu. le serveur et le nombre de demandes simultanées que le serveur doit traiter. De plus, cela dépend également des configurations système dans lesquelles le serveur est hébergé, telles que le nombre de cœurs, la mémoire, etc. Nous devons également modifier certaines configurations de serveur Web (ex: nginx ou apache) comme décrit ici .

Pour vérifier la disponibilité de vos instances EC2 un équilibreur de charge envoie périodiquement des pings, des tentatives de connexion ou des demandes de test des instances EC2. Ces tests sont appelés bilans de santé . Le statut des instances en bonne santé au moment de la vérification de l'état est InService. OutOfService est le statut de toutes les instances en mauvaise santé au moment de la vérification de l'état. L'équilibreur de charge effectue des contrôles d'intégrité sur toutes les instances enregistrées, que l'instance soit dans un état sain ou insalubre.

L'équilibreur de charge route la demande uniquement vers les instances en bonne santé. Lorsque l'équilibreur de charge détermine qu'une instance est en mauvaise santé, il arrête les demandes de routage vers cette instance. L'équilibreur de charge reprend les demandes de routage vers l'instance spécifique lorsqu'elle a été restaurée dans un état sain.

L'équilibreur de charge vérifie l'intégrité des instances enregistrées à l'aide de la configuration de contrôle de l'intégrité par défaut fournie par Elastic Load Balancing ou d'une configuration de vérification de l'intégrité. que le développeur configure. Dans le second cas, le développeur doit fournir une URL de vérification de l'état de santé ({domaine principal} / santé) à l'équipe informatique (chargée de la mise en œuvre ELB), qui sert d'état d'intégrité de l'instance.

L'URL d'intégrité devrait être une API configurée dans le programme Node.js, qui se connecte à la base de données et extrait des données légères pour s'assurer que l'instance fonctionne correctement, c.-à-d. Qu'elle garantit l'intégrité du serveur Web (Nginx), serveur. (Instance Node.js) et le serveur de base de données (MongoDB). Si cette API pouvait répondre avec l’état HTTP 200 dans un délai déterminé (Dites 2 secondes, paramétrable dans la configuration de l’équilibreur de charge), l’instance est marquée comme étant saine, sinon elle est marquée comme étant malsaine.

L'équilibreur de charge vérifie périodiquement l'état de santé de chaque instance (disons 10 secondes, que nous pouvons définir) (si nous l'adaptons à 2 serveurs – vérifie ces 2 instances) via l'URL de santé correspondante, afin de la marquer saine ou malsaine. Ainsi, lors de la prochaine requête, l'équilibreur de charge achemine la requête vers une instance de serveur en bonne santé disponible.

Nous utilisons LoopbackJs – un framework Node.js – et nous pouvons placer le code suivant dans le dossier appelé serveur. / boot (dans la structure de dossiers du framework loopback) pour créer l'URL de l'API de vérification de l'intégrité. Vous pouvez utiliser votre propre chemin d'API si nécessaire.

 module.exports = fonction (serveur) {
 
 var router = server.loopback.Router (); // module de routeur
 
 router.get ('/ health', fonction (req, res) {
 
 server.models.ServerHealthStatus.findOne ({où: {status: 1}, champs: "status"}, fonction (err, healthStatus) {
 
 var réponse = "";
 
 if (err) {// échec
 
 res.status (400) .send (response) .end ();
 
 } autre {
 
 if (healthStatus) {// succès
 
 response + = "Santé: OK";
 
 res.send (réponse); // statut http: 200
 
 } else {// échec
 
 res.status (400) .send (response) .end ();
 
 }
 }
 });
 
 });
 
 

Dans ce code, ServerHealthStatus est un modèle mappé sur la collection de bases de données mongo (similaire à une table dans MySQL) – ServerHealthStatus, qui contient le document suivant (similaire à une ligne dans MySQL):

 {
 
      "_id": ObjectId ("5b2b7u5d1726126da11b1f98"),
 
      "status": "1"
 
 }
 
 

La requête modèle tente de récupérer ce document et envoie la réponse (200 ou 400 en fonction du résultat de la requête). Si le service Node.js est occupé par le traitement d'autres demandes, il se peut que l'extraction des données échoue ou que la réponse prenne plus de temps, ce qui peut dépasser le temps que nous avons configuré dans le délai d'attente de l'équilibreur de charge.

réparties sur plusieurs instances, tout d’abord, nous devons nous assurer que les ressources telles que des images ou des fichiers (images de profil d’utilisateur, documents) sont situées dans un emplacement accessible à toutes les instances. Auparavant, nous utilisions peut-être le système de fichiers local pour le stockage des ressources. Depuis que l'équilibreur de charge est implémenté, plusieurs instances ont besoin que ces contenus soient accessibles . Donc, pour résoudre ce problème, il doit être sauvegardé dans un endroit commun tel que Amazon S3 .

Après avoir hébergé le serveur, nous devons effectuer les tests de charge avec des des outils permettant de s'assurer que le serveur peut gérer le nombre spécifié de demandes simultanées et gérer le trafic. En cas d'échec, nous devons augmenter le nombre de configurations de serveur ou le nombre d'instances de serveur jusqu'à ce qu'il réponde aux normes requises. Nous devons périodiquement surveiller le serveur pour l'utilisation du processeur et de la mémoire lors des tests de charge. Après la mise en œuvre de l'équilibreur de charge, nous devons également effectuer le test de basculement.

De plus, nous devons arrêter manuellement une instance et vérifier si d'autres instances répondent à la demande du client sans délai. En outre, nous devons effectuer les tests de charge en mettant la charge spécifiée dans les exigences du projet (par exemple, 10 000 demandes simultanées).

Mise en cluster de Node.js  NodeJS

Une seule instance de Node.js s'exécute dans un seul thread. Pour tirer parti des systèmes multicœurs, l’utilisateur voudra parfois lancer un cluster de processus Node.js pour gérer la charge. Le module de cluster permet de créer facilement des processus enfants partageant tous les ports du serveur.

 const cluster = require ('cluster');
 
 const http = require ('http');
 
 const numCPUs = require ('os'). cpus (). length;
 
 if (cluster.isMaster) {
 
 console.log (`Master $ {process.pid} est en cours d'exécution`);
 
 // travailleurs de la fourche.
 
 pour (soit i = 0; i < numCPUs; i++) {
 
    cluster.fork();
 
  }
 
  cluster.on('exit', (worker, code, signal) => {
 
 console.log (`worker $ {worker.process.pid} die`);
 
 });
 
 } autre {
 
 // Les travailleurs peuvent partager n'importe quelle connexion TCP
 
 // Dans ce cas c'est un serveur HTTP
 
 http.createServer ((req, res) => {
 
 res.writeHead (200);
 
 res.end ('hello world  n');
 
 }). listen (8000);
 
 console.log (`Worker $ {process.pid} started`);
 }
 
 

Amazon S3 en tant que CDN

Nous utilisions le système de fichiers local pour stocker des ressources statiques (images, fichiers JSON, etc.). Etant donné que l'équilibreur de charge est implémenté, plusieurs instances ont besoin que ce contenu soit accessible. Il doit donc être enregistré dans un emplacement commun. Amazon S3 était le choix automatique, car nous étions déjà sur AWS.

Réplication Mongo

Un jeu de réplicas est un cluster de serveurs de base de données MongoDB qui implémente la réplication maître-esclave (primaire-secondaire). La réplique définit également le basculement automatiquement. Ainsi, si l’un des membres devient indisponible, un nouvel hôte principal est élu et vos données sont toujours accessibles. Nous avons utilisé 3 serveurs mongo. L'ensemble du processus est clairement décrit ici:

https://linode.com/docs/databases/mongodb/create-a-mongodb-replica-set

Nous allons spécifier les noms de domaine de serveur ou les adresses IP des serveurs mongo dans l'URL de connexion à la base de données, comme suit:

 mongodb: // nomutilisateur: motdepasse @ mongo1, mongo2, mongo3 / nom_bdd? replicaSet = rs0 & connectTimeoutMS = 180000 & socketTimeoutMS = 180000 

Où mongo1, mongo2 et le serveur de base utilise des noms de domaine internes, étant donné que tous les serveurs existent dans un même compte AWS, les administrateurs peuvent attribuer des noms définis par l'utilisateur mappés à l'adresse IP, qui peuvent être utilisés à la place des adresses IP dans l'URL de connexion Mongo) qui pointe vers différentes adresses IP.

Dans la structure de bouclage, il est nécessaire de modifier l'URL de connexion dans le fichier datasources.json (il sera remplacé par datasources.prod.json lorsque vous vous trouvez dans l'environnement de production) dans le dossier appelé serveur, comme ceci:

 {
 "db": {
 
 "nom": "db",
 
 "connector": "mongodb",
 
 "url": "mongodb: // userName: password @ mongo1, mongo2, mongo3 / databaseName? replicaSet = rs0 & connectTimeoutMS = 180000 & socketTimeoutMS = 180000"
 
 }
 } 

Microservices FTW

Le fractionnement de fonctionnalités indépendantes en un service autonome peut grandement améliorer les performances du système. Le nœud, avec son architecture asynchrone pilotée par les événements, convient parfaitement à cette tâche.

S'il existe des opérations complexes qui nécessitent une grande quantité de ressources système (CPU, RAM) pendant un temps considérable, vérifiez si elles peuvent être déplacées. en tant que microservice. Donc, la charge ne se reflète pas dans le ou les serveurs principaux. Nous pouvons implémenter cette fonctionnalité sur un serveur indépendant auquel on peut accéder via une URL d'API. Le serveur principal déclenche l'URL de l'API pour que cette tâche soit effectuée.

Par exemple: Envoi de la fonctionnalité de notification push aux applications mobiles

Notre application dispose d'une fonctionnalité d'alerte permettant d'envoyer des notifications à des centaines de milliers d'appareils simultanément. L'envoi de notifications implique la sélection des identifiants de jeton de périphérique de la base de données aux utilisateurs à alerter, tâche qui s'est avérée être une tâche complexe avec la nature non relationnelle de Mongo.

La fonction d'envoi de notifications push prenant beaucoup de temps. processus, la charge du serveur sera élevée pendant ce processus, ce qui peut entraîner la mise en file d'attente d'autres demandes entrantes. Afin de résoudre ce problème, nous avons séparé la fonctionnalité de notification push vers un serveur indépendant en tant que microservice.

Après avoir mis en œuvre tout cela, nous pouvons surveiller l'utilisation des ressources de tous les serveurs tout en effectuant les fonctionnalités, en particulier les fonctionnalités complexes.

Veuillez noter qu'il ne s'agit en aucun cas d'une liste complète. Les optimisations varient en fonction d'un certain nombre de facteurs, de l'architecture et des fonctionnalités aux préférences du client.




Source link