Cet article est une brève explication sur la façon dont l'utilisation d'un mot-clé de langage commun peut avoir une influence sur le budget de l'infrastructure informatique d'un projet ou aider à atteindre certaines limites / restrictions de l'infrastructure d'hébergement et, en outre, sera un bon chant de la qualité et la maturité du code source.
Pour la démonstration des idées, l'article utilisera le langage C #, mais la plupart des idées peuvent être traduites dans d'autres langues.
De l'ensemble des fonctionnalités du langage, de mon point de vue, le «rendement» est le mot-clé le plus sous-évalué. Vous pouvez lire la documentation et trouver un tas d'exemples sur Internet. Pour être bref, disons que «yield» permet de créer implicitement des «itérateurs». De par sa conception, un itérateur doit exposer une source IEnumerable pour une utilisation «publique». Et ici, le délicat commence. Parce que nous avons beaucoup d'implémentations d'IEnumerable dans la langue: liste, dictionnaire, hashset, file d'attente, etc. Et d'après mon expérience, le choix de l'un d'entre eux pour les exigences de satisfaction de certaines tâches commerciales est faux. De plus, tout cela est aggravé par la mise en œuvre choisie, le programme «fonctionne tout simplement» - c'est vraiment ce dont les entreprises ont besoin, n'est-ce pas? Généralement, cela fonctionne, mais uniquement jusqu'à ce que le service soit déployé dans un environnement de production.
Pour une démonstration du problème, je suggère de choisir une analyse de rentabilité très courante pour la plupart des projets d'entreprise que nous pouvons étendre au cours de l'article et de substituer une partie de ce flux pour comprendre une échelle d'influence de cette approche sur les projets d'entreprise. Et cela devrait vous aider à trouver votre propre cas dans cet ensemble pour le réparer.
Exemple de tâche:
- Chargez en ligne un ensemble d'enregistrements d'un fichier ou d'une base de données en mémoire.
- Pour chaque colonne de l'enregistrement, remplacez la valeur par quelqu'un d'autre.
- Enregistrez les résultats de la transformation dans un fichier ou une base de données.
Supposons plusieurs cas où cette logique peut être applicable. En ce moment, je vois deux cas:
- C'est peut-être une partie du flux pour certaines applications ETL de console.
- C'est peut-être une logique à l'intérieur de l'action dans le contrôleur de l'application MVC.
Si nous paraphrasons la tâche de manière plus technique, cela peut ressembler à ceci: "(1) Allouer une quantité de mémoire, (2) charger des informations en mémoire à partir du stockage persistant, (3) modifier et (4) vider les enregistrements changements de mémoire dans le stockage de persistance. " Ici, la première phrase de la description "(1) Allouer une quantité de mémoire" peut avoir une réelle corrélation avec vos besoins non fonctionnels. Parce que votre travail / service doit «vivre» dans un environnement d'hébergement qui peut avoir des limites / restrictions (par exemple, 150 Mo par micro-service) et pour prévoir les dépenses de votre service dans le budget, nous devons prévoir, dans notre cas, la quantité de mémoire quel service utilisera (nous parlons généralement des quantités maximales de mémoire). En d'autres termes, nous devons déterminer une «empreinte» de mémoire pour votre service.
Considérons une empreinte mémoire pour une implémentation vraiment courante que j'observe de temps en temps dans différentes bases de code de projets d'entreprise. En outre, vous pouvez également essayer de le trouver dans vos projets, par exemple, `` sous le capot '' de l'implémentation du modèle `` référentiel '', essayez simplement de trouver ces mots: `` ToList '', `` ToArray '', `` ToReadonlyCollection '', etc. Toutes ces mises en œuvre signifient que:
1. Pour chaque ligne / enregistrement dans le fichier / db, alloue de la mémoire pour contenir les propriétés de l'enregistrement du fichier / db (ie var user = new User () {FirstName = 'Test', LastName = 'Test2'})
2. Ensuite, à l'aide, par exemple, de 'ToArray' ou manuellement, les références d'objet sont conservées dans une collection (par exemple var users = new List (); users.Add (user)). Ainsi, il est alloué une certaine quantité de mémoire pour chaque enregistrement d'un fichier et pour ne pas l'oublier, la référence est stockée dans une collection.
Voici un exemple:
private static IEnumerable<User> LoadUsers2() { var list = new List<User>(); foreach(var line in File.ReadLines("text.txt")) { var splittedLine = line.Split(';'); list.Add(new User() { FirstName = splittedLine[0], LastName = splittedLine[1] }); } return list;
Résultats du profileur de mémoire:

Exactement une telle image que j'ai vue à chaque fois dans un environnement de production avant que les conteneurs s'arrêtent / rechargent en raison de la limitation des ressources d'hébergement par conteneur.
Ainsi, une empreinte pour ce cas, en gros, dépend du nombre d'enregistrements dans un fichier. Parce que la mémoire alloue par enregistrement dans le fichier. Et, la somme de ces petits défauts de mémoire nous donne une quantité maximale de mémoire qui peut être consommée par notre service - c'est l'empreinte du service. Mais cette empreinte est-elle prévisible? Apparemment non. Parce que nous ne pouvons pas prédire un certain nombre d'enregistrements dans le fichier. Et, dans la plupart des cas, la taille du fichier dépasse la quantité de mémoire autorisée dans l'hébergement en plusieurs fois. Cela signifie qu'il est difficile d'utiliser une telle implémentation dans l'environnement de production.
Il semble que c'est le moment de repenser une telle mise en œuvre. L'hypothèse suivante peut nous donner plus d'occasions de calculer une empreinte pour le service: "une empreinte ne devrait dépendre de la taille qu'UN enregistrement dans le fichier". En gros, dans ce cas, nous pouvons calculer la taille maximale de chaque colonne d'un seul enregistrement et les additionner. Il est assez facile de prédire la taille d'un enregistrement au lieu de prédire le nombre d'enregistrements dans le fichier.
Et il est vraiment étonnant que nous puissions implémenter un service qui peut gérer une quantité imprévisible d'enregistrements et ne consomme constamment que quelques mégaoctets avec l'aide d'un seul mot-clé - 'yield' *.Le temps d'un exemple:
class Program { static void Main(string[] args) {
Comme vous pouvez le voir dans l'exemple ci-dessus, il n'y a de mémoire allouée qu'à un seul objet à la fois: 'yield return new User ()' au lieu de créer une collection et de la remplir d'objets. C'est le principal point d'optimisation qui nous permet de calculer une empreinte mémoire plus prévisible pour le service. Parce que nous avons seulement besoin de connaître la taille de deux champs, dans notre cas FirstName et LastName. Lorsqu'un utilisateur modifié est enregistré dans un fichier (voir File.AppendAllLines), l'instance de l'objet utilisateur est disponible pour le garbage collection. Et la mémoire occupée par l'objet est désallouée (c'est-à-dire la prochaine itération de l'instruction 'foreach' dans LoadUsers), de sorte que la prochaine instance de l'objet utilisateur peut être créée. En d'autres termes, à peu près, la même quantité de mémoire remplace par la même quantité de mémoire à chaque itération. C'est pourquoi nous n'avons pas besoin de plus de mémoire que la taille d'un seul enregistrement dans le fichier.
Résultats du profileur de mémoire après optimisation:

D'un autre point de vue, si nous renommons légèrement quelques méthodes dans l'implémentation ci-dessus, afin que l'utilisation puisse remarquer une logique significative pour les contrôleurs dans l'application MVC:
private static void GetUsersAction() {
Une note importante avant de lister le code: la plupart des bibliothèques importantes comme EntityFramework, ASP.net MVC, AutoMapper, Dapper, NHibernate, ADO.net et etc. exposent / consomment les sources IEnumerables. Ainsi, cela signifie dans l'exemple ci-dessus que LoadUsers peut être remplacé par une implémentation qui utilise EntityFramework, par exemple. Qui charge les données ligne par ligne à partir de la table DB, au lieu d'un fichier. MapToDTO peut être remplacé par Automapper et OkResult peut être remplacé par une implémentation «réelle» de IActionResult dans un cadre MVC ou notre propre base d'implémentation sur le flux réseau, par exemple:
private static void OkResult(IEnumerable<User> users) {
Cet exemple «de type mvc» nous montre que nous sommes toujours capables de prédire et de calculer une empreinte mémoire également pour une application Web. Mais dans ce cas, cela dépendra également du nombre de demandes. Par exemple, les exigences non fonctionnelles peuvent sonner de la manière suivante: «Quantité de mémoire maximale pour 1 000 requêtes pas plus: 200 Ko par objet utilisateur x 1 000 requêtes ~ 200 Mo».
Ces calculs sont très utiles pour l'optimisation des performances en cas de mise à l'échelle de l'application Web. Par exemple, vous devez faire évoluer votre application Web sur 100 conteneurs / machines virtuelles. Donc, dans ce cas, pour décider de la quantité de ressources que vous devez allouer au fournisseur d'hébergement, vous pouvez donc ajuster la formule comme suit: 200 Ko par objet utilisateur x 1000 requêtes x 100VMs ~ 20 Go. De plus, c'est la quantité maximale de mémoire et cette quantité est sous le contrôle du budget de votre projet.
J'espère que les informations de cet article seront utiles et permettront d'économiser beaucoup d'argent et de temps dans vos projets.