Utilisation de modules stricts dans des projets Python à grande échelle: expérience Instagram. Partie 1

Nous publions la première partie de la traduction du prochain article d'une série sur le fonctionnement d'Instagram avec Python. Le premier article de cette série a parlé des fonctionnalités du code de serveur Instagram, qu'il s'agit d'un monolithe qui change fréquemment et de la façon dont les outils de vérification de type statique aident à gérer ce monolithe. Le deuxième élément concerne la saisie de l'API HTTP. Nous parlerons ici d'approches pour résoudre certains des problèmes rencontrés par Instagram en utilisant Python dans son projet. L'auteur du document espère que l'expérience Instagram sera utile à ceux qui peuvent rencontrer des problèmes similaires.



Aperçu de la situation


Regardons le module suivant, qui, à première vue, semble complètement innocent:

import re from mywebframework import db, route VALID_NAME_RE = re.compile("^[a-zA-Z0-9]+$") @route('/') def home():     return "Hello World!" class Person(db.Model):     name: str 

Quel code sera exécuté si quelqu'un importe ce module?

  • Tout d'abord, le code associé à l'expression régulière qui compile la chaîne dans un objet modèle sera exécuté.
  • Ensuite, le décorateur @route sera exécuté. Si nous nous appuyons sur ce que nous voyons, alors nous pouvons supposer qu'ici, peut-être, la représentation correspondante est enregistrée dans le système de mappage d'URL. Cela signifie que l'importation habituelle de ce module conduit au fait que, ailleurs, l'état global de l'application est en train de changer.
  • Nous allons maintenant exécuter tout le code du corps de la classe Person . Il peut contenir n'importe quoi. La classe de base Model peut avoir une méthode de métaclasse ou __init_subclass__ , qui, à son tour, peut contenir un autre code qui est exécuté lors de l'importation de notre module.

Problème n ° 1: démarrage et redémarrage lent du serveur


La seule ligne de code pour ce module qui (éventuellement) ne s'exécute pas lors de son importation est de return "Hello World!" . Certes, avec certitude, nous ne pouvons pas dire cela! En conséquence, il s'avère qu'en important ce module simple composé de huit lignes (et ne l'utilisant toujours pas dans notre programme), nous pouvons provoquer le lancement de centaines voire de milliers de lignes de code Python. Et cela sans oublier que l'importation de ce module entraîne une modification du mappage d'URL global situé à un autre endroit du programme.

Que faire Devant nous fait partie de la conséquence du fait que Python est un langage interprété dynamique. Cela nous permet de résoudre avec succès divers problèmes en utilisant des méthodes de métaprogrammation . Mais qu'est-ce qui ne va pas avec ce code?

En fait, ce code est en parfait état. Il en est ainsi tant que quelqu'un l'utilise dans des bases de code relativement petites, sur lesquelles travaillent de petites équipes de programmeurs. Ce code ne cause pas de problème tant que celui qui l'utilise est garanti de maintenir un certain niveau de discipline dans l'utilisation exacte des fonctionnalités Python. Mais certains aspects de ce dynamisme peuvent devenir un problème s'il y a des millions de lignes de code dans le projet sur lesquelles des centaines de programmeurs travaillent, dont beaucoup n'ont pas une connaissance approfondie de Python.

Par exemple, l'une des grandes fonctionnalités de Python est la vitesse des étapes impliquées dans le développement par phases. A savoir, le résultat des modifications de code peut être vu littéralement immédiatement après avoir effectué de telles modifications, sans avoir besoin de compiler le code. Mais si nous parlons d'un projet de plusieurs millions de lignes (et d'un diagramme de dépendance plutôt déroutant de ce projet), alors ce plus de Python commence à se transformer en moins.

Il faut plus de 20 secondes pour démarrer notre serveur. Et parfois, lorsque nous ne prêtons pas l'attention voulue à l'optimisation, ce temps passe à environ une minute. Cela signifie que le développeur a besoin de 20 à 60 secondes pour voir les résultats des modifications apportées au code. Cela s'applique à ce que vous pouvez voir dans le navigateur et même à la vitesse d'exécution des tests unitaires. Malheureusement, ce temps est suffisant pour qu'une personne soit distraite par quelque chose et oublie ce qu'elle avait fait auparavant. La plupart de ce temps, littéralement, est consacré à l'importation de modules et à la création de fonctions et de classes.

D'une certaine manière, cela revient à attendre les résultats de la compilation d'un programme écrit dans un autre langage. Mais généralement, la compilation peut être effectuée de manière incrémentielle . Le fait est que vous ne pouvez recompiler que ce qui a changé et ce qui dépend directement du code modifié. Par conséquent, la compilation des projets, effectuée après avoir apporté de petites modifications, est rapide. Mais lorsque vous travaillez avec Python, car les commandes d'importation peuvent avoir tout type d'effets secondaires, il n'existe aucun moyen fiable et sûr de redémarrer le serveur de manière incrémentielle. Dans le même temps, l'échelle des modifications est sans importance et chaque fois que nous devons redémarrer complètement le serveur, importer tous les modules, recréer toutes les classes et fonctions, recompiler toutes les expressions régulières, etc. Habituellement, à partir du dernier redémarrage du serveur, 99% du code n'a pas changé, mais nous devons toujours faire la même chose encore et encore pour entrer les modifications.

En plus de ralentir les développeurs, cela conduit au gaspillage improductif de quantités importantes de ressources système. Le fait est que nous travaillons dans un mode de déploiement continu des changements, ce qui signifie un rechargement constant du code du serveur de production.

En fait, voici notre premier problème: démarrage et redémarrage lent du serveur. Ce problème est dû au fait que le système doit constamment effectuer une grande quantité d'actions répétitives lors de l'importation de code.

Problème n ° 2: effets secondaires des commandes d'importation non sécurisées


Voici une autre tâche que les développeurs résolvent souvent lors de l'importation de modules. Ceci charge les paramètres à partir du stockage réseau des configurations:

 MY_CONFIG = get_config_from_network_service() 

En plus de ralentir le démarrage du serveur, il est également dangereux. Si le service réseau n'est pas disponible, cela entraînera non seulement le fait que nous recevrons des messages d'erreur concernant l'impossibilité de répondre à certaines demandes. Cela entraînera le démarrage du serveur.

Épaississons les couleurs et imaginons que quelqu'un ait ajouté au module chargé d'initialiser un service réseau important, du code qui est exécuté lors de l'importation. Le développeur ne savait tout simplement pas où lui ajouter ce code, il l'a donc placé dans un module qui est importé au début du démarrage du serveur. Il s'est avéré que ce schéma fonctionne, donc la solution a été considérée comme réussie et le travail s'est poursuivi.

Mais ensuite, quelqu'un d'autre a ajouté ailleurs l'équipe d'importation, ce qui, à première vue, était inoffensif. Par conséquent, à travers une chaîne d'importations avec une profondeur de douze modules, cela a conduit au fait que le module téléchargeant les paramètres du réseau est maintenant importé dans le module qui initialise le service réseau correspondant.

Maintenant, il s'avère que nous essayons d'utiliser le service avant son initialisation. Le système plante naturellement. Dans le meilleur des cas, si nous parlons d'un système dans lequel les interactions sont complètement déterministes, cela peut conduire au fait que le développeur passera une heure ou deux à comprendre comment un changement mineur a conduit à un échec de quelque chose, avec lui, semble déconnecté. Mais dans des situations plus complexes, cela peut conduire à une «chute» du projet en production. Cependant, il n'existe aucun moyen universel d'utiliser le linter pour lutter contre ces problèmes ou pour les éviter.

La racine du problème réside dans deux facteurs dont l'interaction entraîne des conséquences dévastatrices:

  1. Python permet aux modules d'avoir des effets secondaires arbitraires et dangereux qui se produisent lors de l'importation.
  2. L'ordre d'importation du code n'est pas explicitement défini et n'est pas contrôlé. A l'échelle d'un projet, une sorte d '«importation globale» est constituée des commandes d'importation contenues dans tous les modules. Dans ce cas, l'ordre d'importation des modules peut varier en fonction du point d'entrée du système utilisé.

À suivre ...

Chers lecteurs! Avez-vous rencontré des problèmes concernant le démarrage lent des projets Python?


Source: https://habr.com/ru/post/fr475240/


All Articles