Analyse statique de gros volumes de code Python: expérience Instagram. Partie 1

Le code du serveur Instagram est écrit exclusivement en Python. Eh bien, c'est le cas. Nous utilisons un peu de Cython, et les dépendances incluent beaucoup de code C ++ - qui peut être exploité à partir de Python comme avec les extensions C.



Notre application serveur est un monolithe, qui est une grande base de code composée de plusieurs millions de lignes et comprenant plusieurs milliers de points de terminaison Django ( voici une discussion sur l'utilisation de Django sur Instagram). Tout cela est chargé et servi comme une seule entité. Plusieurs services ont été attribués à partir du monolithe, mais notre plan ne prévoit pas une forte séparation du monolithe.

Notre système de serveur est un monolithe qui change très souvent. Chaque jour, des centaines de programmeurs effectuent des centaines de validations de code. Nous déployons continuellement ces changements, en le faisant toutes les sept minutes. En conséquence, le projet est déployé en production une centaine de fois par jour. Nous nous efforçons de faire en sorte qu'il s'écoule moins d'une heure entre l'obtention d'un commit dans la branche master et le déploiement du code correspondant en production (en voici une présentation faite à PyCon 2019).

Il est très difficile de maintenir cette énorme base de code monolithique, en y faisant des centaines de commits quotidiennement, et en même temps de ne pas l'amener dans un état de chaos complet. Nous voulons faire d'Instagram un endroit où les programmeurs peuvent être productifs et capables de préparer rapidement de nouvelles fonctionnalités utiles du système.

Ce matériel se concentre sur la façon dont nous utilisons le linting et le refactoring automatique pour faciliter la gestion d'une base de code Python.

Si vous êtes intéressé à essayer certaines des idées mentionnées dans ce document, sachez que nous avons récemment transféré dans la catégorie des projets open source LibCST , qui sous-tend bon nombre de nos outils internes pour le linting et le refactoring automatique du code.

La deuxième partie

Linting: documentation qui apparaît là où elle est nécessaire


Le linting aide les programmeurs à trouver et à diagnostiquer les problèmes et les contre-modèles que les développeurs eux-mêmes peuvent ne pas connaître sans les remarquer dans le code. Ceci est important pour nous car les idées pertinentes concernant la conception du code sont les plus difficiles à diffuser, plus les programmeurs travaillent sur le projet. Dans notre cas, nous parlons de centaines de spécialistes.


Variétés de peluches

Le peluchage n'est qu'un des nombreux types d'analyse de code statique que nous utilisons sur Instagram.

La façon la plus primitive d'implémenter des règles de peluchage est d'utiliser des expressions régulières. Les expressions régulières sont faciles à écrire, mais Python n'est pas un langage «normal» . Par conséquent, il est très difficile (et parfois impossible) de rechercher de manière fiable des modèles dans du code Python à l'aide d'expressions régulières.

Si nous parlons des moyens les plus complexes et les plus avancés pour implémenter le linter, il existe des outils comme mypy et Pyre . Ce sont deux systèmes de vérification statique des types de code Python qui peuvent effectuer une analyse approfondie du programme. Instagram utilise Pyre. Ce sont des outils puissants, mais ils sont difficiles à étendre et à personnaliser.

Lorsque nous parlons de peluches sur Instagram, nous entendons généralement travailler avec des règles simples basées sur un arbre de syntaxe abstrait. C'est précisément quelque chose comme ça qui sous-tend nos propres règles de peluchage pour le code serveur.

Lorsque Python exécute un module, il commence par démarrer l'analyseur et lui passer le code source. Grâce à cela, un arbre d'analyse est créé - une sorte d'arbre de syntaxe concrète (CST). Cet arbre est une représentation sans perte du code source d'entrée. Chaque détail est enregistré dans cet arbre, comme les commentaires, les crochets et les virgules. Basé sur CST, vous pouvez restaurer complètement le code d'origine.


Arbre d' analyse Python (une variation de CST) généré par lib2to3

Malheureusement, une telle approche conduit à la création d'un arbre complexe, ce qui rend difficile d'en extraire des informations sémantiques qui nous intéressent.

Python compile l'arbre d'analyse en un arbre de syntaxe abstraite (AST). Certaines informations sur le code source sont perdues lors de cette conversion. Nous parlons d '"informations syntaxiques supplémentaires" - telles que des commentaires, des crochets, des virgules. Cependant, la sémantique du code dans l'AST demeure.


Arbre de syntaxe abstraite Python généré par le module ast

Nous avons développé LibCST - une bibliothèque qui nous offre le meilleur des mondes de CST et AST. Il donne une représentation du code dans lequel toutes les informations le concernant sont stockées (comme dans CST), mais il est facile d'extraire des informations sémantiques à ce sujet à partir d'une telle représentation du code (comme lorsque vous travaillez avec AST).


Représentation d'un arbre de syntaxe LibCST spécifique

Nos règles de linting utilisent l' arbre de syntaxe LibCST pour trouver des modèles dans le code. Cet arbre de syntaxe, à un niveau élevé, est facile à explorer, il permet de se débarrasser des problèmes qui accompagnent le travail avec le langage "irrégulier".

Supposons que dans un certain module il existe une dépendance cyclique due à l'importation de type. Python résout ce problème en plaçant les commandes d'importation de type dans un bloc if TYPE_CHECKING . Il s'agit d'une protection contre l'importation de quelque chose lors de l'exécution.

 #    from typing import TYPE_CHECKING from util import helper_fn #    if TYPE_CHECKING:    from circular_dependency import CircularType 

Plus tard, quelqu'un a ajouté un autre type d'importation et un autre bloc if au code. Cependant, celui qui a fait cela pourrait ne pas savoir qu'un tel mécanisme existe déjà dans le module.

 #    from typing import TYPE_CHECKING from util import helper_fn #    if TYPE_CHECKING:    from circular_dependency import CircularType if TYPE_CHECKING: #   !    from other_package import OtherType 

Vous pouvez vous débarrasser de cette redondance en utilisant la règle de linter!

Commençons par initialiser le compteur des blocs «protecteurs» trouvés dans le code.

 class OnlyOneTypeCheckingIfBlockLintRule(CstLintRule):    def __init__(self, context: Context) -> None:        super().__init__(context)        self.__type_checking_blocks = 0 

Ensuite, en remplissant la condition correspondante, nous incrémentons le compteur et vérifions qu'il n'y aurait pas plus d'un bloc de ce type dans le code. Si cette condition n'est pas remplie, nous générons un avertissement à l'endroit approprié dans le code, appelant le mécanisme auxiliaire utilisé pour générer de tels avertissements.

 def visit_If(self, node: cst.If) -> None:    if node.test.value == "TYPE_CHECKING":        self.__type_checking_blocks += 1        if self.__type_checking_blocks > 1:            self.context.report(                node,                "More than one 'if TYPE_CHECKING' section!"            ) 

Des règles de peluchage similaires fonctionnent en examinant l'arborescence LibCST et en collectant des informations. Dans notre linter, ceci est implémenté en utilisant le modèle Visitor. Comme vous l'avez peut-être remarqué, les règles remplacent les méthodes de visit et laissent les méthodes associées au type de nœud. Ces «visiteurs» sont appelés dans un ordre précis.

 class MyNewLintRule(CstLintRule):    def visit_Assign(self, node):        ... #      def visit_Name(self, node):        ... #        def leave_Assign(self, name):        ... #      


Les méthodes de visite sont appelées avant de visiter les descendants des nœuds. Les méthodes de congé sont appelées après avoir visité tous les descendants

Nous adhérons aux principes du travail, selon lesquels les tâches simples sont résolues en premier. Notre première règle de linter a été implémentée dans un seul fichier, contenait un «visiteur» et utilisait un état partagé.


Un fichier, un «visiteur», utilisant l'état partagé

La classe Single Visitor doit avoir des informations sur l'état et la logique de toutes nos règles de peluchage qui ne lui sont pas liées. De plus, il n'est pas toujours évident de savoir quel état correspond à une règle particulière. Cette approche se montre bien dans une situation où il existe littéralement quelques-unes de vos propres règles de peluchage, mais nous en avons une centaine, ce qui complique grandement le soutien du modèle de single-visitor .


Il est difficile de savoir quel état et quelle logique sont associés à chacune des vérifications.

Bien sûr, comme l'une des solutions possibles à ce problème, on pourrait envisager la définition de plusieurs «visiteurs» et l'organisation d'un tel schéma de travail que chacun d'eux regarderait à chaque fois l'arbre entier. Cependant, cela entraînerait une baisse sérieuse de la productivité, et le linter est un programme qui devrait fonctionner rapidement.


Chaque règle de linter peut parcourir plusieurs fois un arbre. Lors du traitement d'un fichier, les règles sont exécutées séquentiellement. Cependant, cette approche, qui traverse souvent l'arbre, entraînerait une baisse sérieuse des performances.

Au lieu de réaliser quelque chose comme ça, nous nous sommes inspirés des linters utilisés dans les écosystèmes d'autres langages de programmation - comme ESLint de JavaScript, et avons créé un registre centralisé des «visiteurs» (Visitor Registry).


Registre centralisé des "visiteurs". Nous pouvons effectivement déterminer quel nœud est intéressé par chaque règle du linter, ce qui fait gagner du temps aux nœuds qui ne s'y intéressent pas.

Lorsque la règle de linter est initialisée, tous les remplacements des méthodes de règle sont stockés dans le Registre. Lorsque nous faisons le tour de l'arbre, nous regardons tous les «visiteurs» enregistrés et les appelons. Si la méthode n'est pas implémentée, cela signifie que vous n'avez pas besoin de l'appeler.

Cela réduit la consommation de ressources informatiques du système lors de l'ajout de nouvelles règles de peluchage. Nous vérifions généralement avec un lintereur un petit nombre de fichiers récemment modifiés. Mais nous pouvons vérifier toutes les règles sur l'ensemble de la base de code du serveur Instagram en parallèle en seulement 26 secondes.

Après avoir résolu les problèmes de performances, nous avons créé un cadre de test qui visait à respecter les techniques de programmation avancées, nécessitant des tests dans des situations où quelque chose doit avoir une certaine qualité et dans des situations où quelque chose ne devrait pas avoir une certaine qualité devrait.

 class MyCustomLintRuleTest(CstLintRuleTest):    RULE = MyCustomLintRule       VALID = [        Valid("good_function('this should not generate a report')"),        Valid("foo.bad_function('nor should this')"),    ]       INVALID = [        Invalid("bad_function('but this should')", "IG00"),    ] 

Suite → deuxième partie

Chers lecteurs! Utilisez-vous des linters?


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


All Articles