Verwendung strenger Module in großen Python-Projekten: Instagram-Erfahrung. Teil 1

Wir veröffentlichen den ersten Teil der Übersetzung eines anderen Artikels aus der Reihe über die Funktionsweise von Instagram mit Python. Der erste Artikel in dieser Reihe befasste sich mit den Funktionen des Instagram-Servercodes, dass es sich um einen Monolithen handelt, der sich häufig ändert, und wie statische Tools zur Typprüfung bei der Verwaltung dieses Monolithen helfen. Das zweite Material handelt von der Eingabe der HTTP-API. Hier werden wir über Lösungsansätze für einige der Probleme sprechen, auf die Instagram bei der Verwendung von Python in seinem Projekt gestoßen ist. Der Autor des Materials hofft, dass die Instagram-Erfahrung für diejenigen nützlich sein wird, die möglicherweise auf ähnliche Probleme stoßen.



Situationsübersicht


Schauen wir uns das folgende Modul an, das auf den ersten Blick völlig unschuldig aussieht:

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 

Welcher Code wird ausgeführt, wenn jemand dieses Modul importiert?

  • Zunächst wird der Code ausgeführt, der dem regulären Ausdruck zugeordnet ist, der die Zeichenfolge in ein Vorlagenobjekt kompiliert.
  • Dann wird der @route Decorator ausgeführt. Wenn wir uns auf das verlassen, was wir sehen, können wir davon ausgehen, dass hier möglicherweise die entsprechende Darstellung im URL-Mapping-System registriert ist. Dies bedeutet, dass der übliche Import dieses Moduls dazu führt, dass sich der globale Status der Anwendung an einer anderen Stelle ändert.
  • Jetzt führen wir den gesamten Body-Code der Person Klasse aus. Es kann alles enthalten. Die Basisklasse Model kann eine Metaklasse oder __init_subclass__ Methode __init_subclass__ , die wiederum einen anderen Code enthalten kann, der beim Importieren unseres Moduls ausgeführt wird.

Problem Nr. 1: langsamer Serverstart und Neustart


Die einzige Codezeile für dieses Modul, die (möglicherweise) beim Import nicht ausgeführt wird, ist return "Hello World!" . Natürlich können wir das nicht mit Sicherheit sagen! Als Ergebnis stellt sich heraus, dass durch den Import dieses einfachen Moduls, das aus acht Zeilen besteht (und noch nicht einmal in unserem Programm verwendet wird), möglicherweise Hunderte oder sogar Tausende von Zeilen Python-Code gestartet werden. Und dies ist nicht zu erwähnen, dass der Import dieses Moduls eine Änderung der globalen URL-Zuordnung verursacht, die sich an einer anderen Stelle im Programm befindet.

Was zu tun ist? Vor uns liegt die Konsequenz, dass Python eine dynamisch interpretierte Sprache ist. Dies ermöglicht es uns, verschiedene Probleme mit Hilfe von Metaprogrammierungsmethoden erfolgreich zu lösen. Aber was ist trotzdem falsch an diesem Code?

In der Tat ist dieser Code in perfekter Reihenfolge. Dies ist so lange, wie jemand es in relativ kleinen Codebasen verwendet, an denen kleine Teams von Programmierern arbeiten. Dieser Code verursacht keine Probleme, solange derjenige, der ihn verwendet, garantiert ein gewisses Maß an Disziplin bei der Verwendung der Python-Funktionen beibehält. Einige Aspekte dieser Dynamik können jedoch zu einem Problem werden, wenn das Projekt Millionen von Codezeilen enthält, an denen Hunderte von Programmierern arbeiten, von denen viele keine umfassenden Python-Kenntnisse besitzen.

Eine der herausragenden Eigenschaften von Python ist beispielsweise die Geschwindigkeit der Schritte, die bei der schrittweisen Entwicklung erforderlich sind. Das Ergebnis von Codeänderungen kann nämlich unmittelbar nach dem Vornehmen solcher Änderungen im wahrsten Sinne des Wortes gesehen werden, ohne dass der Code kompiliert werden muss. Wenn es sich jedoch um ein Projekt mit mehreren Millionen Zeilen handelt (und um ein ziemlich verwirrendes Abhängigkeitsdiagramm dieses Projekts), verwandelt sich dieses Plus von Python in ein Minus.

Es dauert mehr als 20 Sekunden, um unseren Server zu starten. Und manchmal, wenn wir der Optimierung nicht die gebührende Aufmerksamkeit schenken, erhöht sich diese Zeit auf ungefähr eine Minute. Dies bedeutet, dass der Entwickler 20 bis 60 Sekunden benötigt, um die Ergebnisse der am Code vorgenommenen Änderungen anzuzeigen. Dies gilt für das, was Sie im Browser sehen können, und sogar für die Geschwindigkeit, mit der Unit-Tests ausgeführt werden. Leider reicht diese Zeit aus, um einen Menschen von etwas abzulenken und zu vergessen, was er zuvor getan hat. Die meiste Zeit wird buchstäblich für das Importieren von Modulen und das Erstellen von Funktionen und Klassen aufgewendet.

In gewisser Weise entspricht dies dem Warten auf die Ergebnisse der Kompilierung eines Programms, das in einer anderen Sprache geschrieben wurde. In der Regel kann die Kompilierung jedoch inkrementell erfolgen . Der Punkt ist, dass Sie nur neu kompilieren können, was sich geändert hat und was direkt vom geänderten Code abhängt. Infolgedessen erfolgt die Zusammenstellung von Projekten in der Regel schnell, nachdem kleine Änderungen vorgenommen wurden. Bei der Arbeit mit Python gibt es jedoch keine zuverlässige und sichere Möglichkeit, den Server schrittweise neu zu starten, da Importbefehle alle möglichen Nebenwirkungen haben können. Gleichzeitig ist der Umfang der Änderungen unwichtig und jedes Mal müssen wir den Server neu starten, alle Module importieren, alle Klassen und Funktionen neu erstellen, alle regulären Ausdrücke neu kompilieren und so weiter. Normalerweise haben sich seit dem letzten Neustart des Servers 99% des Codes nicht geändert, aber wir müssen immer wieder dasselbe tun, um die Änderungen einzugeben.

Dies verlangsamt nicht nur die Entwicklung, sondern führt auch zu einer unproduktiven Verschwendung erheblicher Mengen an Systemressourcen. Tatsache ist, dass wir in einem Modus der kontinuierlichen Bereitstellung von Änderungen arbeiten, was ein ständiges Neuladen des Produktionsserver-Codes bedeutet.

Hier ist unser erstes Problem: Langsamer Start und Neustart des Servers. Dieses Problem entsteht aufgrund der Tatsache, dass das System während des Code-Imports ständig eine große Anzahl sich wiederholender Aktionen ausführen muss.

Problem 2: Nebenwirkungen unsicherer Importbefehle


Eine weitere Aufgabe, die Entwickler beim Importieren von Modulen häufig lösen. Dies lädt Einstellungen aus dem Netzwerkspeicher von Konfigurationen:

 MY_CONFIG = get_config_from_network_service() 

Es verlangsamt nicht nur den Serverstart, sondern ist auch unsicher. Wenn der Netzwerkdienst nicht verfügbar ist, wird dies nicht nur dazu führen, dass wir Fehlermeldungen bezüglich der Unfähigkeit, einige Anforderungen zu erfüllen, erhalten. Dadurch kann der Server nicht gestartet werden.

Lassen Sie uns die Farben verdicken und sich vorstellen, dass jemand dem Modul, das für die Initialisierung eines wichtigen Netzwerkdienstes verantwortlich ist, Code hinzugefügt hat, der während des Imports ausgeführt wird. Der Entwickler wusste einfach nicht, wo er diesen Code hinzufügen sollte, und platzierte ihn in einem Modul, das zu Beginn des Startvorgangs des Servers importiert wurde. Es stellte sich heraus, dass dieses Schema funktioniert, sodass die Lösung als erfolgreich angesehen und die Arbeit fortgesetzt wurde.

Aber dann hat jemand anderes das Importteam woanders hinzugefügt, was auf den ersten Blick harmlos war. Dies führte durch eine Importkette mit einer Tiefe von zwölf Modulen dazu, dass das Modul, das die Einstellungen aus dem Netzwerk lädt, nun in das Modul importiert wird, das den entsprechenden Netzwerkdienst initialisiert.

Nun stellt sich heraus, dass wir versuchen, den Dienst zu verwenden, bevor er initialisiert wird. Das System stürzt natürlich ab. Wenn es sich im besten Fall um ein System handelt, in dem die Interaktionen vollständig deterministisch sind, kann dies dazu führen, dass der Entwickler ein oder zwei Stunden damit verbringt, herauszufinden, wie eine geringfügige Änderung bei ihm zu einem Fehler geführt hat. scheint nicht verbunden. In komplexeren Situationen kann dies jedoch zu einem „Absturz“ des Projekts in der Produktion führen. Es gibt jedoch keine universellen Möglichkeiten, mit Linter solche Probleme zu bekämpfen oder zu verhindern.

Die Wurzel des Problems liegt in zwei Faktoren, deren Wechselwirkung verheerende Folgen hat:

  1. Mit Python können Module beliebige und unsichere Nebenwirkungen haben, die beim Import auftreten.
  2. Die Importreihenfolge des Codes ist nicht explizit festgelegt und wird nicht gesteuert. Im Projektmaßstab besteht eine Art „umfassender Import“ aus den Importbefehlen, die in allen Modulen enthalten sind. In diesem Fall kann die Importreihenfolge der Module abhängig vom Eingabepunkt des verwendeten Systems variieren.

Fortsetzung folgt…

Sehr geehrte Leser! Haben Sie Probleme mit dem langsamen Start von Python-Projekten?


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


All Articles